GAS(Google Apps Script)でGoogleフォームを作成-選ばれた選択肢でメール送信先を変更

つい最近、Google Apps Script(以下GAS)というGoogleフォームを操作・カスタマイズできる機能を知ったので、備忘録。

GAS公式 https://developers.google.com/apps-script

今回やりたいこと

  • Googleフォームで用意した選択肢の内容によって、連絡先を変更する。

店舗をいくつか持っている会社があるとしましょう。ユーザからの問い合わせは選択された店舗の担当者に対応してもらいたいので、回答は店舗担当者に送信されるようにカスタマイズします。

 

そもそもGASとはなんぞ

公式の日本語ドキュメントがないので(ないよね?)アレルギー起こしている人も多いのではないかと思います。

冒頭ではGoogle Formを操作できる〜と申し上げましたが、公式を見ると他の色々なGoogleアプリを操作・カスタマイズできる機能のようですね。以下公式から引用。

Google Apps Script is a rapid application development platform that makes it fast and easy to create business applications that integrate with Google Workspace. You write code in modern JavaScript and have access to built-in libraries for favorite Google Workspace applications like Gmail, Calendar, Drive, and more. There’s nothing to install—we give you a code editor right in your browser, and your scripts run on Google’s servers.

GメールやGoogleカレンダー、ドライブ、といったGoogleのワークスペースと統合できるアプリ開発のプラットフォーム、とありますね。アプリのインストール不要、ブラウザ上で開発できて、みんな大好きJavaScriptで書けるので便利だよ!というわけです。

ご丁寧に、JavaScriptをご存じない方にはレッスンの案内もされています。

Codecademy https://www.codecademy.com/catalog/language/javascript

GASで具体的に何ができるのか

こちらも公式からの引用ですが、

  • GoogleドキュメントやGoogleスプレッドシート、Googleフォームでのオリジナルメニュー、ダイアログ、サイドバーの作成
  • Googleスプレッドシートでのオリジナル機能、マクロの作成
  • Webアプリの開発…Googleサイトでの独立した、あるいは埋め込みのアプリを作ってローンチ
  • Google AdSenseやGoogleカレンダー、Googleドライブ、Gメール、Googleマップとの連携
  • Google Workspace Marketplaceでアドオンを作成して公開

(意訳ですので参考程度でお願いします)

Googleフォームを作成

できることは色々あれど、今回はこのGASを使ってGoogleフォームをカスタマイズ。

  • Googleフォームで用意した選択肢の内容によって、連絡先を変更する

のですが、細分化するとこんな感じ

  1. Googleフォームの送信
  2. 回答者にメール送信
  3. 管理者側へのメール送信(回答の選択肢によってメール振り分け)

1,2まではGAS使わなくてもできますね。ただ、GASで用意しておくと同じフォームを秒で作れるようになるのでその点でも便利です。

Google フォームを作成

今回は、Googleフォームで受け取った値を元に色々操作してみたいと思いますので、一旦Googleフォームを作成して、返ってくる値を確かめてみようと思います。

フォームの内容はこんな感じ

冒頭のメールアドレス設定はタブの設定>回答>メールアドレスを収集するのトグルをONにするとできます。

GASを作成

フォームのメニュー3点リーダからスクリプトメニューを選択します。

初期状態はこれ。

function myFunction(e) {

}

function myFunction() {}とありますね。この{}の中に、カスタマイズしたい処理を書いていきます。myFunctionの部分は、好きな名前に変更してもOKです。今回はフォーム送信時に、連絡のない店舗へお知らせが届くようにしたいのでautoNotifyとします。

ここで()内に引数(今回はeとします。eventのeですが、なんでもOKです)を入れておくと、送信ボタンが押された際に、回答データがeに入り、あらかじめ用意されている関数onSubmitに渡されます。では実際にどのような内容が入っているのか確認してみましょう。

function autoNotify(e) {
  console.log(e)
}

Google フォームとGASを紐付け

Googleフォームの送信ボタンが押された時にスクリプトが実行されるよう、Google フォームとGASを紐付けします。

トリガーを設定

左のサイドメニュー時計アイコンのトリガーを選択し、

トリガーを追加。

トリガーの設定内容は以下の通り。設定できたら保存ボタンを押下します。

  • 実行する関数を選択 -> autoNotify
  • 実行するデプロイを選択 -> Head
  • イベントのソースを選択 -> フォームから
  • イベントの種類を選択 -> フォーム送信時
  • エラー通知設定 -> 毎日通知を受け取る

保存ボタンを押すとGASを承認するためのポップアップが出ますので、任意のGoogleアカウントを選択して次に進んでください。

これでトリガーが設定できました。

フォームが送信された時に入っている値を見てみよう

先ほどGASで書いたeの部分を確認してみましょう。実際にフォームを入力して、送信します。

フォーム画面右上の送信ボタンから、フォーム回答のための共有リンクを取得できます。

個人の別Googleアカウントからフォームを入力し、回答を送信しました。

GASの編集画面に戻り、左サイドメニュー実行数から、ログを確認できます。

オブジェクトで色々な値が取れていますね。

ちなみにconsole.logではなくLogger.logを使うとオブジェクトが返ってきていることしかわかりません。

console.logとLogger.logの使い分けは下の記事で詳しく解説していただいていました(謝謝!)。

GASでログ出力する2つの方法(Logger.logとconsole.log)の紹介と使い分け

回答データeはこのようにオブジェクトで返ってきているので、その返ってきている値から必要な情報を取り出し、処理を書きます。

今回は、

2. 回答者にメール送信
3. 管理者側へのメール送信(回答の選択肢によってメール振り分け)

をするので、回答者のメールアドレスと、選択された店舗情報を取り出す必要があります。回答内容は、Googleフォーム送信ボタンを押して返ってきた情報(e)の中の、response内、getItemResponses部分から取得できます(e.response.getItemResponses)。

{
toString: [Function],
authMode: {
toString: [Function: toString],
name: [Function: toString],
toJSON: [Function: toString],
ordinal: [Function: ordinal],
compareTo: [Function: compareTo],

// ちょっと省略

FULL: [Circular]
},
response: {
toString: [Function],
submit: [Function],
getId: [Function],
getTimestamp: [Function],
getRespondentEmail: [Function],
getItemResponses: [Function],
getGradableItemResponses: [Function],
getResponseForItem: [Function],
getGradableResponseForItem: [Function],
withItemResponse: [Function],
withItemGrade: [Function],
toPrefilledUrl: [Function],
getEditResponseUrl: [Function]
},

// だいぶ省略

triggerUid: '11025413'
}

フォームのレスポンス(e.response)から値を取り出すメソッドは公式にまとめられています。

https://developers.google.com/apps-script/reference/forms/form-response

getItemResponses() ItemResponse[] Gets all item responses contained in a form response, in the same order that the items appear in the form.

*フォームから受け取った値の全てのresponseアイテムを取得。値はフォームに入っているのと同様の順番で格納されています。

便利ですね〜

では早速これで値を取り出して見てみましょう。

GASのエディタに戻り、コードを書き換えます。

function autoNotify(e) {
  // 質問内容と回答を取得
  const itemResponse = e.response.getItemResponses();

  console.log(itemResponse);
}

スクリプトを保存(⌘+s)したら、フォームをもう一度送信し、GASメニューのログを確認します。

[{
toString: [Function],
getScore: [Function],
setScore: [Function],
getFeedback: [Function],
getResponse: [Function],
getItem: [Function],
setFeedback: [Function]
},
{
toString: [Function],
getScore: [Function],
setScore: [Function],
getFeedback: [Function],
getResponse: [Function],
getItem: [Function],
setFeedback: [Function]
},
{
toString: [Function],
getScore: [Function],
setScore: [Function],
getFeedback: [Function],
getResponse: [Function],
getItem: [Function],
setFeedback: [Function]
},
{
toString: [Function],
getScore: [Function],
setScore: [Function],
getFeedback: [Function],
getResponse: [Function],
getItem: [Function],
setFeedback: [Function]
},
{
toString: [Function],
getScore: [Function],
setScore: [Function],
getFeedback: [Function],
getResponse: [Function],
getItem: [Function],
setFeedback: [Function]
}
]

配列の中に、それぞれの質問内容と回答の情報がオブジェクトに格納されています。

1つ目の質問・回答群から順に、配列の[0],[1],[2]…と格納されています。

最初のメールアドレスはフォーム作成時の設定内のトグルから作成したため、e.response.getItemResponsesとは別の場所に格納されています(後述)。

 

フォームで送信された質問と回答をログ出力してみる

では、空の配列 answerListを用意して、フォームから得られた質問と回答を入れてみましょう。

function autoNotify(e) {
  // 質問内容と回答を取得
  const itemResponse = e.response.getItemResponses();

  let answerList = [];

  // 答えた質問の数だけ繰り返し
  for (let i=0; i<itemResponse.length; i++) {
    let question = itemResponse[i].getItem().getTitle();
    let answer = itemResponse[i].getResponse();

    // 回答が入力されていればその値を、そうでなければ未回答を入れる
    answer ? answerList.push(question + ':' + answer)
      : answerList.push(question + ':' + '未回答');
  }
console.log(answerList);
}

スクリプトを保存(⌘+s)したら、フォームをもう一度送信し、GASメニューのログを確認します。

いい感じですね!

回答者にメール送信

フォームの回答ボタンが押されたらまず回答者にお礼メールを送りたいですね。

メール送信には、GmailAppというGメールアプリへのアクセスを提供するクラスを使用します。メソッド一覧はこちら

https://developers.google.com/apps-script/reference/gmail/gmail-app

上記リファレンスのsendEmailメソッドを使います。

まずはsubjectに件名、bodyに本文を定義します。

// 回答者にメール送信
// 回答者のメールアドレスを取得
const recipient = e.response.getRespondentEmail();
// 件名
const subject = '【回答者側 自動通知】お問い合わせ受付完了'
 
// 本文
const body = 'お問い合わせ受付が完了しました。\n\n内容にお間違いがないかご確認頂きますようお願いいたします。\n\n============\n' + answerList.join('\n') + '\n============\n';

その下にメソッドを追加

Gmailapp.sendEmail(recipient, subject, body);

実はGASのスクリプト内でGmailApp.sendEmail(...と入力すると文法も出てきてくれます。便利!

再びフォームを入力してデバッグしましょう!

スクリプトを保存(⌘+s)したら、フォームをもう一度送信し、スクリプトを保存(⌘+s)したら、入力したメールアドレスに届いたメールを確認します。

私はこの際、マニフェストを登録しておらず引っかかりました。ログから下記のようなエラーが出る場合は、マニフェストを追記しましょう。

やり方は以下の記事にまとめました。

GASのメール送信ができない…それならマニフェスト設定だ(appsscript.json)

のですが、調べていたらどうやらGmailAppよりMailAppの方が良さそうだ(配信速度が速いとかなんとか)…ということでメール送信部分は下記のようにしました。

Mailapp.sendEmail(recipient, subject, body);

管理者側へのメール送信(回答の選択肢によってメール振り分け)

さて、メール送信ができたところで今度は、設問1で選ばれた値によって、メール送信先の店舗先を変える…という機能を追加します。

設問1

設問1はitemResponse[0]に格納されています。getResponseメソッドでユーザーの選択した回答を取得し、mailGetShopに代入しました。Array.isArrayで、返ってきた値が配列であることを確認します。

// 管理者側へのメール送信(回答の選択肢によってメール振り分け)
let mailGetShop = itemResponse[0].getResponse();

// console.log(mailGetShop); // ['◯◯工務店','□□ハウス']
// console.log(Array.isArray(mailGetShop)); // true

今回は試しに'◯◯工務店','□□ハウス'の2店舗にチェックを入れて、テストをします。店舗ごとに、名前とメールアドレスがセットになったオブジェクトを作成し、配列shopListに代入。

今回は'◯◯工務店','□□ハウス'の2つがチェックされたので、hogehoge@hogemail.comunchi@hogemail.comにメールが配信されるようにしたいです。

let adminRecipient = [];
const shopList = [
{'◯◯工務店':'hogehoge@hogemail.com'},
{'□□ハウス':'unchi@hogemail.com'},
{'△△不動産':'nekoinochi@hogemail.com'}
];

shopListの数だけ、mailGetShopに入っている値を検索します。やり方はいろいろありましょうが…私は、indexOfで検索した値がなかった場合は-1を返すという特性を利用して、trueだった時にmailGetShopのメールアドレスをadminRecipientへpushする方法を取りました。


for (let i=0; i<shopList.length;i++) {
let target = Object.keys(shopList[i]).toString();
let result = mailGetShop.indexOf(target);
0<=result ? adminRecipient.push(Object.values(shopList[i])): null;
}
console.log(adminRecipient);  // ['hogehoge@hogemail.com','unchi@hogemail.com']

次は、adminRecipient内のアドレスへお知らせメールを送信します。これは先ほどの、回答者へのメール送信と同様ですね。

// 管理者へのメール文
let adminSubject = "【管理者側 自動通知】Googleフォームで新規回答を受け付けました";

let adminBody = 'Googleフォームで新規回答を受け付けました。\n\n内容を確認しご対応をお願いいたします。\n\n============\n'
+ answerList.join('\n')
+ '\n============\n';

//管理者にメール送信
MailApp.sendEmail(strAdminRecipient, adminSubject, adminBody);

ただ、先ほど得られたメールアドレスですが、配列の方がその後色々いじりやすいかと思っていたものの、MailApp.sendEmail()のrecipient(第一引数)はカンマ区切りのStringで渡さなければいけないらしいです。

recipient String the addresses of the recipients, separated by comma

https://developers.google.com/apps-script/reference/mail/mail-app#sendemailrecipient,-subject,-body

というわけで、MailApp.sendEmail(strAdminRecipient, adminSubject, adminBody);の前に以下を1行足します。

let strAdminRecipient = adminRecipient.join(',');

コードがかけたら例に倣って保存し、フォームを送信してデバッグ内容を確認します。

無事通りましたか?通ってくれ!

 

[Nuxt.js]フィルターを作成する

Nuxtのファイル構成を意識したNuxt.jsでのフィルターの作成方法です。Vueのやり方だと上手く行かなかったため、備忘録。

グローバルフィルター

pluginsディレクトリ内にjsフォルダを作成し、どのページでもフィルタが読み込めるようにする方法です。今回は、数字を3桁ごとにカンマで区切るフィルタと、英語の文字列を全て大文字にするフィルタを作成します。

詳しく

まずはpagesディレクトリ内にfilter.vueの雛形を作成します。

<template>
  <div>
    <Header />
    <v-container>
      <!-- local filter -->
      <h2>Mustash</h2>
      <p>{{ price | numberFormat }}</p>
      <p>{{ text | toUpperCase }}</p>
      <h2>b-vind</h2>
      <input type="text" :value="price | numberFormat" />
      <input type="text" :value="text | toUpperCase" />
    </v-container>
  </div>
</template>


<script>
export default {
    data() {
      return {
        price: 25400,
        text: 'Hello, Nuxt.js!',
    }
  },
}
</script>
グローバルフィルターはpluginsディレクトリ内にjsフォルダを作成し、その中にフィルターの機能を記述します。
import Vue from 'vue'

Vue.filter('toUpperCase', (value) => {
  return value.toUpperCase()
})

Vue.filter('numberFormat', (value) => {
  return value.toLocaleString()
}

最後に、nuxt.config.jsにグローバルフィルタの場所を追記します。

export default {
  plugins: [ { src: '~plugins/filter.js' } ],
}

ローカルフィルター

ローカルフィルターは、pagesディレクトリ内の各ページに定義するそのページ限定のフィルターです。せっかくなので、次は長い文字列を短く表現し、省略した以降の文字は「…」と表現させる「引数あり」のフィルターを作成します。

長文
これを
こう

詳しく

まずは雛形を作成します。

<template>
  <div>
    <v-container>
      <p>{{ longText }}</p>
    </v-container>
  <Footer />
  </div>
</template>

<script>
export default {
  data() {
    return {
      longText:
        'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
    }
  },
}
</script>

この時点ではこれです。

フィルターを加えます。

<script>
export default {
  filters: {
    readMore: (text, length, suffix) => {
      return text.substring(0, length) + suffix
    },
  }

  data() {
   // 省略
}
</script>

readMoreの第一引数が適用する文字列、lengthが長さ、suffixが接尾辞です。

文字列textに対し.substringメソッドで0番目の文字から第二引数で渡した文字数(length)の、文字列の部分集合を返し、最後に指定したsuffixを加える、という処理が書かれています。

上記フィルターをlongTextに適用すると、コード全文は下記のようになります。

<template>
  <div>
    <v-container>
      <p>{{ longText | readMore(20, '...') }}</p> // フィルタを適用
    </v-container>
  <Footer />
  </div>
</template>

<script>
export default {
  filters: {
    readMore: (text, length, suffix) => {
      return text.substring(0, length) + suffix
    },
  }

  data() {
    return {
      longText:
        'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
    }
  },
}
</script>

フィルターでは第三引数まで指定していたのに、textの引数は書かれていないことに注意してください。

 

フィルターはv-bind式でもできる

mustache構文を使わないでフィルターを適用させたい時もあるかと思います。2.1.0以降でサポート。

https://jp.vuejs.org/v2/guide/filters.html

<!-- mustaches -->
{{ message | capitalize }}

<!-- v-bind -->
<div v-bind:id="rawId | formatId"></div>

v-bind式の構文は、

<div v-bind:id=”フィルタに渡すデータ | フィルタ”></div>

です。

 

おしまい

[Nuxt.js]AccuWeatherのAPIを使ってお天気アプリを制作

Nuxt.jsでAPIを使う修行を自分に課していました。というわけで、今回はお天気アプリです。

トップページのカードに、このように表示させます。

取ってきている情報自体は、任意の場所のその日のお天気と気温(と日付)だけなのですが、なかなかどうして時間のかかったものです。さて、復習しますか。

雛形となるカードを用意

まずは雛形を用意。お馴染みVuetifyを使っています。

<template>
  <div>
    <v-card class="mx-auto" max-width="374">
      <v-img
        height="250"
        src="https://images.unsplash.com/photo-1558486012-817176f84c6d?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8"
        gradient="to top, rgba(255,255,255,.4), rgba(255,255,255,.9)"
      >
        <v-list-item two-line>
          <v-list-item-content>
            <v-list-item-title class="text-h5">
              CURRENT WEATHER
            </v-list-item-title>
            <v-list-item-subtitle>YYYY-MM-DD</v-list-item-subtitle> // 今日の日付を取ってくる
            <v-list-item-subtitle>TOKYO</v-list-item-subtitle>
          </v-list-item-content>
        </v-list-item>

        <v-card-text>
          <v-row align="center">
            <v-col text-h2 cols="12">
              <v-list-item-subtitle>お天気ステータス</v-list-item-subtitle>
            </v-col>
            <v-col cols="12">
              <v-card-subtitle class="text-h1 temp">温度
                <span class="celcius">&#8451;</span>
              </v-card-subtitle>
            </v-col>
          </v-row>
        </v-card-text>
      </v-img>
    </v-card>
  </div>
</template>

AccuWeatherから情報を取ってくる

さてこのAccuWeather、ちょっと苦戦しました。アクセス1日50回(だったかな?)なら無料のお天気APIです。

https://developer.accuweather.com/

必要なことは大体公式サイトに載っているのですが、ここからお天気の情報を取ってくるには何段階かステップが必要です。

  1. AccuWeatherに登録する
  2. AccuWeatherでAppを作る
  3. APIキーゲト
  4. ロケーションキーをゲト

AccuWeatherに登録する

まずは公式サイトでアカウントを作成します。

https://developer.accuweather.com/

右上のREGISTERからですね。

AccuWeatherでAppを作る

諸々入力し終え、アカウントが作成できたら、メニューバーのMY APPからAdd a new Appを選択します。Appの名前とかはどうでもいいです。今回は無料版を使うため、Productの箇所ではLimited Trialを選択しましょう。

 

APIキーゲト

上記の設定を終えたらCreate Appボタンを押します。

APIキーが取得できました。

ロケーションキーをゲト

APIキーをゲットしたら次はロケーションキーが必要ですね。

こちらにアクセスするとロケーションキーがわかります。

https://www.accuweather.com/

検索窓から取得したいロケーションを入力します。「東京」や「サンフランシスコ」などで選択肢が表示されます。

ページ遷移した先のURL最後の数字がロケーションキーです。

AccuWeatherのメニューバーAPI REFERENCE > Forecast APIを選択し、取りたいデータを選択します。今回はHour of Hourly Forecastsを選択しました。

https://developer.accuweather.com/accuweather-forecast-api/apis/get/forecasts/v1/hourly/1hour/%7BlocationKey%7D

一番上のResource URLの部分に、お天気情報を取得するためのURLが書かれていますね。

http://dataservice.accuweather.com/forecasts/v1/hourly/1hour/{LOCATIONKEY}

この{LOCATIONKEY}の部分を先ほど取得した数値に置き換え、APIキーをつけてアクセスすると格納されている情報が見られるようになります。

例:http://dataservice.accuweather.com/forecasts/v1/hourly/1hour/226396?apikey=XXX

XXXの箇所は、先ほど取得した自分のAPIに置き換えてください。

アクセス先

Chromeの拡張機能JSON Viewを使うと見やすいです。

こちらのURLをスクリプトのmounted部分で使います。

取得したお天気情報をaxiosでゲットし、変数に代入する

 

APIキーはconstantsディレクトリのdefine.jsに記載しました。またaxiosはダウンロードしている前提です。インストール方法はこちらから

https://www.npmjs.com/package/axios

export const API_KEY = 'xxx'
<script>
import axios from 'axios'
import { API_KEY } from '~/constants/define'


export default {
  data() {
    return {
      forecasts: [],
    }
  },
  mounted() {
    axios
      .get(
        'http://dataservice.accuweather.com/forecasts/v1/hourly/1hour/226396' +
        `?apikey=${API_KEY}`
      )
      .then((response) => (this.forecasts = response.data))
  },
}
</script>

これで、先ほどアクセスした http://dataservice.accuweather.com/forecasts/v1/hourly/1hour/226396?apikey=XXX の情報をforecasts[]に代入することができました。

画面に天気を表示させてみます。カードにv-forを追加しました。

    <v-card-text v-for="forecast in forecasts" :key="forecast.id">
      <v-row align="center">
        <v-col text-h2 cols="12">
          <v-list-item-subtitle>{{
            forecast.IconPhrase
          }}</v-list-item-subtitle>
        </v-col>
        <v-col cols="12">
          <v-card-subtitle class="text-h1 temp">
            {{ forecast.Temperature.Value | toCelsius | mathFloor }}
            <span class="celcius">℃</span>
          </v-card-subtitle>
        </v-col>
      </v-row>
    </v-card-text>

気温は華氏表記だったため、フィルターを作成し摂氏表記にします。

        <v-col cols="12">
          <v-card-subtitle class="text-h1 temp">
            {{ forecast.Temperature.Value | toCelsius | mathFloor }}
            <span class="celcius">℃</span>
          </v-card-subtitle>
        </v-col>

Scriptには下記を追記

<script>

// 省略
export default {
  filters: {
    toCelsius(value) {
    return ((value - 32) * 5) / 9 // fahrenheit to celsius
    },
    mathFloor(value) {
    return Math.floor(value)
    },
  },

 

 

<script>
import axios from 'axios'
import { API_KEY } from '~/constants/define'


export default {
  filters: {
    toCelsius(value) {
    return ((value - 32) * 5) / 9
    },
    mathFloor(value) {
    return Math.floor(value)
    },
  },
  data() {
    return {
      forecasts: [],
      hasError: false,
      loading: true,
      now: '',
    }
  },
  mounted() {
    this.now = this.$dayjs().format('YYYY-MM-DD')
    axios
      .get(
        'http://dataservice.accuweather.com/forecasts/v1/hourly/1hour/226396' +
        `?apikey=${API_KEY}`
      )
      .then((response) => (this.forecasts = response.data))
      .catch(function (error) {
        console.log(error)
        this.hasError = true
      })
      .finally(() => (this.loading = false))
  },
}
</script>

あとは、dayjsを使い今日の日付を表示、error時とloading時の表示を追加して完成です。

 

      <v-list-item-content>
        <v-list-item-title class="text-h5">
          CURRENT WEATHER
        </v-list-item-title>
        <v-list-item-subtitle>YYYY-MM-DD</v-list-item-subtitle> // 今日の日付を取ってくる
        <v-list-item-subtitle>TOKYO</v-list-item-subtitle>
      </v-list-item-content>

    <v-card-text v-for="forecast in forecasts" :key="forecast.id">
      <section v-if="hasError">Error.</section>. // エラーメッセージを表示
      <section v-else> // エラーじゃない時は正常に表示
        <div v-if="loading">Loading...</div> // ローディング時の表示を追加
          <v-row align="center">
          </v-row>
        </section>
      </v-card-text>
 // 省略

export default {
 // 省略
  data() {
    return {
      forecasts: [],
      hasError: false,
      loading: true,
      now: '',
    }
  },
  mounted() {
    this.now = this.$dayjs().format('YYYY-MM-DD')
    axios
      .get(
        'http://dataservice.accuweather.com/forecasts/v1/hourly/1hour/226396' +
        `?apikey=${API_KEY}`
      )
      .then((response) => (this.forecasts = response.data))
      .catch(function (error) {
        console.log(error)
        this.hasError = true
      })
      .finally(() => (this.loading = false))
  },
}
</script>

完成コード

すみません所々 インデントが合ってないかもしれないです

<template>
  <div>
    <v-card class="mx-auto" max-width="374">
      <v-img
        height="250"
        src="https://images.unsplash.com/photo-1558486012-817176f84c6d?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8"
        gradient="to top, rgba(255,255,255,.4), rgba(255,255,255,.9)"
      >
      <v-list-item two-line>
        <v-list-item-content>
          <v-list-item-title class="text-h5">
            CURRENT WEATHER
          </v-list-item-title>
         <v-list-item-subtitle>{{ now }}</v-list-item-subtitle> // 今日の日付を取ってくる
         <v-list-item-subtitle>TOKYO</v-list-item-subtitle>
       </v-list-item-content>
     </v-list-item>

      <v-card-text v-for="forecast in forecasts" :key="forecast.id">
        <section v-if="hasError">Error.</section>
        <section v-else>
          <div v-if="loading">Loading...</div>
          <v-row align="center">
            <v-col text-h2 cols="12">
              <v-list-item-subtitle>{{
                forecast.IconPhrase
              }}</v-list-item-subtitle>
            </v-col>
            <v-col cols="12">
              <v-card-subtitle class="text-h1 temp">
                {{ forecast.Temperature.Value | toCelsius | mathFloor }}
                <span class="celcius">℃</span>
              </v-card-subtitle>
            </v-col>
           </v-row>
         </section>
        </v-card-text>
      </v-img>
    </v-card>
  </div>
</template>


<script>
import axios from 'axios'
import { API_KEY } from '~/constants/define'


export default {
filters: {
toCelsius(value) {
return ((value - 32) * 5) / 9
},
mathFloor(value) {
return Math.floor(value)
},
},
  data() {
    return {
      forecasts: [],
      hasError: false,
      loading: true,
      now: '',
    }
  },
  mounted() {
    this.now = this.$dayjs().format('YYYY-MM-DD')
    axios
      .get(
        'http://dataservice.accuweather.com/forecasts/v1/hourly/1hour/226396' +
        `?apikey=${API_KEY}`
      )
      .then((response) => (this.forecasts = response.data))
      .catch(function (error) {
        console.log(error)
        this.hasError = true
      })
      .finally(() => (this.loading = false))
  },
}
</script>


<style scoped>
.temp {
  margin-top: -2rem;
}
.celcius {
  font-size: 1.7rem;
}
</style>

参考にしたのはこちらのサイト

https://www.npmjs.com/package/iobroker.accuweather

https://www.accuweather.com/

アクセスした際は日本語になっている可能性があるため注意。

axiosのmounted部分、なんでアロー関数じゃなきゃダメなんだろう?と思っていたら、こういうことでした。

なので、functionの外でページのインスタンスであるthisを違う変数に突っ込むと、アロー関数じゃない書き方でもthisが使えるようになるようです。

mounted() {
  this.now = this.$dayjs().format('YYYY-MM-DD')
  const self = this
  axios
    .get(
      'http://dataservice.accuweather.com/forecasts/v1/hourly/1hour/226396' +
      `?apikey=${API_KEY}`
    )
    .then(function (response) {
      console.log(response)
      self.forecasts = response.data
})
// 以下省略
あるいは宣言せずに.bind(this)する
.then(
  function (response) {
    console.log(response)
    this.forecasts = response.data
  }.bind(this)
)