feat: 前端新增中英文多语言支持
- 引入 vue-i18n,新增 locales/messages.ts 以中英对照方式维护词条 - 新增 LocaleSwitch 组件,在登录/注册页与主布局顶部栏提供切换入口 - 语言持久化到 localStorage 并同步 <html lang>,Naive UI 组件语言联动 - 各视图、表格列、弹窗与提示文案全部接入国际化 - 修复 confirmImport 缺失的 tagIds 类型定义
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
// 统计面板组件:展示指定时间范围的收支汇总、概览指标、趋势、分类占比与支付方式分布
|
||||
import { ref, computed, onMounted, watch, nextTick, h } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { NCard, NGrid, NGridItem, NStatistic, NSpace, NSelect, NDatePicker, NInput, NInputNumber, NButton } from 'naive-ui'
|
||||
import VChart from 'vue-echarts'
|
||||
import { use } from 'echarts/core'
|
||||
@@ -13,21 +14,23 @@ import { getCategoryList } from '../api/category'
|
||||
// 注册 ECharts 所需组件
|
||||
use([CanvasRenderer, LineChart, PieChart, BarChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent])
|
||||
|
||||
const { t, locale } = useI18n() // 国际化
|
||||
|
||||
// 时间范围快捷选项
|
||||
const rangeOptions = [
|
||||
{ label: '本月', value: 'month' },
|
||||
{ label: '近3月', value: '3month' },
|
||||
{ label: '近12月', value: '12month' },
|
||||
{ label: '自定义', value: 'custom' },
|
||||
]
|
||||
const rangeOptions = computed(() => [
|
||||
{ label: t('dashboard.range.month'), value: 'month' },
|
||||
{ label: t('dashboard.range.3month'), value: '3month' },
|
||||
{ label: t('dashboard.range.12month'), value: '12month' },
|
||||
{ label: t('dashboard.range.custom'), value: 'custom' },
|
||||
])
|
||||
// 趋势图时间粒度选项
|
||||
const granularityOptions = [
|
||||
{ label: '按日', value: 'day' },
|
||||
{ label: '按周', value: 'week' },
|
||||
{ label: '按月', value: 'month' },
|
||||
{ label: '按季', value: 'quarter' },
|
||||
{ label: '按年', value: 'year' },
|
||||
]
|
||||
const granularityOptions = computed(() => [
|
||||
{ label: t('dashboard.granularity.day'), value: 'day' },
|
||||
{ label: t('dashboard.granularity.week'), value: 'week' },
|
||||
{ label: t('dashboard.granularity.month'), value: 'month' },
|
||||
{ label: t('dashboard.granularity.quarter'), value: 'quarter' },
|
||||
{ label: t('dashboard.granularity.year'), value: 'year' },
|
||||
])
|
||||
|
||||
const rangeType = ref<'month' | '3month' | '12month' | 'custom'>('month') // 时间范围类型
|
||||
const customRange = ref<[number, number] | null>(null) // 自定义起止时间戳
|
||||
@@ -42,16 +45,16 @@ const filterMaxAmount = ref<number | null>(null) // 最大金额
|
||||
const filterRemark = ref('') // 备注关键字
|
||||
|
||||
// 类型与支付方式选项
|
||||
const typeOptions = [
|
||||
{ label: '支出', value: 'EXPENSE' },
|
||||
{ label: '收入', value: 'INCOME' },
|
||||
]
|
||||
const paymentOptions = [
|
||||
{ label: '微信', value: 'WECHAT' },
|
||||
{ label: '支付宝', value: 'ALIPAY' },
|
||||
{ label: '现金', value: 'CASH' },
|
||||
{ label: '银行卡', value: 'BANK' },
|
||||
]
|
||||
const typeOptions = computed(() => [
|
||||
{ label: t('common.types.expense'), value: 'EXPENSE' },
|
||||
{ label: t('common.types.income'), value: 'INCOME' },
|
||||
])
|
||||
const paymentOptions = computed(() => [
|
||||
{ label: t('common.payment.wechat'), value: 'WECHAT' },
|
||||
{ label: t('common.payment.alipay'), value: 'ALIPAY' },
|
||||
{ label: t('common.payment.cash'), value: 'CASH' },
|
||||
{ label: t('common.payment.bank'), value: 'BANK' },
|
||||
])
|
||||
|
||||
/** 分类选项数据结构 */
|
||||
interface CategoryOption {
|
||||
@@ -64,7 +67,7 @@ interface CategoryOption {
|
||||
const categories = ref<CategoryOption[]>([]) // 分类选项列表
|
||||
// 分类筛选项:固定包含「未分类」,其余分类按类型筛选联动
|
||||
const categoryFilterOptions = computed(() => [
|
||||
{ label: '未分类', value: 0, type: '' },
|
||||
{ label: t('dashboard.uncategorized'), value: 0, type: '' },
|
||||
...categories.value.filter(c => filterType.value.length === 0 || filterType.value.includes(c.type)),
|
||||
])
|
||||
|
||||
@@ -180,7 +183,7 @@ function buildKeys(start: Date, end: Date, gran: string): string[] {
|
||||
/** 更新支出分类饼图 */
|
||||
function updatePieChart() {
|
||||
pieOption.value = {
|
||||
title: { text: '支出分类占比', left: 'center' },
|
||||
title: { text: t('dashboard.expenseCategoryPie'), left: 'center' },
|
||||
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
|
||||
series: [
|
||||
{
|
||||
@@ -197,7 +200,7 @@ function updatePieChart() {
|
||||
/** 更新支付方式分布饼图 */
|
||||
function updatePaymentChart() {
|
||||
paymentOption.value = {
|
||||
title: { text: '支付方式分布', left: 'center' },
|
||||
title: { text: t('dashboard.paymentPie'), left: 'center' },
|
||||
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
|
||||
series: [
|
||||
{
|
||||
@@ -222,13 +225,13 @@ function updateTrendChart() {
|
||||
|
||||
trendOption.value = {
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { data: ['收入', '支出'], bottom: 0 },
|
||||
legend: { data: [t('common.types.income'), t('common.types.expense')], bottom: 0 },
|
||||
grid: { left: 60, right: 20, top: 40, bottom: 40 },
|
||||
xAxis: { type: 'category', data: labels },
|
||||
yAxis: { type: 'value' },
|
||||
series: [
|
||||
{ name: '收入', type: 'bar', data: income, color: '#18a058' },
|
||||
{ name: '支出', type: 'bar', data: expense, color: '#d03050' },
|
||||
{ name: t('common.types.income'), type: 'bar', data: income, color: '#18a058' },
|
||||
{ name: t('common.types.expense'), type: 'bar', data: expense, color: '#d03050' },
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -332,6 +335,12 @@ watch(rangeType, (v) => {
|
||||
watch([() => range.value.startTime, () => range.value.endTime, granularity, filterParams], fetchData)
|
||||
// 任一偏好变化时自动保存
|
||||
watch([rangeType, customRange, granularity, filterType, filterCategory, filterPaymentMethod, filterMinAmount, filterMaxAmount, filterRemark], savePrefs, { deep: true })
|
||||
// 语言切换时重绘图表,更新标题与图例文案
|
||||
watch(locale, () => {
|
||||
updatePieChart()
|
||||
updatePaymentChart()
|
||||
updateTrendChart()
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
loadPrefs()
|
||||
@@ -346,7 +355,7 @@ onMounted(async () => {
|
||||
<div>
|
||||
<!-- 顶部:标题与时间范围选择 -->
|
||||
<NSpace justify="space-between" align="center" style="margin-bottom: 20px;" :wrap="true">
|
||||
<h2 style="margin: 0;">统计面板</h2>
|
||||
<h2 style="margin: 0;">{{ t('dashboard.title') }}</h2>
|
||||
<NSpace align="center" :wrap="true">
|
||||
<NSelect v-model:value="rangeType" :options="rangeOptions" style="width: 110px;" />
|
||||
<NDatePicker
|
||||
@@ -369,7 +378,7 @@ onMounted(async () => {
|
||||
multiple
|
||||
clearable
|
||||
:max-tag-count="2"
|
||||
placeholder="类型"
|
||||
:placeholder="t('dashboard.filterType')"
|
||||
style="width: 160px;"
|
||||
/>
|
||||
<NSelect
|
||||
@@ -380,7 +389,7 @@ onMounted(async () => {
|
||||
filterable
|
||||
:max-tag-count="2"
|
||||
:render-label="renderCategoryLabel"
|
||||
placeholder="分类"
|
||||
:placeholder="t('dashboard.filterCategory')"
|
||||
style="width: 200px;"
|
||||
/>
|
||||
<NSelect
|
||||
@@ -389,7 +398,7 @@ onMounted(async () => {
|
||||
multiple
|
||||
clearable
|
||||
:max-tag-count="2"
|
||||
placeholder="支付方式"
|
||||
:placeholder="t('dashboard.filterPaymentMethod')"
|
||||
style="width: 200px;"
|
||||
/>
|
||||
<NInputNumber
|
||||
@@ -397,7 +406,7 @@ onMounted(async () => {
|
||||
:min="0"
|
||||
:precision="2"
|
||||
clearable
|
||||
placeholder="最小金额"
|
||||
:placeholder="t('dashboard.minAmount')"
|
||||
style="width: 130px;"
|
||||
/>
|
||||
<span style="color: #999;">~</span>
|
||||
@@ -406,16 +415,16 @@ onMounted(async () => {
|
||||
:min="0"
|
||||
:precision="2"
|
||||
clearable
|
||||
placeholder="最大金额"
|
||||
:placeholder="t('dashboard.maxAmount')"
|
||||
style="width: 130px;"
|
||||
/>
|
||||
<NInput
|
||||
v-model:value="filterRemark"
|
||||
clearable
|
||||
placeholder="备注搜索"
|
||||
:placeholder="t('dashboard.remark')"
|
||||
style="width: 180px;"
|
||||
/>
|
||||
<NButton @click="handleResetFilters">重置筛选</NButton>
|
||||
<NButton @click="handleResetFilters">{{ t('dashboard.resetFilters') }}</NButton>
|
||||
</NSpace>
|
||||
</NCard>
|
||||
|
||||
@@ -423,26 +432,26 @@ onMounted(async () => {
|
||||
<NGrid :cols="4" :x-gap="16" :y-gap="16" style="margin-bottom: 16px;">
|
||||
<NGridItem>
|
||||
<NCard>
|
||||
<NStatistic label="总收入" :value="Number(summary.totalIncome).toFixed(2)" style="color: #18a058;">
|
||||
<NStatistic :label="t('dashboard.totalIncome')" :value="Number(summary.totalIncome).toFixed(2)" style="color: #18a058;">
|
||||
<template #prefix>+</template>
|
||||
</NStatistic>
|
||||
</NCard>
|
||||
</NGridItem>
|
||||
<NGridItem>
|
||||
<NCard>
|
||||
<NStatistic label="总支出" :value="Number(summary.totalExpense).toFixed(2)" style="color: #d03050;">
|
||||
<NStatistic :label="t('dashboard.totalExpense')" :value="Number(summary.totalExpense).toFixed(2)" style="color: #d03050;">
|
||||
<template #prefix>-</template>
|
||||
</NStatistic>
|
||||
</NCard>
|
||||
</NGridItem>
|
||||
<NGridItem>
|
||||
<NCard>
|
||||
<NStatistic label="结余" :value="Number(summary.balance).toFixed(2)" />
|
||||
<NStatistic :label="t('dashboard.balance')" :value="Number(summary.balance).toFixed(2)" />
|
||||
</NCard>
|
||||
</NGridItem>
|
||||
<NGridItem>
|
||||
<NCard>
|
||||
<NStatistic label="交易笔数" :value="summary.transactionCount" />
|
||||
<NStatistic :label="t('dashboard.transactionCount')" :value="summary.transactionCount" />
|
||||
</NCard>
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
@@ -451,17 +460,17 @@ onMounted(async () => {
|
||||
<NGrid :cols="4" :x-gap="16" :y-gap="16" style="margin-bottom: 20px;">
|
||||
<NGridItem>
|
||||
<NCard>
|
||||
<NStatistic label="日均收入" :value="Number(summary.avgDailyIncome).toFixed(2)" />
|
||||
<NStatistic :label="t('dashboard.avgDailyIncome')" :value="Number(summary.avgDailyIncome).toFixed(2)" />
|
||||
</NCard>
|
||||
</NGridItem>
|
||||
<NGridItem>
|
||||
<NCard>
|
||||
<NStatistic label="日均支出" :value="Number(summary.avgDailyExpense).toFixed(2)" />
|
||||
<NStatistic :label="t('dashboard.avgDailyExpense')" :value="Number(summary.avgDailyExpense).toFixed(2)" />
|
||||
</NCard>
|
||||
</NGridItem>
|
||||
<NGridItem>
|
||||
<NCard>
|
||||
<NStatistic label="最大单笔支出" :value="Number(summary.maxExpenseAmount).toFixed(2)">
|
||||
<NStatistic :label="t('dashboard.maxExpense')" :value="Number(summary.maxExpenseAmount).toFixed(2)">
|
||||
<template #suffix>
|
||||
<span style="font-size: 12px; color: #999;">{{ (summary.maxExpenseRemark || '').slice(0, 12) }}</span>
|
||||
</template>
|
||||
@@ -470,7 +479,7 @@ onMounted(async () => {
|
||||
</NGridItem>
|
||||
<NGridItem>
|
||||
<NCard>
|
||||
<NStatistic label="平均每笔支出" :value="Number(summary.avgExpenseAmount).toFixed(2)" />
|
||||
<NStatistic :label="t('dashboard.avgExpense')" :value="Number(summary.avgExpenseAmount).toFixed(2)" />
|
||||
</NCard>
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
@@ -479,7 +488,7 @@ onMounted(async () => {
|
||||
<NCard style="margin-bottom: 16px;">
|
||||
<template #header>
|
||||
<NSpace justify="space-between" align="center">
|
||||
<span>收支趋势</span>
|
||||
<span>{{ t('dashboard.trend') }}</span>
|
||||
<NSelect v-model:value="granularity" :options="granularityOptions" style="width: 110px;" size="small" />
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user