migrate income&expense overview card and monthly income&expense card to composition API and typescript

This commit is contained in:
MaysWind
2025-01-12 02:10:40 +08:00
parent 9bbe4d2dcf
commit 5cacfc8daf
6 changed files with 512 additions and 275 deletions
+5 -1
View File
@@ -3,7 +3,11 @@ import { ALL_CURRENCIES, DEFAULT_CURRENCY_SYMBOL } from '@/consts/currency.ts';
import { isString, isNumber } from './common.ts'; import { isString, isNumber } from './common.ts';
export function getCurrencyFraction(currencyCode: string): number | undefined { export function getCurrencyFraction(currencyCode?: string): number | undefined {
if (!currencyCode) {
return undefined;
}
const currencyInfo = ALL_CURRENCIES[currencyCode]; const currencyInfo = ALL_CURRENCIES[currencyCode];
return currencyInfo?.fraction; return currencyInfo?.fraction;
} }
+1 -1
View File
@@ -10,7 +10,7 @@ export function getSystemTheme(): ThemeType {
} }
} }
export function getExpenseAndIncomeAmountColor(expenseAmountColorType: number, incomeAmountColorType: number, isDarkMode: boolean): AmountColor { export function getExpenseAndIncomeAmountColor(expenseAmountColorType: number, incomeAmountColorType: number, isDarkMode?: boolean): AmountColor {
let expenseAmountColor = expenseAmountColorType ? PresetAmountColor.valueOf(expenseAmountColorType) : null; let expenseAmountColor = expenseAmountColorType ? PresetAmountColor.valueOf(expenseAmountColorType) : null;
let incomeAmountColor = incomeAmountColorType ? PresetAmountColor.valueOf(incomeAmountColorType) : null; let incomeAmountColor = incomeAmountColorType ? PresetAmountColor.valueOf(incomeAmountColorType) : null;
+224 -6
View File
@@ -17,20 +17,54 @@ import {
LongTimeFormat, LongTimeFormat,
ShortTimeFormat ShortTimeFormat
} from '@/core/datetime.ts'; } from '@/core/datetime.ts';
import { type LocalizedAccountCategory, AccountType, AccountCategory } from '@/core/account.ts';
import { TransactionEditScopeType, TransactionTagFilterType } from '@/core/transaction.ts'; import {
import { ScheduledTemplateFrequencyType } from '@/core/template.ts'; type NumberFormatOptions,
import { StatisticsAnalysisType, CategoricalChartType, TrendChartType, ChartDataType, ChartSortingType, ChartDateAggregationType } from '@/core/statistics.ts'; DecimalSeparator,
DigitGroupingSymbol,
DigitGroupingType
} from '@/core/numeral.ts';
import {
type CurrencyPrependAndAppendText,
CurrencyDisplayType
} from '@/core/currency.ts';
import {
type LocalizedAccountCategory,
AccountType,
AccountCategory
} from '@/core/account.ts';
import {
TransactionEditScopeType,
TransactionTagFilterType
} from '@/core/transaction.ts';
import {
ScheduledTemplateFrequencyType
} from '@/core/template.ts';
import {
StatisticsAnalysisType,
CategoricalChartType,
TrendChartType,
ChartDataType,
ChartSortingType,
ChartDateAggregationType
} from '@/core/statistics.ts';
import type { LocaleDefaultSettings } from '@/core/setting.ts'; import type { LocaleDefaultSettings } from '@/core/setting.ts';
import type { ErrorResponse } from '@/core/api.ts'; import type { ErrorResponse } from '@/core/api.ts';
import { ALL_CURRENCIES } from '@/consts/currency.ts';
import { KnownErrorCode, SPECIFIED_API_NOT_FOUND_ERRORS, PARAMETERIZED_ERRORS } from '@/consts/api.ts'; import { KnownErrorCode, SPECIFIED_API_NOT_FOUND_ERRORS, PARAMETERIZED_ERRORS } from '@/consts/api.ts';
import { import {
isObject,
isString, isString,
isNumber, isNumber,
isObject isBoolean
} from '@/lib/common.ts'; } from '@/lib/common.ts';
import { import {
@@ -40,8 +74,21 @@ import {
getDateTimeFormatType getDateTimeFormatType
} from '@/lib/datetime.ts'; } from '@/lib/datetime.ts';
import logger from '@/lib/logger.ts'; import {
appendDigitGroupingSymbol,
parseAmount,
formatAmount,
formatExchangeRateAmount,
getAdaptiveDisplayAmountRate
} from '@/lib/numeral.ts';
import {
getCurrencyFraction,
getAmountPrependAndAppendCurrencySymbol, appendCurrencySymbol
} from '@/lib/currency.ts';
import services from '@/lib/services.ts'; import services from '@/lib/services.ts';
import logger from '@/lib/logger.ts';
import { useUserStore } from '@/stores/user.ts'; import { useUserStore } from '@/stores/user.ts';
@@ -306,6 +353,30 @@ export function useI18n() {
return getLocalizedDateTimeFormat<ShortTimeFormat>('shortTime', ShortTimeFormat.all(), ShortTimeFormat.values(), userStore.currentUserShortTimeFormat, 'shortTimeFormat', ShortTimeFormat.Default); return getLocalizedDateTimeFormat<ShortTimeFormat>('shortTime', ShortTimeFormat.all(), ShortTimeFormat.values(), userStore.currentUserShortTimeFormat, 'shortTimeFormat', ShortTimeFormat.Default);
} }
function getNumberFormatOptions(currencyCode?: string): NumberFormatOptions {
return {
decimalSeparator: getCurrentDecimalSeparator(userStore.currentUserDecimalSeparator),
decimalNumberCount: getCurrencyFraction(currencyCode),
digitGroupingSymbol: getCurrentDigitGroupingSymbol(userStore.currentUserDigitGroupingSymbol),
digitGrouping: getCurrentDigitGroupingType(userStore.currentUserDigitGrouping),
};
}
function getCurrentCurrencyDisplayType(): CurrencyDisplayType {
let currencyDisplayType = CurrencyDisplayType.valueOf(userStore.currentUserCurrencyDisplayType);
if (!currencyDisplayType) {
const defaultCurrencyDisplayTypeName = t('default.currencyDisplayType');
currencyDisplayType = CurrencyDisplayType.parse(defaultCurrencyDisplayTypeName);
}
if (!currencyDisplayType) {
currencyDisplayType = CurrencyDisplayType.Default;
}
return currencyDisplayType;
}
// public functions // public functions
function translateIf(text: string, isTranslate: boolean): string { function translateIf(text: string, isTranslate: boolean): string {
if (isTranslate) { if (isTranslate) {
@@ -373,6 +444,20 @@ export function useI18n() {
return t(`currency.name.${currencyCode}`); return t(`currency.name.${currencyCode}`);
} }
function getCurrencyUnitName(currencyCode: string, isPlural: boolean): string {
const currencyInfo = ALL_CURRENCIES[currencyCode];
if (currencyInfo && currencyInfo.unit) {
if (isPlural) {
return t(`currency.unit.${currencyInfo.unit}.plural`);
} else {
return t(`currency.unit.${currencyInfo.unit}.normal`);
}
}
return '';
}
function getAllMeridiemIndicators(): LocalizedMeridiemIndicator { function getAllMeridiemIndicators(): LocalizedMeridiemIndicator {
const allMeridiemIndicators = MeridiemIndicator.values(); const allMeridiemIndicators = MeridiemIndicator.values();
const meridiemIndicatorNames = []; const meridiemIndicatorNames = [];
@@ -527,6 +612,51 @@ export function useI18n() {
return joinMultiText(finalWeekdayNames); return joinMultiText(finalWeekdayNames);
} }
function getCurrentDecimalSeparator(decimalSeparator: number): string {
let decimalSeparatorType = DecimalSeparator.valueOf(decimalSeparator);
if (!decimalSeparatorType) {
const defaultDecimalSeparatorTypeName = t('default.decimalSeparator');
decimalSeparatorType = DecimalSeparator.parse(defaultDecimalSeparatorTypeName);
if (!decimalSeparatorType) {
decimalSeparatorType = DecimalSeparator.Default;
}
}
return decimalSeparatorType.symbol;
}
function getCurrentDigitGroupingSymbol(digitGroupingSymbol: number): string {
let digitGroupingSymbolType = DigitGroupingSymbol.valueOf(digitGroupingSymbol);
if (!digitGroupingSymbolType) {
const defaultDigitGroupingSymbolTypeName = t('default.digitGroupingSymbol');
digitGroupingSymbolType = DigitGroupingSymbol.parse(defaultDigitGroupingSymbolTypeName);
if (!digitGroupingSymbolType) {
digitGroupingSymbolType = DigitGroupingSymbol.Default;
}
}
return digitGroupingSymbolType.symbol;
}
function getCurrentDigitGroupingType(digitGrouping: number): number {
let digitGroupingType = DigitGroupingType.valueOf(digitGrouping);
if (!digitGroupingType) {
const defaultDigitGroupingTypeName = t('default.digitGrouping');
digitGroupingType = DigitGroupingType.parse(defaultDigitGroupingTypeName);
if (!digitGroupingType) {
digitGroupingType = DigitGroupingType.Default;
}
}
return digitGroupingType.type;
}
function isLongDateMonthAfterYear() { function isLongDateMonthAfterYear() {
return getLocalizedDateTimeType(LongDateFormat.all(), LongDateFormat.values(), userStore.currentUserLongDateFormat, 'longDateFormat', LongDateFormat.Default).isMonthAfterYear; return getLocalizedDateTimeType(LongDateFormat.all(), LongDateFormat.values(), userStore.currentUserLongDateFormat, 'longDateFormat', LongDateFormat.Default).isMonthAfterYear;
} }
@@ -562,6 +692,84 @@ export function useI18n() {
} }
} }
function getNumberWithDigitGroupingSymbol(value: number | string): string {
const numberFormatOptions = getNumberFormatOptions();
return appendDigitGroupingSymbol(value, numberFormatOptions);
}
function getParsedAmountNumber(value: string): number {
const numberFormatOptions = getNumberFormatOptions();
return parseAmount(value, numberFormatOptions);
}
function getFormattedAmount(value: number | string, currencyCode: string): string {
const numberFormatOptions = getNumberFormatOptions(currencyCode);
return formatAmount(value, numberFormatOptions);
}
function getFormattedAmountWithCurrency(value: number | string, currencyCode?: string, notConvertValue?: boolean, currencyDisplayType?: CurrencyDisplayType): string | null {
if (!isNumber(value) && !isString(value)) {
return null;
}
if (isNumber(value)) {
value = value.toString();
}
let textualValue = value as string;
const isPlural: boolean = textualValue !== '100' && textualValue !== '-100';
if (!notConvertValue) {
const numberFormatOptions = getNumberFormatOptions();
const hasIncompleteFlag = isString(textualValue) && textualValue.charAt(textualValue.length - 1) === '+';
if (hasIncompleteFlag) {
textualValue = textualValue.substring(0, textualValue.length - 1);
}
textualValue = formatAmount(textualValue, numberFormatOptions);
if (hasIncompleteFlag) {
textualValue = textualValue + '+';
}
}
if (!isBoolean(currencyCode) && !currencyCode) {
currencyCode = userStore.currentUserDefaultCurrency;
} else if (isBoolean(currencyCode) && !currencyCode) {
currencyCode = '';
}
if (!currencyCode) {
return textualValue;
}
if (!currencyDisplayType) {
currencyDisplayType = getCurrentCurrencyDisplayType();
}
const currencyUnit = getCurrencyUnitName(currencyCode, isPlural);
const currencyName = getCurrencyName(currencyCode);
return appendCurrencySymbol(textualValue, currencyDisplayType, currencyCode, currencyUnit, currencyName, isPlural);
}
function getFormattedExchangeRateAmount(value: number | string): string {
const numberFormatOptions = getNumberFormatOptions();
return formatExchangeRateAmount(value, numberFormatOptions);
}
function getAdaptiveAmountRate(amount1: number, amount2: number, fromExchangeRate: { rate: string }, toExchangeRate: { rate: string }): string | null {
const numberFormatOptions = getNumberFormatOptions();
return getAdaptiveDisplayAmountRate(amount1, amount2, fromExchangeRate, toExchangeRate, numberFormatOptions);
}
function getAmountPrependAndAppendText(currencyCode: string, isPlural: boolean): CurrencyPrependAndAppendText | null {
const currencyDisplayType = getCurrentCurrencyDisplayType();
const currencyUnit = getCurrencyUnitName(currencyCode, isPlural);
const currencyName = getCurrencyName(currencyCode);
return getAmountPrependAndAppendCurrencySymbol(currencyDisplayType, currencyCode, currencyUnit, currencyName, isPlural);
}
function setLanguage(languageKey: string | null, force?: boolean): LocaleDefaultSettings | null { function setLanguage(languageKey: string | null, force?: boolean): LocaleDefaultSettings | null {
if (!languageKey) { if (!languageKey) {
languageKey = getDefaultLanguage(); languageKey = getDefaultLanguage();
@@ -677,6 +885,9 @@ export function useI18n() {
getWeekdayLongName, getWeekdayLongName,
getMultiMonthdayShortNames, getMultiMonthdayShortNames,
getMultiWeekdayLongNames, getMultiWeekdayLongNames,
getCurrentDecimalSeparator,
getCurrentDigitGroupingSymbol,
getCurrentDigitGroupingType,
isLongDateMonthAfterYear, isLongDateMonthAfterYear,
isShortDateMonthAfterYear, isShortDateMonthAfterYear,
isLongTime24HourFormat, isLongTime24HourFormat,
@@ -696,6 +907,13 @@ export function useI18n() {
formatUnixTimeToLongTime: (unixTime: number, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, getLocalizedLongTimeFormat(), utcOffset, currentUtcOffset), formatUnixTimeToLongTime: (unixTime: number, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, getLocalizedLongTimeFormat(), utcOffset, currentUtcOffset),
formatUnixTimeToShortTime: (unixTime: number, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, getLocalizedShortTimeFormat(), utcOffset, currentUtcOffset), formatUnixTimeToShortTime: (unixTime: number, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, getLocalizedShortTimeFormat(), utcOffset, currentUtcOffset),
formatYearQuarter, formatYearQuarter,
appendDigitGroupingSymbol: getNumberWithDigitGroupingSymbol,
parseAmount: getParsedAmountNumber,
formatAmount: getFormattedAmount,
formatAmountWithCurrency: getFormattedAmountWithCurrency,
formatExchangeRateAmount: getFormattedExchangeRateAmount,
getAdaptiveAmountRate,
getAmountPrependAndAppendText,
setLanguage, setLanguage,
setTimeZone, setTimeZone,
initLocale initLocale
+8
View File
@@ -234,3 +234,11 @@ export interface TransactionOverviewResponseItem {
incompleteExpenseAmount: boolean; incompleteExpenseAmount: boolean;
amounts?: TransactionAmountsResponseItemAmountInfo[]; amounts?: TransactionAmountsResponseItemAmountInfo[];
} }
export interface TransactionMonthlyIncomeAndExpenseData {
monthStartTime: number;
incomeAmount: number;
expenseAmount: number;
incompleteIncomeAmount: boolean;
incompleteExpenseAmount: boolean;
}
@@ -20,7 +20,7 @@
<v-skeleton-loader class="skeleton-no-margin mt-4 mb-8" type="text" width="120px" :loading="true" v-else-if="loading && !incomeAmount"></v-skeleton-loader> <v-skeleton-loader class="skeleton-no-margin mt-4 mb-8" type="text" width="120px" :loading="true" v-else-if="loading && !incomeAmount"></v-skeleton-loader>
<div class="text-truncate text-h5 text-expense" v-if="!loading || expenseAmount">{{ expenseAmount }}</div> <div class="text-truncate text-h5 text-expense" v-if="!loading || expenseAmount">{{ expenseAmount }}</div>
<v-skeleton-loader class="skeleton-no-margin mb-1" style="padding-bottom: 2px" type="text" width="120px" :loading="true" v-else-if="loading && !expenseAmount"></v-skeleton-loader> <v-skeleton-loader class="skeleton-no-margin mb-1" style="padding-bottom: 2px" type="text" width="120px" :loading="true" v-else-if="loading && !expenseAmount"></v-skeleton-loader>
<div class="text-truncate text-h5 mt-2 mb-7" style="padding-bottom: 2px" v-if="!loading && !incomeAmount && !expenseAmount">{{ $t('No data') }}</div> <div class="text-truncate text-h5 mt-2 mb-7" style="padding-bottom: 2px" v-if="!loading && !incomeAmount && !expenseAmount">{{ tt('No data') }}</div>
</v-card-text> </v-card-text>
<v-card-text class="mt-6"> <v-card-text class="mt-6">
<span class="text-caption">{{ datetime }}</span> <span class="text-caption">{{ datetime }}</span>
@@ -28,27 +28,26 @@
</v-card> </v-card>
</template> </template>
<script> <script setup lang="ts">
import { useI18n } from '@/locales/helpers.ts';
import { import {
mdiDotsVertical mdiDotsVertical
} from '@mdi/js'; } from '@mdi/js';
export default { defineProps<{
props: [ loading: boolean;
'loading', disabled: boolean;
'disabled', icon: string;
'icon', title: string;
'title', expenseAmount: string;
'expenseAmount', incomeAmount: string;
'incomeAmount', datetime: string;
'datetime' }>();
],
data() { const { tt } = useI18n();
return {
icons: { const icons = {
more: mdiDotsVertical more: mdiDotsVertical
} };
};
}
}
</script> </script>
@@ -1,7 +1,7 @@
<template> <template>
<v-card :class="{ 'disabled': disabled }"> <v-card :class="{ 'disabled': disabled }">
<template #title> <template #title>
<span>{{ $t('Income and Expense Trends') }}</span> <span>{{ tt('Income and Expense Trends') }}</span>
</template> </template>
<v-card-text class="overview-monthly-chart-container overview-monthly-chart-overlay" v-if="loading && !hasAnyData"> <v-card-text class="overview-monthly-chart-container overview-monthly-chart-overlay" v-if="loading && !hasAnyData">
@@ -15,7 +15,7 @@
<v-card-text class="overview-monthly-chart-container overview-monthly-chart-overlay" v-else-if="!loading && !hasAnyData"> <v-card-text class="overview-monthly-chart-container overview-monthly-chart-overlay" v-else-if="!loading && !hasAnyData">
<div class="d-flex flex-column align-center justify-center w-100 h-100"> <div class="d-flex flex-column align-center justify-center w-100 h-100">
<h2 style="margin-top: -40px">{{ $t('No data') }}</h2> <h2 style="margin-top: -40px">{{ tt('No data') }}</h2>
</div> </div>
</v-card-text> </v-card-text>
@@ -24,282 +24,290 @@
</v-card> </v-card>
</template> </template>
<script> <script setup lang="ts">
import { useTheme } from 'vuetify'; import { computed } from 'vue';
import type { ECElementEvent } from 'echarts/core';
import type { CallbackDataParams } from 'echarts/types/dist/shared';
import { useI18n } from '@/locales/helpers.ts';
import { mapStores } from 'pinia';
import { useSettingsStore } from '@/stores/setting.ts'; import { useSettingsStore } from '@/stores/setting.ts';
import { useUserStore } from '@/stores/user.ts'; import { useUserStore } from '@/stores/user.ts';
import { TransactionType } from '@/core/transaction.ts'; import { TransactionType } from '@/core/transaction.ts';
import { type TransactionMonthlyIncomeAndExpenseData } from '@/models/transaction.ts';
import { import {
parseDateFromUnixTime, parseDateFromUnixTime,
getMonthName getMonthName
} from '@/lib/datetime.ts'; } from '@/lib/datetime.ts';
import { getExpenseAndIncomeAmountColor } from '@/lib/ui/common.ts'; import { getExpenseAndIncomeAmountColor } from '@/lib/ui/common.ts';
export default { interface MonthlyIncomeAndExpenseCardClickEvent {
props: [ transactionType: TransactionType;
'loading', monthStartTime: number;
'data', }
'disabled',
'isDarkMode', const props = defineProps<{
'enableClickItem' loading: boolean;
], data: TransactionMonthlyIncomeAndExpenseData[];
emits: [ disabled: boolean;
'click' isDarkMode?: boolean;
], enableClickItem?: boolean;
computed: { }>();
...mapStores(useSettingsStore, useUserStore), const emit = defineEmits<{
showAmountInHomePage() { (e: 'click', event: MonthlyIncomeAndExpenseCardClickEvent): void;
return this.settingsStore.appSettings.showAmountInHomePage; }>();
},
defaultCurrency() { const { tt, getMonthShortName, formatAmountWithCurrency } = useI18n();
return this.userStore.currentUserDefaultCurrency;
}, const settingsStore = useSettingsStore();
hasAnyData() { const userStore = useUserStore();
if (!this.data || !this.data.length || this.data.length < 1) {
return false; const showAmountInHomePage = computed<boolean>(() => settingsStore.appSettings.showAmountInHomePage);
const defaultCurrency = computed<string>(() => userStore.currentUserDefaultCurrency);
const hasAnyData = computed<boolean>(() => {
if (!props.data || !props.data.length || props.data.length < 1) {
return false;
}
for (let i = 0; i < props.data.length; i++) {
const item = props.data[i];
if (item.incomeAmount > 0 || item.incomeAmount < 0 || item.expenseAmount > 0 || item.expenseAmount < 0) {
return true;
}
}
return false;
});
const chartOptions = computed<object>(() => {
const monthNames: string[] = [];
const incomeAmounts: number[] = [];
const expenseAmounts: number[] = [];
let minAmount = 0;
let maxAmount = 0;
const expenseIncomeAmountColor = getExpenseAndIncomeAmountColor(userStore.currentUserExpenseAmountColor, userStore.currentUserIncomeAmountColor, props.isDarkMode);
if (props.data) {
for (let i = 0; i < props.data.length; i++) {
const item = props.data[i];
const month = getMonthName(parseDateFromUnixTime(item.monthStartTime));
monthNames.push(getMonthShortName(month));
incomeAmounts.push(item.incomeAmount);
expenseAmounts.push(-item.expenseAmount);
if (item.incomeAmount > maxAmount) {
maxAmount = item.incomeAmount;
} }
for (let i = 0; i < this.data.length; i++) { if (-item.expenseAmount > maxAmount) {
const item = this.data[i]; maxAmount = -item.expenseAmount;
if (item.incomeAmount > 0 || item.incomeAmount < 0 || item.expenseAmount > 0 || item.expenseAmount < 0) {
return true;
}
} }
return false; if (item.incomeAmount < minAmount) {
}, minAmount = item.incomeAmount;
chartOptions() { }
const self = this;
const monthNames = [];
const incomeAmounts = [];
const expenseAmounts = [];
let minAmount = 0;
let maxAmount = 0;
const expenseIncomeAmountColor = getExpenseAndIncomeAmountColor(this.userStore.currentUserExpenseAmountColor, this.userStore.currentUserIncomeAmountColor, this.isDarkMode); if (-item.expenseAmount < minAmount) {
minAmount = -item.expenseAmount;
}
}
}
if (self.data) { const amountGap = maxAmount - minAmount;
for (let i = 0; i < self.data.length; i++) {
const item = self.data[i];
const month = getMonthName(parseDateFromUnixTime(item.monthStartTime));
monthNames.push(self.$locale.getMonthShortName(month)); return {
incomeAmounts.push(item.incomeAmount); tooltip: {
expenseAmounts.push(-item.expenseAmount); trigger: 'axis',
axisPointer: {
type: 'shadow'
},
backgroundColor: props.isDarkMode ? '#333' : '#fff',
borderColor: props.isDarkMode ? '#333' : '#fff',
textStyle: {
color: props.isDarkMode ? '#eee' : '#333'
},
formatter: (params: CallbackDataParams[]) => {
let incomeAmount: string | null = null;
let expenseAmount: string | null = null;
if (item.incomeAmount > maxAmount) { for (let i = 0; i < params.length; i++) {
maxAmount = item.incomeAmount; const param = params[i];
} const dataIndex = param.dataIndex;
const data = props.data[dataIndex];
if (-item.expenseAmount > maxAmount) { if (param.seriesId === 'seriesIncome') {
maxAmount = -item.expenseAmount; incomeAmount = getDisplayIncomeAmount(data);
} } else if (param.seriesId === 'seriesExpense') {
expenseAmount = getDisplayExpenseAmount(data);
if (item.incomeAmount < minAmount) {
minAmount = item.incomeAmount;
}
if (-item.expenseAmount < minAmount) {
minAmount = -item.expenseAmount;
} }
} }
return `<table>` +
`<thead>` +
`<tr>` +
`<td colspan="2" class="text-left">${params[0].name}</td>` +
`</tr>` +
`</thead>` +
`<tbody>` +
(
incomeAmount !== null ?
`<tr>` +
`<td><span class="overview-monthly-chart-tooltip-indicator bg-income mr-1"></span><span class="mr-4">${tt('Income')}</span></td>` +
`<td><strong>${incomeAmount}</strong></td>` +
`</tr>` : ''
)+
(
expenseAmount !== null ?
`<tr>` +
`<td><span class="overview-monthly-chart-tooltip-indicator bg-expense mr-1"></span><span class="mr-4">${tt('Expense')}</span></td>` +
`<td><strong>${expenseAmount}</strong></td>` +
`</tr>` : ''
) +
`</tbody>` +
`</table>`;
} }
},
const amountGap = maxAmount - minAmount; legend: {
bottom: 20,
return { itemWidth: 14,
tooltip: { itemHeight: 14,
trigger: 'axis', textStyle: {
axisPointer: { color: props.isDarkMode ? '#eee' : '#333'
type: 'shadow' },
}, icon: 'circle',
backgroundColor: self.isDarkMode ? '#333' : '#fff', data: [ tt('Income'), tt('Expense') ]
borderColor: self.isDarkMode ? '#333' : '#fff', },
textStyle: { grid: {
color: self.isDarkMode ? '#eee' : '#333' left: '20px',
}, right: '20px',
formatter: params => { top: '10px',
let incomeAmount = 0; bottom: '100px'
let expenseAmount = 0; },
xAxis: [
for (let i = 0; i < params.length; i++) { {
const param = params[i]; type: 'category',
const dataIndex = param.dataIndex; data: monthNames,
const data = self.data[dataIndex]; axisLine: {
show: false
if (param.seriesId === 'seriesIncome') { },
incomeAmount = self.getDisplayIncomeAmount(data); axisTick: {
} else if (param.seriesId === 'seriesExpense') { show: false
expenseAmount = self.getDisplayExpenseAmount(data); },
} axisLabel: {
} padding: [ 20, 0, 0, 0 ]
}
return `<table>` + }
`<thead>` + ],
`<tr>` + yAxis: [
`<td colspan="2" class="text-left">${params[0].name}</td>` + {
`</tr>` + type: 'value',
`</thead>` + min: minAmount - amountGap / 20,
`<tbody>` + max: maxAmount,
`<tr>` + splitNumber: 10,
`<td><span class="overview-monthly-chart-tooltip-indicator bg-income mr-1"></span><span class="mr-4">${self.$t('Income')}</span></td>` + axisLabel: {
`<td><strong>${incomeAmount}</strong></td>` + show: false
`</tr>` + },
`<tr>` + splitLine: {
`<td><span class="overview-monthly-chart-tooltip-indicator bg-expense mr-1"></span><span class="mr-4">${self.$t('Expense')}</span></td>` + show: false
`<td><strong>${expenseAmount}</strong></td>` + }
`</tr>` + },
`</tbody>` + {
`</table>`; type: 'value',
min: minAmount,
max: maxAmount + amountGap / 20,
splitNumber: 10,
axisLabel: {
show: false
},
splitLine: {
show: false
}
}
],
series: [
{
type: 'bar',
id: 'seriesIncome',
name: tt('Income'),
yAxisIndex: 0,
stack: 'Total',
itemStyle: {
color: expenseIncomeAmountColor.incomeAmountColor,
borderRadius: 16
},
emphasis: {
focus: 'series',
labelLine: {
show: false
} }
}, },
legend: { barMaxWidth: 16,
bottom: 20, data: incomeAmounts
itemWidth: 14, },
itemHeight: 14, {
textStyle: { type: 'bar',
color: self.isDarkMode ? '#eee' : '#333' id: 'seriesExpense',
}, name: tt('Expense'),
icon: 'circle', yAxisIndex: 1,
data: [ self.$t('Income'), self.$t('Expense') ] stack: 'Total',
itemStyle: {
color: expenseIncomeAmountColor.expenseAmountColor,
borderRadius: 16
}, },
grid: { emphasis: {
left: '20px', focus: 'series',
right: '20px', labelLine: {
top: '10px', show: false
bottom: '100px' }
}, },
xAxis: [ barMaxWidth: 16,
{ data: expenseAmounts
type: 'category',
data: monthNames,
axisLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
padding: [ 20, 0, 0, 0 ]
}
}
],
yAxis: [
{
type: 'value',
min: minAmount - amountGap / 20,
max: maxAmount,
splitNumber: 10,
axisLabel: {
show: false
},
splitLine: {
show: false
}
},
{
type: 'value',
min: minAmount,
max: maxAmount + amountGap / 20,
splitNumber: 10,
axisLabel: {
show: false
},
splitLine: {
show: false
}
}
],
series: [
{
type: 'bar',
id: 'seriesIncome',
name: self.$t('Income'),
yAxisIndex: 0,
stack: 'Total',
itemStyle: {
color: expenseIncomeAmountColor.incomeAmountColor,
borderRadius: 16
},
emphasis: {
focus: 'series',
labelLine: {
show: false
}
},
barMaxWidth: 16,
data: incomeAmounts
},
{
type: 'bar',
id: 'seriesExpense',
name: self.$t('Expense'),
yAxisIndex: 1,
stack: 'Total',
itemStyle: {
color: expenseIncomeAmountColor.expenseAmountColor,
borderRadius: 16
},
emphasis: {
focus: 'series',
labelLine: {
show: false
}
},
barMaxWidth: 16,
data: expenseAmounts
}
]
};
}
},
setup() {
const theme = useTheme();
return {
globalTheme: theme
};
},
methods: {
clickItem: function (e) {
if (!this.enableClickItem || !this.data || e.componentType !== 'series') {
return;
} }
]
};
});
const clickData = this.data[e.dataIndex]; function getDisplayCurrency(value: number | string, currencyCode: string): string {
return formatAmountWithCurrency(value, currencyCode) || '0';
}
if (clickData && e.seriesId === 'seriesIncome') { function getDisplayAmount(amount: number, incomplete: boolean): string {
this.$emit('click', { if (!showAmountInHomePage.value) {
transactionType: TransactionType.Income, return getDisplayCurrency('***', defaultCurrency.value);
monthStartTime: clickData.monthStartTime }
});
} else if (clickData && e.seriesId === 'seriesExpense') {
this.$emit('click', {
transactionType: TransactionType.Expense,
monthStartTime: clickData.monthStartTime
});
}
},
getDisplayCurrency(value, currencyCode) {
return this.$locale.formatAmountWithCurrency(this.settingsStore, this.userStore, value, currencyCode);
},
getDisplayAmount(amount, incomplete) {
if (!this.showAmountInHomePage) {
return this.getDisplayCurrency('***', this.defaultCurrency);
}
return this.getDisplayCurrency(amount, this.defaultCurrency) + (incomplete ? '+' : ''); return getDisplayCurrency(amount, defaultCurrency.value) + (incomplete ? '+' : '');
}, }
getDisplayIncomeAmount(category) {
return this.getDisplayAmount(category.incomeAmount, category.incompleteIncomeAmount); function getDisplayIncomeAmount(data: TransactionMonthlyIncomeAndExpenseData): string {
}, return getDisplayAmount(data.incomeAmount, data.incompleteIncomeAmount);
getDisplayExpenseAmount(category) { }
return this.getDisplayAmount(category.expenseAmount, category.incompleteExpenseAmount);
} function getDisplayExpenseAmount(data: TransactionMonthlyIncomeAndExpenseData): string {
return getDisplayAmount(data.expenseAmount, data.incompleteExpenseAmount);
}
function clickItem(e: ECElementEvent): void {
if (!props.enableClickItem || !props.data || e.componentType !== 'series') {
return;
}
const clickData = props.data[e.dataIndex];
if (clickData && e.seriesId === 'seriesIncome') {
emit('click', {
transactionType: TransactionType.Income,
monthStartTime: clickData.monthStartTime
});
} else if (clickData && e.seriesId === 'seriesExpense') {
emit('click', {
transactionType: TransactionType.Expense,
monthStartTime: clickData.monthStartTime
});
} }
} }
</script> </script>