はじめに
前回の[[僕のlonicでスマホアプリ開発]#6 データインポート/エクスポート画面作成(https://qiita.com/SSKNOK/items/2e71535b154df4670a80)続きになります。
今回作成するスマホアプリは画面が以下の通りになる想定ですが、
- 残高確認画面
- 残高登録画面
- 口座登録画面
- データインポート/エクスポート画面
今回は残高グラフ画面
を実装します。
「いや、そんな画面無いじゃん」って感じですが、隠し機能って感じですね。
前回開発した残高確認画面にくっついてる「グラフ」ボタンを押下したら遷移して画面を表示するイメージ。
残高画面開発
入力項目
入力項目は特に想定してないですが、パスパラメータで
- 銀行名
- 口座名義人名
- 検索日FROM
- 検索日TO
を受け取ります。
詰まったポイント
パスパラメータ
詰まったポイントではないんですが、他の画面と違って画面遷移時のパラメータがあるのがちょい癖ポイント1つ目です。
何か他に良いパラメータの渡し方はなかったのか。
パラメータを渡すときはvue-router
のindex.js
にパスパラメータの定義を行います。
src/router/index.js
import { createRouter, createWebHistory } from '@ionic/vue-router';
import TabsPage from '../views/TabsPage.vue'
const routes = [
{
path: '/',
redirect: '/tabs/view'
},
{
path: '/tabs/',
component: TabsPage,
children: [
{
path: '',
redirect: '/tabs/view'
},
{
path: 'view',
component: () => import('@/views/View.vue')
},
{
path: 'balance/:balanceNo',
component: () => import('@/views/Balance.vue')
},
{
path: 'account',
component: () => import('@/views/Account.vue')
},
{
path: 'importExport',
component: () => import('@/views/importExport.vue')
},
{
path: 'graph/:bank/:accountHolder/:fromDate/:toDate', //←ココ
component: () => import('@/views/Graph.vue')
}
]
}
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
export default router
グラフ描画処理
createGraph
で描画するデータをコネコネして、最後にdescribeGraph
に渡すことでグラフを描画しています。
グラフの描画にはChart.js
を使っています。
Chart.js
はとりあえずグラフが描画できればそれでよかったので、あんま深く調べず、コチラをまんまコピって使いました。
src/views/Graph.vue
<script setup>
/* ----------------------------------
method:グラフ作成。
detail:グラフに必用なデータを取得し、グラフを描画する。
---------------------------------- */
const createGraph = 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,
});
}
});
// 日付別に残高を合算
const recordDataMap = new Map();
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,
accountInfoBank: accountInfoBank,
no: infoJson.no,
recordDate: infoJson.recordDate,
balance: infoJson.balance,
bank: accountInfoBank,
accountHolder: accountInfoAccountHolder,
};
// 表示対象の場合は日付別に合算する。
if (
checkBalanceInfoMatchCondition(
balanceData,
searchBank,
searchAccountHolder,
searchFromDate,
searchToDate
)
) {
if (recordDataMap.has(balanceData.recordDate)) {
let newBalance =
Number(infoJson.balance) +
Number(recordDataMap.get(balanceData.recordDate));
recordDataMap.set(balanceData.recordDate, newBalance);
} else {
recordDataMap.set(balanceData.recordDate, infoJson.balance);
}
}
}
})
.then(() => {
let graphData = []; // 描画用のデータ
recordDataMap.forEach((value, key) => {
graphData.push({ x: key, y: value });
});
//データを時系列順に並べ替え
graphData.sort(function (first, second) {
const firstRecordDate = new Date(first.x);
const secondRecordDate = new Date(second.x);
if (firstRecordDate < secondRecordDate) {
return -1;
} else if (firstRecordDate > secondRecordDate) {
return 1;
} else {
return 0;
}
});
describeGraph(graphData);
});
};
/* ----------------------------------
method:グラフ描画
detail:受け取った値のリストに対するグラフを描画する。
---------------------------------- */
const describeGraph = function (data) {
// test
new Chart("chart", {
type: "line",
data: {
datasets: [
{
label: "BALANCE GRAPH",
backgroundColor: "rgb(255, 99, 132)",
borderColor: "rgb(255, 99, 132)",
fill: false,
data: data,
},
],
},
options: {
aspectRatio: 0.6,
scales: {
xAxes: [
{
type: "time",
time: {
unit: "day",
},
},
],
},
},
});
};
</script>
ソース全量
Account.vue全量
src/views/Graph.vue
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>GRAPH</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">GRAPH</ion-title>
</ion-toolbar>
</ion-header>
<canvas id="chart"></canvas>
</ion-content>
</ion-page>
</template>
<script setup>
import {
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonContent,
IonGrid,
IonRow,
IonCol,
IonIcon,
IonButton,
IonItem,
IonTextarea,
onIonViewWillEnter,
alertController,
} from "@ionic/vue";
import { arrowDownOutline, arrowUpOutline } from "ionicons/icons";
import ExploreContainer from "@/components/ExploreContainer.vue";
import { useRoute } from "vue-router";
import { ref, onMounted } from "vue";
import { Storage } from "@ionic/storage";
import { Chart, registerables } from "chart.js";
/* ******** プロパティ ******** */
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";
const TIME_DEFAULT = "T00:00:00";
/* ******** router準備 ******** */
const route = useRoute();
/* ******** ライフサイクルフック ******** */
onIonViewWillEnter(async function () {
// ストレージオブジェクト生成
const store = new Storage();
storage = await store.create();
const bank = String(route.params["bank"]);
const accountHolder = String(route.params["accountHolder"]);
const fromDate = String(route.params["fromDate"]);
const toDate = String(route.params["toDate"]);
searchBalance(bank, accountHolder, fromDate, toDate);
});
/* ******** メソッド ******** */
/* ----------------------------------
method:残高情報選択
detail:検索条件にあった残高情報を取得する
---------------------------------- */
const searchBalance = function (bank, accountHolder, fromDate, toDate) {
createGraph(bank, accountHolder, fromDate, toDate);
};
/* ----------------------------------
method:グラフ作成。
detail:グラフに必用なデータを取得し、グラフを描画する。
---------------------------------- */
const createGraph = 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,
});
}
});
// 日付別に残高を合算
const recordDataMap = new Map();
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,
accountInfoBank: accountInfoBank,
no: infoJson.no,
recordDate: infoJson.recordDate,
balance: infoJson.balance,
bank: accountInfoBank,
accountHolder: accountInfoAccountHolder,
};
// 表示対象の場合は日付別に合算する。
if (
checkBalanceInfoMatchCondition(
balanceData,
searchBank,
searchAccountHolder,
searchFromDate,
searchToDate
)
) {
if (recordDataMap.has(balanceData.recordDate)) {
let newBalance =
Number(infoJson.balance) +
Number(recordDataMap.get(balanceData.recordDate));
recordDataMap.set(balanceData.recordDate, newBalance);
} else {
recordDataMap.set(balanceData.recordDate, infoJson.balance);
}
}
}
})
.then(() => {
let graphData = []; // 描画用のデータ
recordDataMap.forEach((value, key) => {
graphData.push({ x: key, y: value });
});
//データを時系列順に並べ替え
graphData.sort(function (first, second) {
const firstRecordDate = new Date(first.x);
const secondRecordDate = new Date(second.x);
if (firstRecordDate < secondRecordDate) {
return -1;
} else if (firstRecordDate > secondRecordDate) {
return 1;
} else {
return 0;
}
});
describeGraph(graphData);
});
};
/* ----------------------------------
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:グラフ描画
detail:受け取った値のリストに対するグラフを描画する。
---------------------------------- */
const describeGraph = function (data) {
// test
new Chart("chart", {
type: "line",
data: {
datasets: [
{
label: "BALANCE GRAPH",
backgroundColor: "rgb(255, 99, 132)",
borderColor: "rgb(255, 99, 132)",
fill: false,
data: data,
},
],
},
options: {
aspectRatio: 0.6,
scales: {
xAxes: [
{
type: "time",
time: {
unit: "day",
},
},
],
},
},
});
};
</script>
<style scoped>
</style>