feat: 统计面板与收支记录支持图表点击跳转与筛选双向联动
- 统计面板支出/收入分类饼图与支出/收入渠道饼图支持点击扇区跳转收支记录,并带上对应分类或渠道筛选 - 新增统计面板「查看收支记录」与收支记录「查看统计」按钮,实现两页双向跳转 - 收支记录跳转统计面板时按共享偏好还原类型、分类、渠道、标签、金额、备注、计入统计与时间范围 - 时间范围统一为具体起止区间,面板预设选择时换算为区间写入共享偏好 - 「计入统计」默认统一为「仅计入」,并修复「全部」与金额字段显式 null 的记忆 - 抽出公共跳转字段与共享筛选读写,收敛统计面板多处跳转的重复逻辑
This commit is contained in:
@@ -384,6 +384,7 @@ export default {
|
|||||||
expenseChannelPie: { zh: '支出交易渠道占比', en: 'Expense by Channel', ja: '支出の取引チャネル割合' },
|
expenseChannelPie: { zh: '支出交易渠道占比', en: 'Expense by Channel', ja: '支出の取引チャネル割合' },
|
||||||
incomeChannelPie: { zh: '收入交易渠道占比', en: 'Income by Channel', ja: '収入の取引チャネル割合' },
|
incomeChannelPie: { zh: '收入交易渠道占比', en: 'Income by Channel', ja: '収入の取引チャネル割合' },
|
||||||
uncategorized: { zh: '未分类', en: 'Uncategorized', ja: '未分類' },
|
uncategorized: { zh: '未分类', en: 'Uncategorized', ja: '未分類' },
|
||||||
|
viewTransactions: { zh: '查看收支记录', en: 'View Transactions', ja: '収支記録を表示' },
|
||||||
},
|
},
|
||||||
|
|
||||||
// 收支记录
|
// 收支记录
|
||||||
@@ -401,6 +402,7 @@ export default {
|
|||||||
exitBatchMode: { zh: '退出批量', en: 'Exit Batch', ja: '一括終了' },
|
exitBatchMode: { zh: '退出批量', en: 'Exit Batch', ja: '一括終了' },
|
||||||
importBill: { zh: '导入账单', en: 'Import Bill', ja: '明細をインポート' },
|
importBill: { zh: '导入账单', en: 'Import Bill', ja: '明細をインポート' },
|
||||||
exportExcel: { zh: '导出 Excel', en: 'Export Excel', ja: 'Excel をエクスポート' },
|
exportExcel: { zh: '导出 Excel', en: 'Export Excel', ja: 'Excel をエクスポート' },
|
||||||
|
viewStatistics: { zh: '查看统计', en: 'View Statistics', ja: '統計を表示' },
|
||||||
addRecord: { zh: '新增记录', en: 'Add Record', ja: '記録を追加' },
|
addRecord: { zh: '新增记录', en: 'Add Record', ja: '記録を追加' },
|
||||||
moreFilter: { zh: '更多筛选', en: 'More Filters', ja: '詳細フィルター' },
|
moreFilter: { zh: '更多筛选', en: 'More Filters', ja: '詳細フィルター' },
|
||||||
clear: { zh: '清空', en: 'Clear', ja: 'クリア' },
|
clear: { zh: '清空', en: 'Clear', ja: 'クリア' },
|
||||||
|
|||||||
@@ -40,12 +40,14 @@ export function loadTransactionFilterPrefs(): Partial<TransactionFilterPrefs> {
|
|||||||
if (Array.isArray(raw.filterDateRange) && raw.filterDateRange.length === 2) {
|
if (Array.isArray(raw.filterDateRange) && raw.filterDateRange.length === 2) {
|
||||||
result.filterDateRange = raw.filterDateRange as [number, number]
|
result.filterDateRange = raw.filterDateRange as [number, number]
|
||||||
}
|
}
|
||||||
if (raw.filterIncludeInStats === 'true' || raw.filterIncludeInStats === 'false') {
|
// filterIncludeInStats 为三态:'true'-计入、'false'-不计入、null-全部;显式 null 也需保留
|
||||||
|
if (raw.filterIncludeInStats === 'true' || raw.filterIncludeInStats === 'false' || raw.filterIncludeInStats === null) {
|
||||||
result.filterIncludeInStats = raw.filterIncludeInStats
|
result.filterIncludeInStats = raw.filterIncludeInStats
|
||||||
}
|
}
|
||||||
if (typeof raw.filterRemark === 'string') result.filterRemark = raw.filterRemark
|
if (typeof raw.filterRemark === 'string') result.filterRemark = raw.filterRemark
|
||||||
if (typeof raw.filterMinAmount === 'number') result.filterMinAmount = raw.filterMinAmount
|
// 金额支持显式 null(表示已清空),需与「键不存在」区分
|
||||||
if (typeof raw.filterMaxAmount === 'number') result.filterMaxAmount = raw.filterMaxAmount
|
if (typeof raw.filterMinAmount === 'number' || raw.filterMinAmount === null) result.filterMinAmount = raw.filterMinAmount
|
||||||
|
if (typeof raw.filterMaxAmount === 'number' || raw.filterMaxAmount === null) result.filterMaxAmount = raw.filterMaxAmount
|
||||||
if (typeof raw.pageSize === 'number') result.pageSize = raw.pageSize
|
if (typeof raw.pageSize === 'number') result.pageSize = raw.pageSize
|
||||||
if (typeof raw.sortValue === 'string') result.sortValue = raw.sortValue
|
if (typeof raw.sortValue === 'string') result.sortValue = raw.sortValue
|
||||||
if (Array.isArray(raw.filterTag)) result.filterTag = raw.filterTag
|
if (Array.isArray(raw.filterTag)) result.filterTag = raw.filterTag
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// 统计面板组件:展示指定时间范围的收支汇总、概览指标、趋势、分类占比与交易渠道分布
|
// 统计面板组件:展示指定时间范围的收支汇总、概览指标、趋势、分类占比与交易渠道分布
|
||||||
import { ref, computed, onMounted, watch, nextTick, unref, defineAsyncComponent } from 'vue'
|
import { ref, computed, onMounted, watch, nextTick, unref, defineAsyncComponent } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { NCard, NGrid, NGridItem, NStatistic, NSpace, NSelect, NDatePicker, NInput, NInputNumber, NButton, NIcon, useMessage } from 'naive-ui'
|
import { NCard, NGrid, NGridItem, NStatistic, NSpace, NSelect, NDatePicker, NInput, NInputNumber, NButton, NIcon, useMessage } from 'naive-ui'
|
||||||
import { SparklesOutline } from '@vicons/ionicons5'
|
import { SparklesOutline } from '@vicons/ionicons5'
|
||||||
import { getStatisticsSummary, getStatisticsGroup } from '../api/statistics'
|
import { getStatisticsSummary, getStatisticsGroup } from '../api/statistics'
|
||||||
@@ -15,7 +15,7 @@ import { getChannelLabel } from '../utils/channel'
|
|||||||
import { renderCategoryLabel } from '../utils/render'
|
import { renderCategoryLabel } from '../utils/render'
|
||||||
import { readJson, writeJson } from '../utils/storage'
|
import { readJson, writeJson } from '../utils/storage'
|
||||||
import { STORAGE_KEYS } from '../utils/storageKeys'
|
import { STORAGE_KEYS } from '../utils/storageKeys'
|
||||||
import { saveTransactionFilterPrefs } from '../utils/transactionFilterPrefs'
|
import { loadTransactionFilterPrefs, saveTransactionFilterPrefs } from '../utils/transactionFilterPrefs'
|
||||||
import AiQueryWindow from '../components/AiQueryWindow.vue'
|
import AiQueryWindow from '../components/AiQueryWindow.vue'
|
||||||
|
|
||||||
// 图表组件异步加载:echarts 体积较大,登录后进入面板时先渲染页面骨架,
|
// 图表组件异步加载:echarts 体积较大,登录后进入面板时先渲染页面骨架,
|
||||||
@@ -44,6 +44,7 @@ const VChart = defineAsyncComponent(async () => {
|
|||||||
const { t, locale } = useI18n() // 国际化
|
const { t, locale } = useI18n() // 国际化
|
||||||
const message = useMessage() // 消息提示
|
const message = useMessage() // 消息提示
|
||||||
const router = useRouter() // 路由跳转
|
const router = useRouter() // 路由跳转
|
||||||
|
const route = useRoute() // 当前路由(用于识别来自收支记录页的跳转)
|
||||||
|
|
||||||
// 时间范围快捷选项
|
// 时间范围快捷选项
|
||||||
const rangeOptions = computed(() => [
|
const rangeOptions = computed(() => [
|
||||||
@@ -259,9 +260,10 @@ function updatePieChart() {
|
|||||||
{
|
{
|
||||||
type: 'pie',
|
type: 'pie',
|
||||||
radius: ['40%', '70%'],
|
radius: ['40%', '70%'],
|
||||||
|
cursor: 'pointer',
|
||||||
data: categoryData.value
|
data: categoryData.value
|
||||||
.filter((c: any) => c.type === 'EXPENSE')
|
.filter((c: any) => c.type === 'EXPENSE')
|
||||||
.map((c: any) => ({ name: c.name, value: Number(c.total) })),
|
.map((c: any) => ({ name: c.name, value: Number(c.total), categoryId: c.key == null ? 0 : Number(c.key), navType: 'EXPENSE' })),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -276,9 +278,10 @@ function updateIncomePieChart() {
|
|||||||
{
|
{
|
||||||
type: 'pie',
|
type: 'pie',
|
||||||
radius: ['40%', '70%'],
|
radius: ['40%', '70%'],
|
||||||
|
cursor: 'pointer',
|
||||||
data: categoryData.value
|
data: categoryData.value
|
||||||
.filter((c: any) => c.type === 'INCOME')
|
.filter((c: any) => c.type === 'INCOME')
|
||||||
.map((c: any) => ({ name: c.name, value: Number(c.total) })),
|
.map((c: any) => ({ name: c.name, value: Number(c.total), categoryId: c.key == null ? 0 : Number(c.key), navType: 'INCOME' })),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -293,9 +296,10 @@ function updateChannelChart() {
|
|||||||
{
|
{
|
||||||
type: 'pie',
|
type: 'pie',
|
||||||
radius: ['40%', '70%'],
|
radius: ['40%', '70%'],
|
||||||
|
cursor: 'pointer',
|
||||||
data: channelData.value
|
data: channelData.value
|
||||||
.filter((p: any) => Number(p.expense) > 0)
|
.filter((p: any) => Number(p.expense) > 0)
|
||||||
.map((p: any) => ({ name: getChannelLabel(p.key), value: Number(p.expense) })),
|
.map((p: any) => ({ name: getChannelLabel(p.key), value: Number(p.expense), channelCode: p.key, navType: 'EXPENSE' })),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -310,9 +314,10 @@ function updateIncomeChannelChart() {
|
|||||||
{
|
{
|
||||||
type: 'pie',
|
type: 'pie',
|
||||||
radius: ['40%', '70%'],
|
radius: ['40%', '70%'],
|
||||||
|
cursor: 'pointer',
|
||||||
data: channelData.value
|
data: channelData.value
|
||||||
.filter((p: any) => Number(p.income) > 0)
|
.filter((p: any) => Number(p.income) > 0)
|
||||||
.map((p: any) => ({ name: getChannelLabel(p.key), value: Number(p.income) })),
|
.map((p: any) => ({ name: getChannelLabel(p.key), value: Number(p.income), channelCode: p.key, navType: 'INCOME' })),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -377,6 +382,33 @@ function bucketRange(key: string, gran: string): [number, number] {
|
|||||||
return [startOfDay(y, 0, 1), endOfDay(y, 11, 31)]
|
return [startOfDay(y, 0, 1), endOfDay(y, 11, 31)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 统计口径映射为收支记录页的计入统计三态 */
|
||||||
|
const STATS_SCOPE_MAP: Record<string, string | null> = {
|
||||||
|
INCLUDED: 'true',
|
||||||
|
EXCLUDED: 'false',
|
||||||
|
ALL: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 跳转收支记录页的公共筛选字段:统计口径、标签、金额、备注 */
|
||||||
|
function commonNavigationPatch() {
|
||||||
|
return {
|
||||||
|
filterTag: filterTag.value,
|
||||||
|
filterIncludeInStats: STATS_SCOPE_MAP[filterStatsScope.value] ?? null,
|
||||||
|
filterMinAmount: filterMinAmount.value,
|
||||||
|
filterMaxAmount: filterMaxAmount.value,
|
||||||
|
filterRemark: filterRemark.value.trim(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将当前面板时间范围换算为收支记录页的日期筛选区间 */
|
||||||
|
function currentDateRange(): [number, number] {
|
||||||
|
const { startTime, endTime } = range.value
|
||||||
|
return [
|
||||||
|
new Date(startTime.replace('T', ' ').replace(/-/g, '/')).getTime(),
|
||||||
|
new Date(endTime.replace('T', ' ').replace(/-/g, '/')).getTime(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将趋势时间桶与收支类型写入收支记录页筛选偏好,并跳转到收支记录
|
* 将趋势时间桶与收支类型写入收支记录页筛选偏好,并跳转到收支记录
|
||||||
*
|
*
|
||||||
@@ -384,27 +416,83 @@ function bucketRange(key: string, gran: string): [number, number] {
|
|||||||
* :param type: 收支类型;为空表示不限定类型(沿用面板当前类型筛选)
|
* :param type: 收支类型;为空表示不限定类型(沿用面板当前类型筛选)
|
||||||
*/
|
*/
|
||||||
function applyTrendNavigation(key: string, type: string | null) {
|
function applyTrendNavigation(key: string, type: string | null) {
|
||||||
// 统计口径映射为收支记录页的计入统计三态
|
|
||||||
const statsScopeMap: Record<string, string | null> = {
|
|
||||||
INCLUDED: 'true',
|
|
||||||
EXCLUDED: 'false',
|
|
||||||
ALL: null,
|
|
||||||
}
|
|
||||||
// 合并写入偏好,保留分页、排序等与本跳转无关的既有设置
|
// 合并写入偏好,保留分页、排序等与本跳转无关的既有设置
|
||||||
saveTransactionFilterPrefs({
|
saveTransactionFilterPrefs({
|
||||||
filterType: type ? [type] : filterType.value,
|
filterType: type ? [type] : filterType.value,
|
||||||
filterCategory: filterCategory.value,
|
filterCategory: filterCategory.value,
|
||||||
filterChannel: filterChannel.value,
|
filterChannel: filterChannel.value,
|
||||||
filterTag: filterTag.value,
|
|
||||||
filterDateRange: bucketRange(key, granularity.value),
|
filterDateRange: bucketRange(key, granularity.value),
|
||||||
filterIncludeInStats: statsScopeMap[filterStatsScope.value] ?? null,
|
...commonNavigationPatch(),
|
||||||
filterMinAmount: filterMinAmount.value,
|
|
||||||
filterMaxAmount: filterMaxAmount.value,
|
|
||||||
filterRemark: filterRemark.value.trim(),
|
|
||||||
})
|
})
|
||||||
router.push('/transactions')
|
router.push('/transactions')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点击分类饼图扇区:按该分类与饼图收支类型跳转收支记录页,并清空渠道筛选
|
||||||
|
*
|
||||||
|
* :param type: 收支类型(饼图所属类型)
|
||||||
|
* :param categoryId: 分类ID,0 表示未分类
|
||||||
|
*/
|
||||||
|
function applyCategoryNavigation(type: string, categoryId: number) {
|
||||||
|
saveTransactionFilterPrefs({
|
||||||
|
filterType: [type],
|
||||||
|
filterCategory: [categoryId],
|
||||||
|
filterChannel: [],
|
||||||
|
filterDateRange: currentDateRange(),
|
||||||
|
...commonNavigationPatch(),
|
||||||
|
})
|
||||||
|
router.push('/transactions')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点击渠道饼图扇区:按该渠道与饼图收支类型跳转收支记录页,并清空分类筛选
|
||||||
|
*
|
||||||
|
* :param type: 收支类型(饼图所属类型)
|
||||||
|
* :param channelCode: 交易渠道编码
|
||||||
|
*/
|
||||||
|
function applyChannelNavigation(type: string, channelCode: string) {
|
||||||
|
saveTransactionFilterPrefs({
|
||||||
|
filterType: [type],
|
||||||
|
filterChannel: [channelCode],
|
||||||
|
filterCategory: [],
|
||||||
|
filterDateRange: currentDateRange(),
|
||||||
|
...commonNavigationPatch(),
|
||||||
|
})
|
||||||
|
router.push('/transactions')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 携带统计面板当前全部筛选条件跳转到收支记录页 */
|
||||||
|
function applyPanelNavigation() {
|
||||||
|
saveTransactionFilterPrefs({
|
||||||
|
filterType: filterType.value,
|
||||||
|
filterCategory: filterCategory.value,
|
||||||
|
filterChannel: filterChannel.value,
|
||||||
|
filterDateRange: currentDateRange(),
|
||||||
|
...commonNavigationPatch(),
|
||||||
|
})
|
||||||
|
router.push('/transactions')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理分类/渠道饼图扇区点击:分类按分类ID跳转,渠道按渠道编码跳转,无渠道编码的扇区不响应
|
||||||
|
*
|
||||||
|
* :param params: ECharts 元素点击事件参数
|
||||||
|
*/
|
||||||
|
function handlePieClick(params: any) {
|
||||||
|
if (params?.componentType !== 'series') return
|
||||||
|
const d = params?.data
|
||||||
|
if (!d) return
|
||||||
|
// 渠道饼图数据项带 channelCode 字段,为空表示未指定渠道,不响应
|
||||||
|
if ('channelCode' in d) {
|
||||||
|
if (!d.channelCode) return
|
||||||
|
applyChannelNavigation(d.navType, d.channelCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (d.categoryId != null) {
|
||||||
|
applyCategoryNavigation(d.navType, Number(d.categoryId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 命中趋势柱子的点击:按该柱子的时间桶与收支类型跳转收支记录页
|
* 命中趋势柱子的点击:按该柱子的时间桶与收支类型跳转收支记录页
|
||||||
*
|
*
|
||||||
@@ -490,6 +578,33 @@ function loadPrefs() {
|
|||||||
if (['day', 'week', 'month', 'quarter', 'year'].includes(p.granularity)) granularity.value = p.granularity
|
if (['day', 'week', 'month', 'quarter', 'year'].includes(p.granularity)) granularity.value = p.granularity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将收支记录页共享的筛选偏好应用到统计面板(仅从收支记录跳转进入时调用)
|
||||||
|
*
|
||||||
|
* 时间范围统一为具体起止区间:存在日期区间时切换为自定义范围。
|
||||||
|
*/
|
||||||
|
function applySharedFilterPrefs() {
|
||||||
|
const p = loadTransactionFilterPrefs()
|
||||||
|
if (p.filterType) filterType.value = p.filterType
|
||||||
|
if (p.filterCategory) filterCategory.value = p.filterCategory
|
||||||
|
if (p.filterChannel) filterChannel.value = p.filterChannel
|
||||||
|
if (p.filterTag) filterTag.value = p.filterTag
|
||||||
|
if (p.filterMinAmount !== undefined) filterMinAmount.value = p.filterMinAmount
|
||||||
|
if (p.filterMaxAmount !== undefined) filterMaxAmount.value = p.filterMaxAmount
|
||||||
|
if (p.filterRemark !== undefined) filterRemark.value = p.filterRemark
|
||||||
|
// 计入统计三态反向映射为面板统计口径
|
||||||
|
if (p.filterIncludeInStats !== undefined) {
|
||||||
|
filterStatsScope.value = p.filterIncludeInStats === 'true'
|
||||||
|
? 'INCLUDED'
|
||||||
|
: p.filterIncludeInStats === 'false' ? 'EXCLUDED' : 'ALL'
|
||||||
|
}
|
||||||
|
// 日期区间统一映射为自定义时间范围
|
||||||
|
if (p.filterDateRange) {
|
||||||
|
rangeType.value = 'custom'
|
||||||
|
customRange.value = p.filterDateRange
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 保存面板偏好,下次进入自动还原 */
|
/** 保存面板偏好,下次进入自动还原 */
|
||||||
function savePrefs() {
|
function savePrefs() {
|
||||||
writeJson(DASHBOARD_PREFS_KEY, {
|
writeJson(DASHBOARD_PREFS_KEY, {
|
||||||
@@ -674,6 +789,11 @@ onMounted(async () => {
|
|||||||
loadChannels()
|
loadChannels()
|
||||||
loadTags()
|
loadTags()
|
||||||
loadAiConfig()
|
loadAiConfig()
|
||||||
|
// 从收支记录页跳转进入时,用共享筛选偏好覆盖面板筛选,并清除来源标记
|
||||||
|
if (route.query.from === 'transactions') {
|
||||||
|
applySharedFilterPrefs()
|
||||||
|
router.replace({ path: '/dashboard' })
|
||||||
|
}
|
||||||
await nextTick()
|
await nextTick()
|
||||||
suppressRangeWatch = false
|
suppressRangeWatch = false
|
||||||
fetchData()
|
fetchData()
|
||||||
@@ -695,6 +815,7 @@ onMounted(async () => {
|
|||||||
style="width: 260px;"
|
style="width: 260px;"
|
||||||
/>
|
/>
|
||||||
<span style="color: #999; font-size: 12px;">{{ range.startTime.substring(0, 10) }} ~ {{ range.endTime.substring(0, 10) }}</span>
|
<span style="color: #999; font-size: 12px;">{{ range.startTime.substring(0, 10) }} ~ {{ range.endTime.substring(0, 10) }}</span>
|
||||||
|
<NButton @click="applyPanelNavigation">{{ t('dashboard.viewTransactions') }}</NButton>
|
||||||
<NButton v-if="aiEnabled" @click="handleAiSearchOpen">
|
<NButton v-if="aiEnabled" @click="handleAiSearchOpen">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon :component="SparklesOutline" />
|
<NIcon :component="SparklesOutline" />
|
||||||
@@ -854,22 +975,22 @@ onMounted(async () => {
|
|||||||
<NGrid :cols="2" :x-gap="16" :y-gap="16">
|
<NGrid :cols="2" :x-gap="16" :y-gap="16">
|
||||||
<NGridItem>
|
<NGridItem>
|
||||||
<NCard>
|
<NCard>
|
||||||
<VChart :option="pieOption" style="height: 350px;" autoresize />
|
<VChart :option="pieOption" style="height: 350px;" autoresize @click="handlePieClick" />
|
||||||
</NCard>
|
</NCard>
|
||||||
</NGridItem>
|
</NGridItem>
|
||||||
<NGridItem>
|
<NGridItem>
|
||||||
<NCard>
|
<NCard>
|
||||||
<VChart :option="incomePieOption" style="height: 350px;" autoresize />
|
<VChart :option="incomePieOption" style="height: 350px;" autoresize @click="handlePieClick" />
|
||||||
</NCard>
|
</NCard>
|
||||||
</NGridItem>
|
</NGridItem>
|
||||||
<NGridItem>
|
<NGridItem>
|
||||||
<NCard>
|
<NCard>
|
||||||
<VChart :option="channelOption" style="height: 350px;" autoresize />
|
<VChart :option="channelOption" style="height: 350px;" autoresize @click="handlePieClick" />
|
||||||
</NCard>
|
</NCard>
|
||||||
</NGridItem>
|
</NGridItem>
|
||||||
<NGridItem>
|
<NGridItem>
|
||||||
<NCard>
|
<NCard>
|
||||||
<VChart :option="incomeChannelOption" style="height: 350px;" autoresize />
|
<VChart :option="incomeChannelOption" style="height: 350px;" autoresize @click="handlePieClick" />
|
||||||
</NCard>
|
</NCard>
|
||||||
</NGridItem>
|
</NGridItem>
|
||||||
</NGrid>
|
</NGrid>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ref, onMounted, onBeforeUnmount, computed, h, watch } from 'vue'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useMessage, useDialog, NButton, NSpace, NDataTable, NTag, NModal, NForm, NFormItem, NInput, NInputNumber, NSelect, NDatePicker, NPagination, NCard, NSwitch, NIcon, NPopover, NAutoComplete, NAlert, NDivider, NUpload, NUploadDragger, NTabs, NTabPane } from 'naive-ui'
|
import { useMessage, useDialog, NButton, NSpace, NDataTable, NTag, NModal, NForm, NFormItem, NInput, NInputNumber, NSelect, NDatePicker, NPagination, NCard, NSwitch, NIcon, NPopover, NAutoComplete, NAlert, NDivider, NUpload, NUploadDragger, NTabs, NTabPane } from 'naive-ui'
|
||||||
import { EyeOutline, EyeOffOutline, SparklesOutline, RefreshOutline, CloudUploadOutline, CheckmarkCircleOutline } from '@vicons/ionicons5'
|
import { EyeOutline, EyeOffOutline, SparklesOutline, RefreshOutline, CloudUploadOutline, CheckmarkCircleOutline, StatsChartOutline } from '@vicons/ionicons5'
|
||||||
import type { DataTableColumns, UploadFileInfo } from 'naive-ui'
|
import type { DataTableColumns, UploadFileInfo } from 'naive-ui'
|
||||||
import { getTransactionPage, createTransaction, updateTransaction, deleteTransaction, exportTransaction, toggleTransactionStats, toggleRemarkHidden, batchUpdateCategory, batchTags, getChannels, batchDeleteTransactions, batchCreateTransactions } from '../api/transaction'
|
import { getTransactionPage, createTransaction, updateTransaction, deleteTransaction, exportTransaction, toggleTransactionStats, toggleRemarkHidden, batchUpdateCategory, batchTags, getChannels, batchDeleteTransactions, batchCreateTransactions } from '../api/transaction'
|
||||||
import { getAiConfig, suggestByAi, suggestOneByAi, applyAiSuggestions, queryByAi, parseRecordByAi, parseCategoryByAi, parseTagByAi, parseChannelByAi } from '../api/ai'
|
import { getAiConfig, suggestByAi, suggestOneByAi, applyAiSuggestions, queryByAi, parseRecordByAi, parseCategoryByAi, parseTagByAi, parseChannelByAi } from '../api/ai'
|
||||||
@@ -263,7 +263,7 @@ const filterType = ref<string[]>([]) // 类型筛选(多选)
|
|||||||
const filterCategory = ref<number[]>([]) // 分类筛选(多选)
|
const filterCategory = ref<number[]>([]) // 分类筛选(多选)
|
||||||
const filterChannel = ref<string[]>([]) // 交易渠道筛选(多选)
|
const filterChannel = ref<string[]>([]) // 交易渠道筛选(多选)
|
||||||
const filterDateRange = ref<[number, number] | null>(null) // 日期范围筛选(起止时间戳)
|
const filterDateRange = ref<[number, number] | null>(null) // 日期范围筛选(起止时间戳)
|
||||||
const filterIncludeInStats = ref<string | null>(null) // 计入统计筛选:null-全部,'true'-计入,'false'-不计入
|
const filterIncludeInStats = ref<string | null>('true') // 计入统计筛选:null-全部,'true'-计入,'false'-不计入(默认仅计入,与统计面板一致)
|
||||||
const filterRemark = ref('') // 备注关键字搜索
|
const filterRemark = ref('') // 备注关键字搜索
|
||||||
// 备注关键词历史存储键
|
// 备注关键词历史存储键
|
||||||
const REMARK_HISTORY_KEY = STORAGE_KEYS.transactionRemarkHistory
|
const REMARK_HISTORY_KEY = STORAGE_KEYS.transactionRemarkHistory
|
||||||
@@ -341,10 +341,10 @@ function loadFilterPrefs() {
|
|||||||
if (p.filterCategory) filterCategory.value = p.filterCategory
|
if (p.filterCategory) filterCategory.value = p.filterCategory
|
||||||
if (p.filterChannel) filterChannel.value = p.filterChannel
|
if (p.filterChannel) filterChannel.value = p.filterChannel
|
||||||
if (p.filterDateRange) filterDateRange.value = p.filterDateRange
|
if (p.filterDateRange) filterDateRange.value = p.filterDateRange
|
||||||
if (p.filterIncludeInStats) filterIncludeInStats.value = p.filterIncludeInStats
|
if (p.filterIncludeInStats !== undefined) filterIncludeInStats.value = p.filterIncludeInStats
|
||||||
if (typeof p.filterRemark === 'string') filterRemark.value = p.filterRemark
|
if (typeof p.filterRemark === 'string') filterRemark.value = p.filterRemark
|
||||||
if (typeof p.filterMinAmount === 'number') filterMinAmount.value = p.filterMinAmount
|
if (p.filterMinAmount !== undefined) filterMinAmount.value = p.filterMinAmount
|
||||||
if (typeof p.filterMaxAmount === 'number') filterMaxAmount.value = p.filterMaxAmount
|
if (p.filterMaxAmount !== undefined) filterMaxAmount.value = p.filterMaxAmount
|
||||||
if (typeof p.pageSize === 'number' && pageSizes.includes(p.pageSize)) pageSize.value = p.pageSize
|
if (typeof p.pageSize === 'number' && pageSizes.includes(p.pageSize)) pageSize.value = p.pageSize
|
||||||
if (typeof p.sortValue === 'string' && sortOptions.value.some((o) => o.value === p.sortValue)) sortValue.value = p.sortValue
|
if (typeof p.sortValue === 'string' && sortOptions.value.some((o) => o.value === p.sortValue)) sortValue.value = p.sortValue
|
||||||
if (p.filterTag) filterTag.value = p.filterTag
|
if (p.filterTag) filterTag.value = p.filterTag
|
||||||
@@ -609,6 +609,17 @@ function goChannelManage() {
|
|||||||
router.push('/channels')
|
router.push('/channels')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 携带当前筛选条件跳转到统计面板
|
||||||
|
*
|
||||||
|
* 先将当前筛选写入共享偏好(与统计面板共用),再带来源标记跳转,
|
||||||
|
* 统计面板据此还原同一套筛选条件并生成对应统计信息。
|
||||||
|
*/
|
||||||
|
function goStatistics() {
|
||||||
|
saveFilterPrefs()
|
||||||
|
router.push({ path: '/dashboard', query: { from: 'transactions' } })
|
||||||
|
}
|
||||||
|
|
||||||
/** 加载「不计入统计」记录展示样式偏好,失败时保持默认删除线 */
|
/** 加载「不计入统计」记录展示样式偏好,失败时保持默认删除线 */
|
||||||
async function loadExcludedDisplayStyle() {
|
async function loadExcludedDisplayStyle() {
|
||||||
try {
|
try {
|
||||||
@@ -2018,6 +2029,12 @@ onBeforeUnmount(() => {
|
|||||||
<NButton @click="showImportModal = true">{{ t('transaction.importBill') }}</NButton>
|
<NButton @click="showImportModal = true">{{ t('transaction.importBill') }}</NButton>
|
||||||
<NButton @click="goChannelManage">{{ t('channel.manageEntry') }}</NButton>
|
<NButton @click="goChannelManage">{{ t('channel.manageEntry') }}</NButton>
|
||||||
<NButton :loading="exporting" @click="handleExport">{{ t('transaction.exportExcel') }}</NButton>
|
<NButton :loading="exporting" @click="handleExport">{{ t('transaction.exportExcel') }}</NButton>
|
||||||
|
<NButton @click="goStatistics">
|
||||||
|
<template #icon>
|
||||||
|
<NIcon :component="StatsChartOutline" />
|
||||||
|
</template>
|
||||||
|
{{ t('transaction.viewStatistics') }}
|
||||||
|
</NButton>
|
||||||
<NButton :loading="loading" @click="fetchData">
|
<NButton :loading="loading" @click="fetchData">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon :component="RefreshOutline" />
|
<NIcon :component="RefreshOutline" />
|
||||||
|
|||||||
Reference in New Issue
Block a user