[僕のlonicでスマホアプリ開発]#5 残高確認画面作成

# はじめに
前回の[[僕のlonicでスマホアプリ開発]#4 残高登録画面作成](https://qiita.com/SSKNOK/items/606081cbc9e72aea8a73)続きになります。
今回作成するスマホアプリは画面が以下の通りになる想定ですが、
- 残高確認画面
- 残高登録画面
- 口座登録画面
- データインポート/エクスポート画面

今回は`残高確認画面`を実装していきます。

# 残高画面開発
## 入力項目

- 銀行名
- 口座名（セレクトボックス）
- 登録年月日（FROM）
- 登録年月日（TO）

を想定

## 詰まったポイント

この画面も詰まったポイントは特になかったですが、
強いて言うなら↓の固定ボタンを初実装だったのでそれくらいですかね
<img width=300 src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549748/7ebc4ae5-4b90-8ec0-2cc1-3226efeb621d.png>

といってもリファレンス読んで普通に実装しただけで特に詰まったわけではなかったんですが。

```vue:src/views/View.vue
<template>
// ...
    <ion-button slot="fixed" @click="scrollToTopOrBottom">{{
            scrollButtonLabel
          }}</ion-button>
</template>
// ...

<script setup>
    /* ----------------------------------
    method:スクロールトップorボトム
    detail:画面最上部もしくは最下部にスクロールする。
     ---------------------------------- */
    const scrollToTopOrBottom = function () {
      if (scrollToBottomFlag.value) {
        scrollButtonLabel.value = "↑";
        scrollToBottomFlag.value = false;
        const scrollContent = document.querySelector("ion-content.scrollContent");
        scrollContent.scrollToBottom(200);
      } else {
        scrollButtonLabel.value = "↓";
        scrollToBottomFlag.value = top;
        const scrollContent = document.querySelector("ion-content.scrollContent");
        scrollContent.scrollToTop(200);
      }
    };

</script>

```

# 最後にソース全量
<details><summary>Account.vue全量</summary>

```vue:src/views/View.vue
<template>
  <ion-page>
    <ion-header>
      <ion-toolbar>
        <ion-title>VIEW</ion-title>
      </ion-toolbar>
    </ion-header>
    <ion-content :fullscreen="true" class="scrollContent">
      <ion-header collapse="condense">
        <ion-toolbar>
          <ion-title size="large">VIEW</ion-title>
        </ion-toolbar>
      </ion-header>
      <!-- メインコンテンツ -->
      <!-- 銀行名入力欄 -->
      <ion-item>
        <ion-input
          required="true"
          label="BANK"
          label-placement="stacked"
          placeholder="SSK BANK"
          color="primary"
          :value="bank"
          @input="bank = $event.target.value"
        ></ion-input>
      </ion-item>
      <!-- 口座名義人名入力欄 -->
      <ion-item>
        <ion-input
          required="true"
          label="ACCOUNT HOLDER"
          label-placement="stacked"
          placeholder="TARO SUZUKI"
          color="primary"
          :value="accountHolder"
          @input="accountHolder = $event.target.value"
        ></ion-input>
      </ion-item>
      <!-- 検索年月日（FROM） -->
      <ion-item>
        <ion-grid>
          <ion-row>
            <ion-col>
              <ion-input
                label="FROM DATE"
                label-placement="stacked"
                type="date"
                :value="fromDate"
                @input="fromDate = $event.target.value"
              >
              </ion-input>
            </ion-col>
            <!-- 検索年月日（TO）-->
            <ion-col>
              <ion-input
                label="TO DATE"
                label-placement="stacked"
                type="date"
                :value="toDate"
                @input="toDate = $event.target.value"
              >
              </ion-input>
            </ion-col>
          </ion-row>
        </ion-grid>
      </ion-item>
      <ion-grid>
        <ion-row>
          <!-- 検索ボタン -->
          <ion-col>
            <ion-button expand="block" color="success" @click="searchBalance"
              ><ion-icon :icon="searchOutline" slot="start"></ion-icon
              >SEARCH</ion-button
            >
          </ion-col>
          <!-- グラフボタン -->
          <ion-col>
            <ion-button expand="block" color="warning" @click="describeGraph"
              ><ion-icon :icon="analyticsOutline" slot="start"></ion-icon
              >GRAPH</ion-button
            >
          </ion-col>
        </ion-row>
      </ion-grid>
      <!-- 残高一覧表示エリア -->
      <ion-list v-for="balance in balanceList" :key="balance.key">
        <ion-item
          button
          color="light"
          @click="moveToBalanceEditPage(balance.no)"
        >
          <ion-label>
            <p>{{ balance.recordDate }}</p>
            <p>{{ balance.bank }}：{{ balance.accountHolder }}</p>
            <h3>{{ balance.balance }}</h3>
          </ion-label>
        </ion-item>
      </ion-list>
      <ion-button slot="fixed" @click="scrollToTopOrBottom">{{
        scrollButtonLabel
      }}</ion-button>
    </ion-content>
  </ion-page>
</template>

<script setup>
import {
  IonPage,
  IonHeader,
  IonToolbar,
  IonTitle,
  IonContent,
  IonItem,
  IonInput,
  IonGrid,
  IonRow,
  IonCol,
  onIonViewWillEnter,
  IonIcon,
  IonList,
  IonLabel,
  IonButton,
  useIonRouter,
} from "@ionic/vue";
import ExploreContainer from "@/components/ExploreContainer.vue";
import { searchOutline, analyticsOutline } from "ionicons/icons";
import { ref, onMounted } from "vue";
import { Storage } from "@ionic/storage";

/* ******** プロパティ ******** */
let bank = ref(); // 銀行名
let accountHolder = ref(); // 口座名義人
let balanceList = ref([]); // 表示残高リスト
let fromDate = ref(); // 対象日（FROM）
let toDate = ref(); // 対象日(TO)
let scrollButtonLabel = ref("↓"); // スクロールボタンラベル
let scrollToBottomFlag = ref(true); // スクロールボトムフラグ

let storage; // ストレージオブジェクト

const DATA_TYPE_FOR_ACCOUNT = "accountInfo"; // ストレージのデータタイプ（口座情報）
const KEY_PREFIX_FOR_ACCOUNT = "accountInfo_"; // ストレージのキーの接頭辞（口座情報）
const DATA_TYPE_FOR_BALANCE = "balanceInfo"; // ストレージのデータタイプ（残高情報）
const KEY_PREFIX_FOR_BALANCE = "balanceInfo_"; // ストレージのキーの接頭辞（残高情報）
const SEARCH_TEMPLATE = ".*{keyWord}.*";
const SEARCH_ALL = ".*";
const MINIMUM_FROM_DATE = "1900-01-01";
const MAXIMUM_TO_DATE = "9999-12-31";

/* ******** router準備 ******** */
const ionRouter = useIonRouter();

/* ******** ライフサイクルフック ******** */
onIonViewWillEnter(async function () {
  // ストレージオブジェクト生成
  const store = new Storage();
  storage = await store.create();
  // 残高情報読み込み
  selectBalance(null, null, null, null);
});

/* ******** メソッド ******** */
/* ----------------------------------
method:残高情報選択
detail:検索条件にあった残高情報を取得する
 ---------------------------------- */
const searchBalance = function () {
  selectBalance(bank.value, accountHolder.value, fromDate.value, toDate.value);
};

/* ----------------------------------
method:残高情報選択
detail:検索条件にあった残高情報を取得する
 ---------------------------------- */
const selectBalance = async function (bank, accountHolder, fromDate, toDate) {
  // 検索条件の組み立て
  // 銀行
  let searchBank = checkEmptyValue(bank)
    ? SEARCH_ALL
    : SEARCH_TEMPLATE.replaceAll("{keyWord}", bank);
  // 口座名義人
  let searchAccountHolder = checkEmptyValue(accountHolder)
    ? SEARCH_ALL
    : SEARCH_TEMPLATE.replaceAll("{keyWord}", accountHolder); // 口座名義人
  // 対象日（FROM）
  let searchFromDate = checkEmptyValue(fromDate) ? MINIMUM_FROM_DATE : fromDate;
  // 対象日（TO）
  let searchToDate = checkEmptyValue(toDate) ? MAXIMUM_TO_DATE : toDate;

  // 口座情報のリストを生成
  let accountList = [];
  storage.forEach((value, key) => {
    let infoJson = JSON.parse(value);
    // データタイプがaccountInfoなら口座情報として読み込む
    if (infoJson.dataType === DATA_TYPE_FOR_ACCOUNT) {
      accountList.push({
        key: key,
        bank: infoJson.bank,
        accountHolder: infoJson.accountHolder,
      });
    }
  });

  // 残高情報のリストを生成
  balanceList.value = [];
  storage
    .forEach((value, key) => {
      let infoJson = JSON.parse(value);

      // accountKeyから口座情報を取得して銀行と口座名義人を取得
      let accountInfoBank = "";
      let accountInfoAccountHolder = "";
      accountList.forEach((account) => {
        if (account.key === infoJson.accountKey) {
          accountInfoBank = account.bank;
          accountInfoAccountHolder = account.accountHolder;
          return true;
        }
      });

      // データタイプがbalanceInfoなら残高情報として読み込む
      if (infoJson.dataType === DATA_TYPE_FOR_BALANCE) {
        let balanceData = {
          key: key,
          accountKey: infoJson.accountKey,
          no: infoJson.no,
          bank: accountInfoBank,
          accountHolder: accountInfoAccountHolder,
          recordDate: infoJson.recordDate,
          balance: infoJson.balance,
        };
        // 表示するか判定
        if (
          checkBalanceInfoMatchCondition(
            balanceData,
            searchBank,
            searchAccountHolder,
            searchFromDate,
            searchToDate
          )
        ) {
          balanceList.value.push(balanceData);
        }
      }
    })
    .then(() => {
      balanceList.value.sort(function (first, second) {
        const firstRecordDate = new Date(first.recordDate);
        const secondRecordDate = new Date(second.recordDate);
        if (firstRecordDate > secondRecordDate) {
          return -1;
        } else if (firstRecordDate < secondRecordDate) {
          return 1;
        } else {
          return 0;
        }
      });
    });
};

/* ----------------------------------
method:残高情報条件チェック
detail:残高情報が検索条件に合致する場合はTrueを返却する
 ---------------------------------- */
const checkBalanceInfoMatchCondition = function (
  balanceData,
  searchBank,
  searchAccountHolder,
  searchFromDate,
  searchToDate
) {
  // 銀行チェック
  const searchBankRegExp = new RegExp(searchBank);
  if (!searchBankRegExp.test(balanceData.bank)) {
    return false;
  }
  // 口座名義人チェック
  const searchAccountHolderRegExp = new RegExp(searchAccountHolder);
  if (!searchAccountHolderRegExp.test(balanceData.accountHolder)) {
    return false;
  }
  // 対象日（FROM）
  if (new Date(balanceData.recordDate) < new Date(searchFromDate)) {
    return false;
  }
  // 対象日（TO）
  if (new Date(balanceData.recordDate) > new Date(searchToDate)) {
    return false;
  }
  return true;
};

/* ----------------------------------
method:空文字・NULL・undefined判定
detail:空文字・NULL・undefinedの場合、trueを返す。
 ---------------------------------- */
const checkEmptyValue = function (targetValue) {
  if (
    targetValue === null ||
    targetValue === "" ||
    targetValue === undefined ||
    targetValue === "undefined"
  ) {
    return true;
  }
  return false;
};

/* ----------------------------------
method:スクロールトップorボトム
detail:画面最上部もしくは最下部にスクロールする。
 ---------------------------------- */
const scrollToTopOrBottom = function () {
  if (scrollToBottomFlag.value) {
    scrollButtonLabel.value = "↑";
    scrollToBottomFlag.value = false;
    const scrollContent = document.querySelector("ion-content.scrollContent");
    scrollContent.scrollToBottom(200);
  } else {
    scrollButtonLabel.value = "↓";
    scrollToBottomFlag.value = top;
    const scrollContent = document.querySelector("ion-content.scrollContent");
    scrollContent.scrollToTop(200);
  }
};

/* ----------------------------------
method:残高編集画面遷移
detail:残高編集画面に遷移する。
 ---------------------------------- */
const moveToBalanceEditPage = function (balanceNo) {
  ionRouter.push("balance/" + String(balanceNo));
};

/* ----------------------------------
method:残高編集画面遷移
detail:残高編集画面に遷移する。
 ---------------------------------- */
const describeGraph = function () {
  // 各検索条件の組み立て
  let searchBank = checkEmptyValue(bank.value) ? ".*" : bank.value;
  let searchAccountHolder = checkEmptyValue(accountHolder.value) ? ".*" : accountHolder.value;
  let searchFromDate = checkEmptyValue(fromDate.value) ? "1900-01-01" : fromDate.value;
  let searchToDate = checkEmptyValue(toDate.value) ? "9999-12-31" : toDate.value;

    ionRouter.push(
      "graph/" +
        String(searchBank) +
        "/" +
        String(searchAccountHolder) +
        "/" +
        String(searchFromDate) +
        "/" +
        String(searchToDate)
    );
};
</script>

<style scoped>
ion-button[slot="fixed"] {
  bottom: 0%;
  right: 0px;
}
</style>
```
</details>