From ccd2d0386ade5f663c7a9ba3c4c3b0098890b54d Mon Sep 17 00:00:00 2001 From: CHE LIANG ZHAO Date: Thu, 8 Jan 2026 11:33:30 +0800 Subject: [PATCH] =?UTF-8?q?feat(analysis):=20=E8=B6=8B=E5=8A=BF=E5=9B=BE?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=9C=AC=E5=91=A8=E9=80=89=E9=A1=B9=E3=80=81?= =?UTF-8?q?=E7=BA=BF=E6=80=A7=E5=9B=BE=E7=AE=80=E5=8C=96=E4=B8=BA=E6=80=BB?= =?UTF-8?q?=E9=87=91=E9=A2=9D=E6=9B=B2=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加本周时间范围选项 - 线性图模式只显示总支出曲线,不再显示分类曲线 - 图例根据图表类型动态切换(堆叠图显示分类,线性图显示总支出) - 时间范围选项:7天、本周、30天、本月、3个月、本年 --- .../analysis/DailyTrendChart.svelte | 314 ++++++++++++++---- web/src/lib/data/demo.ts | 238 ++++++++++--- 2 files changed, 449 insertions(+), 103 deletions(-) diff --git a/web/src/lib/components/analysis/DailyTrendChart.svelte b/web/src/lib/components/analysis/DailyTrendChart.svelte index e38a51a..ecd4766 100644 --- a/web/src/lib/components/analysis/DailyTrendChart.svelte +++ b/web/src/lib/components/analysis/DailyTrendChart.svelte @@ -6,6 +6,8 @@ import TrendingUp from '@lucide/svelte/icons/trending-up'; import TrendingDown from '@lucide/svelte/icons/trending-down'; import Calendar from '@lucide/svelte/icons/calendar'; + import AreaChart from '@lucide/svelte/icons/area-chart'; + import LineChart from '@lucide/svelte/icons/line-chart'; import { Button } from '$lib/components/ui/button'; import type { BillRecord } from '$lib/api'; import { pieColors } from '$lib/constants/chart'; @@ -24,8 +26,12 @@ let selectedDateRecords = $state([]); // 时间范围选项 - type TimeRange = '7d' | '30d' | '3m'; - let timeRange = $state('3m'); + type TimeRange = '7d' | 'week' | '30d' | 'month' | '3m' | 'year'; + let timeRange = $state('month'); + + // 图表类型 + type ChartType = 'area' | 'line'; + let chartType = $state('area'); // 隐藏的类别 let hiddenCategories = $state>(new Set()); @@ -42,8 +48,11 @@ const timeRangeOptions = [ { value: '7d', label: '最近 7 天' }, + { value: 'week', label: '本周' }, { value: '30d', label: '最近 30 天' }, - { value: '3m', label: '最近 3 个月' } + { value: 'month', label: '本月' }, + { value: '3m', label: '最近 3 个月' }, + { value: 'year', label: '本年' } ]; // 获取截止日期 @@ -52,15 +61,58 @@ switch (range) { case '7d': return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + case 'week': + // 本周一 + const day = now.getDay(); + const diff = now.getDate() - day + (day === 0 ? -6 : 1); + return new Date(now.getFullYear(), now.getMonth(), diff); case '30d': return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + case 'month': + // 本月第一天 + return new Date(now.getFullYear(), now.getMonth(), 1); case '3m': - default: return new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000); + case 'year': + // 本年第一天 + return new Date(now.getFullYear(), 0, 1); + default: + return new Date(now.getFullYear(), now.getMonth(), 1); } } - // 按日期和分类分组数据 + // 聚合粒度类型 + type AggregationType = 'day' | 'week' | 'month'; + + // 获取周的起始日期(周一) + function getWeekStart(date: Date): string { + const d = new Date(date); + const day = d.getDay(); + const diff = d.getDate() - day + (day === 0 ? -6 : 1); // 调整到周一 + d.setDate(diff); + return d.toISOString().split('T')[0]; + } + + // 获取月份标识 + function getMonthKey(date: Date): string { + return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; + } + + // 格式化聚合后的标签 + function formatAggregationLabel(key: string, type: AggregationType): string { + if (type === 'day') { + const d = new Date(key); + return `${d.getMonth() + 1}/${d.getDate()}`; + } else if (type === 'week') { + const d = new Date(key); + return `${d.getMonth() + 1}/${d.getDate()}周`; + } else { + const [year, month] = key.split('-'); + return `${month}月`; + } + } + + // 按日期和分类分组数据(支持智能聚合) let processedData = $derived(() => { const cutoffDate = getCutoffDate(timeRange); @@ -71,11 +123,10 @@ return recordDate >= cutoffDate; }); - if (expenseRecords.length === 0) return { data: [], categories: [], maxValue: 0 }; + if (expenseRecords.length === 0) return { data: [], categories: [], maxValue: 0, aggregationType: 'day' as AggregationType }; - // 按日期分组 - const dateMap = new Map>(); - const categorySet = new Set(); + // 先按天分组,计算天数 + const dailyMap = new Map>(); const categoryTotals: Record = {}; expenseRecords.forEach(record => { @@ -83,16 +134,49 @@ const category = record.category || '其他'; const amount = parseFloat(record.amount) || 0; - categorySet.add(category); categoryTotals[category] = (categoryTotals[category] || 0) + amount; - if (!dateMap.has(dateStr)) { - dateMap.set(dateStr, new Map()); + if (!dailyMap.has(dateStr)) { + dailyMap.set(dateStr, new Map()); } - const dayData = dateMap.get(dateStr)!; + const dayData = dailyMap.get(dateStr)!; dayData.set(category, (dayData.get(category) || 0) + amount); }); + const dayCount = dailyMap.size; + + // 根据天数决定聚合粒度 + let aggregationType: AggregationType = 'day'; + if (dayCount > 90) { + aggregationType = 'month'; + } else if (dayCount > 30) { + aggregationType = 'week'; + } + + // 按聚合粒度重新分组 + const aggregatedMap = new Map>(); + + dailyMap.forEach((dayData, dateStr) => { + const date = new Date(dateStr); + let key: string; + + if (aggregationType === 'day') { + key = dateStr; + } else if (aggregationType === 'week') { + key = getWeekStart(date); + } else { + key = getMonthKey(date); + } + + if (!aggregatedMap.has(key)) { + aggregatedMap.set(key, new Map()); + } + const aggData = aggregatedMap.get(key)!; + dayData.forEach((amount, cat) => { + aggData.set(cat, (aggData.get(cat) || 0) + amount); + }); + }); + // 获取前5大分类 const sortedCategories = Object.entries(categoryTotals) .sort((a, b) => b[1] - a[1]) @@ -100,16 +184,26 @@ .map(([cat]) => cat); // 转换为数组格式并计算堆叠值 - const data = Array.from(dateMap.entries()) - .map(([dateStr, dayData]) => { + const data = Array.from(aggregatedMap.entries()) + .map(([key, aggData]) => { + // 为不同聚合类型创建正确的日期对象 + let date: Date; + if (aggregationType === 'month') { + const [year, month] = key.split('-'); + date = new Date(parseInt(year), parseInt(month) - 1, 15); // 月中 + } else { + date = new Date(key); + } + const result: Record = { - date: new Date(dateStr), - dateStr: dateStr + date, + dateStr: key, + label: formatAggregationLabel(key, aggregationType) }; let cumulative = 0; sortedCategories.forEach(cat => { - const value = dayData.get(cat) || 0; + const value = aggData.get(cat) || 0; result[cat] = value; // 只有未隐藏的类别参与堆叠 if (!hiddenCategories.has(cat)) { @@ -124,7 +218,7 @@ // 其他分类汇总 let otherSum = 0; - dayData.forEach((amount, cat) => { + aggData.forEach((amount, cat) => { if (!sortedCategories.includes(cat)) { otherSum += amount; } @@ -153,7 +247,7 @@ const maxValue = Math.max(...data.map(d => d.total || 0), 1); - return { data, categories: finalCategories, maxValue }; + return { data, categories: finalCategories, maxValue, aggregationType, dayCount }; }); // 获取颜色 @@ -176,13 +270,23 @@ // 获取描述文本 let descriptionText = $derived(() => { - const { data } = processedData(); + const { data, aggregationType, dayCount } = processedData(); const label = timeRangeOptions.find(o => o.value === timeRange)?.label || '最近 3 个月'; - return `${label}各分类支出趋势 (${data.length} 天)`; + + let aggregationHint = ''; + if (aggregationType === 'week') { + aggregationHint = `,按周聚合 (${data.length} 周)`; + } else if (aggregationType === 'month') { + aggregationHint = `,按月聚合 (${data.length} 月)`; + } else { + aggregationHint = ` (${dayCount || data.length} 天)`; + } + + return `${label}各分类支出趋势${aggregationHint}`; }); function handleTimeRangeChange(value: string | undefined) { - if (value && ['7d', '30d', '3m'].includes(value)) { + if (value && ['7d', 'week', '30d', 'month', '3m', 'year'].includes(value)) { timeRange = value as TimeRange; } } @@ -295,13 +399,46 @@ return path; } + // 生成线性图路径(总金额曲线) + function generateTotalLinePath(data: any[], maxValue: number): string { + if (data.length === 0) return ''; + + // 计算每天的总支出(所有可见分类的总和) + const points: { x: number; y: number }[] = data.map((d) => { + // 使用 total 字段,这是所有可见分类的累计值 + const total = d.total || 0; + return { + x: xScale(d.date, data), + y: yScale(total, maxValue) + }; + }); + + // 起始点 + const startPoint = points[0]; + let path = `M ${startPoint.x},${startPoint.y}`; + + // 平滑曲线 + path += generateSmoothPath(points); + + return path; + } + + // 生成线性图的数据点坐标(总金额) + function getTotalLinePoints(data: any[], maxValue: number): { x: number; y: number; value: number }[] { + return data.map((d) => ({ + x: xScale(d.date, data), + y: yScale(d.total || 0, maxValue), + value: d.total || 0 + })); + } + // 生成 X 轴刻度 function getXTicks(data: any[]): { x: number; label: string }[] { if (data.length === 0) return []; const step = Math.max(1, Math.floor(data.length / 6)); return data.filter((_, i) => i % step === 0 || i === data.length - 1).map(d => ({ x: xScale(d.date, data), - label: `${d.date.getMonth() + 1}/${d.date.getDate()}` + label: d.label || `${d.date.getMonth() + 1}/${d.date.getDate()}` })); } @@ -437,39 +574,72 @@ {descriptionText()} - - - {timeRangeOptions.find(o => o.value === timeRange)?.label} - - - {#each timeRangeOptions as option} - {option.label} - {/each} - - +
+ +
+ + +
+ + + + + {timeRangeOptions.find(o => o.value === timeRange)?.label} + + + {#each timeRangeOptions as option} + {option.label} + {/each} + + +
- +
- {#each categories as category, i} - {@const isHidden = hiddenCategories.has(category)} - + {/each} + {:else} + +
- {category} - - {/each} + 总支出 +
+ {/if}
- +
{/each} - - {#each [...categories].reverse() as category, i} - {@const colorIdx = categories.length - 1 - i} + {#if chartType === 'area'} + + {#each [...categories].reverse() as category, i} + {@const colorIdx = categories.length - 1 - i} + + {/each} + {:else} + + - {/each} + + {#each getTotalLinePoints(data, maxValue) as point} + + {/each} + {/if} {#if tooltipData} @@ -582,7 +778,7 @@ style="left: {adjustedLeft}%; top: 15%;" >
- {tooltipData.date.toLocaleDateString('zh-CN')} + {tooltipData.label || tooltipData.date.toLocaleDateString('zh-CN')}
{#each categories as category, i} diff --git a/web/src/lib/data/demo.ts b/web/src/lib/data/demo.ts index 4076fcb..5fa572b 100644 --- a/web/src/lib/data/demo.ts +++ b/web/src/lib/data/demo.ts @@ -1,48 +1,198 @@ import type { BillRecord } from '$lib/api'; -/** 演示数据(基于支付宝真实数据格式) */ -export const demoRecords: BillRecord[] = [ - { time: '2026-01-07 12:01:02', category: '餐饮美食', merchant: '金山武汉食堂', description: '金山武汉食堂-烧腊', income_expense: '支出', amount: '23.80', payment_method: '招商银行信用卡', status: '交易成功', remark: '', needs_review: '' }, - { time: '2026-01-06 15:54:53', category: '餐饮美食', merchant: '友宝', description: '智能货柜消费', income_expense: '支出', amount: '7.19', payment_method: '招商银行信用卡', status: '交易成功', remark: '', needs_review: '' }, - { time: '2026-01-06 11:55:10', category: '餐饮美食', merchant: '金山武汉食堂', description: '金山武汉食堂-小碗菜', income_expense: '支出', amount: '12.00', payment_method: '招商银行信用卡', status: '交易成功', remark: '', needs_review: '' }, - { time: '2026-01-06 09:35:09', category: '交通出行', merchant: '高德打车', description: '高德打车订单', income_expense: '支出', amount: '16.09', payment_method: '招商银行信用卡', status: '交易成功', remark: '', needs_review: '' }, - { time: '2026-01-05 18:59:11', category: '餐饮美食', merchant: '板栗', description: '收钱码收款', income_expense: '支出', amount: '21.00', payment_method: '花呗', status: '交易成功', remark: '', needs_review: '' }, - { time: '2026-01-05 18:22:34', category: '日用百货', merchant: '金山便利店', description: '立码收收款', income_expense: '支出', amount: '40.69', payment_method: '招商银行信用卡', status: '交易成功', remark: '', needs_review: '' }, - { time: '2026-01-05 15:16:38', category: '充值缴费', merchant: '武汉供电公司', description: '电费自动缴费', income_expense: '支出', amount: '50.00', payment_method: '招商银行信用卡', status: '交易成功', remark: '', needs_review: '' }, - { time: '2026-01-05 13:49:13', category: '餐饮美食', merchant: '友宝', description: '维他柠檬茶', income_expense: '支出', amount: '2.40', payment_method: '招商银行信用卡', status: '交易成功', remark: '', needs_review: '' }, - { time: '2026-01-05 11:59:45', category: '餐饮美食', merchant: '金山武汉食堂', description: '金山武汉食堂-小碗菜', income_expense: '支出', amount: '9.00', payment_method: '招商银行信用卡', status: '交易成功', remark: '', needs_review: '' }, - { time: '2026-01-05 09:36:44', category: '交通出行', merchant: '高德打车', description: '高德打车订单', income_expense: '支出', amount: '13.43', payment_method: '招商银行信用卡', status: '交易成功', remark: '', needs_review: '' }, - { time: '2026-01-04 19:27:50', category: '日用百货', merchant: '朴朴超市', description: '朴朴商品订单', income_expense: '支出', amount: '52.77', payment_method: '招商银行信用卡', status: '交易成功', remark: '', needs_review: '' }, - { time: '2026-01-04 17:06:22', category: '餐饮美食', merchant: '友宝', description: '智能货柜消费', income_expense: '支出', amount: '2.55', payment_method: '招商银行信用卡', status: '交易成功', remark: '', needs_review: '' }, - { time: '2026-01-04 12:03:39', category: '餐饮美食', merchant: '金山武汉食堂', description: '金山武汉食堂-烧腊', income_expense: '支出', amount: '23.80', payment_method: '招商银行信用卡', status: '交易成功', remark: '', needs_review: '' }, - { time: '2026-01-04 09:29:28', category: '交通出行', merchant: '高德打车', description: '高德打车订单', income_expense: '支出', amount: '22.86', payment_method: '招商银行信用卡', status: '交易成功', remark: '', needs_review: '' }, - { time: '2026-01-04 09:04:05', category: '餐饮美食', merchant: '巴比鲜包', description: '早餐', income_expense: '支出', amount: '8.00', payment_method: '花呗', status: '交易成功', remark: '', needs_review: '' }, - { time: '2026-01-03 22:30:01', category: '餐饮美食', merchant: '美团', description: '长沙臭豆腐', income_expense: '支出', amount: '20.88', payment_method: '招商银行信用卡', status: '交易成功', remark: '', needs_review: '' }, - { time: '2026-01-03 17:04:37', category: '家居家装', merchant: '淘宝', description: '四件套', income_expense: '支出', amount: '156.35', payment_method: '招商银行信用卡', status: '支付成功', remark: '', needs_review: '' }, - { time: '2026-01-03 13:44:03', category: '餐饮美食', merchant: '淘宝闪购', description: '必胜客外卖', income_expense: '支出', amount: '55.00', payment_method: '招商银行信用卡', status: '交易成功', remark: '', needs_review: '' }, - { time: '2026-01-03 10:16:31', category: '充值缴费', merchant: '湖北联通', description: '手机充值', income_expense: '支出', amount: '50.00', payment_method: '招商银行信用卡', status: '充值成功', remark: '', needs_review: '' }, - { time: '2026-01-03 00:17:12', category: '交通出行', merchant: '高德打车', description: '高德打车订单', income_expense: '支出', amount: '17.45', payment_method: '招商银行信用卡', status: '交易成功', remark: '', needs_review: '' }, - { time: '2026-01-02 21:29:15', category: '交通出行', merchant: '高德打车', description: '高德打车订单', income_expense: '支出', amount: '20.65', payment_method: '招商银行信用卡', status: '交易成功', remark: '', needs_review: '' }, - { time: '2026-01-02 15:39:08', category: '交通出行', merchant: '高德打车', description: '高德打车订单', income_expense: '支出', amount: '12.61', payment_method: '招商银行信用卡', status: '交易成功', remark: '', needs_review: '' }, - { time: '2026-01-02 13:30:02', category: '充值缴费', merchant: '武汉燃气集团', description: '燃气费', income_expense: '支出', amount: '300.00', payment_method: '招商银行信用卡', status: '交易成功', remark: '', needs_review: '' }, - { time: '2026-01-02 12:06:04', category: '餐饮美食', merchant: '淘宝闪购', description: '外卖订单', income_expense: '支出', amount: '17.38', payment_method: '招商银行信用卡', status: '交易成功', remark: '', needs_review: '' }, - { time: '2026-01-02 12:04:27', category: '运动健身', merchant: '携程', description: '武汉冰雪中心', income_expense: '支出', amount: '390.00', payment_method: '招商银行信用卡', status: '交易成功', remark: '', needs_review: '' }, - { time: '2026-01-02 11:05:33', category: '充值缴费', merchant: '中国移动', description: '话费充值', income_expense: '支出', amount: '50.00', payment_method: '招商银行信用卡', status: '交易成功', remark: '', needs_review: '' }, - { time: '2026-01-02 01:46:12', category: '充值缴费', merchant: '中国移动', description: '话费自动充值', income_expense: '支出', amount: '50.00', payment_method: '招商银行信用卡', status: '交易成功', remark: '', needs_review: '' }, - { time: '2026-01-01 21:42:18', category: '文化休闲', merchant: '雷神', description: '超级会员', income_expense: '支出', amount: '88.00', payment_method: '招商银行信用卡', status: '交易成功', remark: '', needs_review: '' }, - { time: '2026-01-01 20:44:27', category: '餐饮美食', merchant: '美团', description: '茶百道', income_expense: '支出', amount: '6.85', payment_method: '招商银行信用卡', status: '交易成功', remark: '', needs_review: '' }, - { time: '2026-01-01 20:42:34', category: '餐饮美食', merchant: '美团', description: '南膳房北京烤鸭', income_expense: '支出', amount: '20.20', payment_method: '招商银行信用卡', status: '交易成功', remark: '', needs_review: '' }, - { time: '2026-01-01 15:38:40', category: '餐饮美食', merchant: '淘宝闪购', description: '米已成粥外卖', income_expense: '支出', amount: '19.90', payment_method: '招商银行信用卡', status: '交易成功', remark: '', needs_review: '' }, - { time: '2026-01-01 14:58:20', category: '家居家装', merchant: '淘宝', description: '四件套', income_expense: '支出', amount: '137.85', payment_method: '招商银行信用卡', status: '支付成功', remark: '', needs_review: '' }, - { time: '2026-01-01 14:57:26', category: '数码电器', merchant: '天猫', description: '手机膜', income_expense: '支出', amount: '22.24', payment_method: '招商银行信用卡', status: '等待确认收货', remark: '', needs_review: '' }, - { time: '2026-01-01 14:29:47', category: '文化休闲', merchant: '南方新媒体', description: '超级大会员', income_expense: '支出', amount: '25.00', payment_method: '招商银行信用卡', status: '交易成功', remark: '', needs_review: '' }, - { time: '2025-12-31 18:30:00', category: '餐饮美食', merchant: '火锅店', description: '跨年火锅', income_expense: '支出', amount: '288.00', payment_method: '招商银行信用卡', status: '交易成功', remark: '', needs_review: '' }, - { time: '2025-12-30 12:00:00', category: '餐饮美食', merchant: '金山食堂', description: '午餐', income_expense: '支出', amount: '15.00', payment_method: '招商银行信用卡', status: '交易成功', remark: '', needs_review: '' }, - { time: '2025-12-29 09:00:00', category: '交通出行', merchant: '高德打车', description: '打车', income_expense: '支出', amount: '18.50', payment_method: '招商银行信用卡', status: '交易成功', remark: '', needs_review: '' }, - { time: '2025-12-28 20:00:00', category: '餐饮美食', merchant: '外卖', description: '晚餐外卖', income_expense: '支出', amount: '35.00', payment_method: '花呗', status: '交易成功', remark: '', needs_review: '' }, - // 收入记录 - { time: '2026-01-05 10:00:00', category: '退款', merchant: '淘宝', description: '商品退款', income_expense: '收入', amount: '99.00', payment_method: '原路退回', status: '退款成功', remark: '', needs_review: '' }, - { time: '2026-01-01 09:00:00', category: '其他收入', merchant: '支付宝', description: '新年红包', income_expense: '收入', amount: '8.88', payment_method: '余额', status: '已到账', remark: '', needs_review: '' }, -]; +// 生成随机金额 +function randomAmount(min: number, max: number): string { + return (Math.random() * (max - min) + min).toFixed(2); +} +// 生成指定日期的时间字符串 +function formatDateTime(date: Date, hour: number, minute: number): string { + const y = date.getFullYear(); + const m = String(date.getMonth() + 1).padStart(2, '0'); + const d = String(date.getDate()).padStart(2, '0'); + const h = String(hour).padStart(2, '0'); + const min = String(minute).padStart(2, '0'); + return `${y}-${m}-${d} ${h}:${min}:00`; +} +// 商家和消费场景配置 +const merchants = { + 餐饮美食: [ + { merchant: '金山武汉食堂', descriptions: ['午餐', '烧腊', '小碗菜'], amountRange: [12, 28] }, + { merchant: '瑞幸咖啡', descriptions: ['生椰拿铁', '美式', '拿铁'], amountRange: [9.9, 18] }, + { merchant: '星巴克', descriptions: ['美式咖啡', '拿铁', '星冰乐'], amountRange: [28, 45] }, + { merchant: '麦当劳', descriptions: ['早餐套餐', '午餐套餐', '汉堡'], amountRange: [20, 45] }, + { merchant: '肯德基', descriptions: ['套餐', '炸鸡', '汉堡'], amountRange: [25, 50] }, + { merchant: '海底捞', descriptions: ['火锅', '聚餐'], amountRange: [150, 300] }, + { merchant: '美团外卖', descriptions: ['外卖订单', '午餐外卖', '晚餐外卖'], amountRange: [20, 50] }, + { merchant: '饿了么', descriptions: ['外卖订单', '午餐', '晚餐'], amountRange: [18, 45] }, + { merchant: '便利店', descriptions: ['零食', '饮料', '早餐'], amountRange: [8, 30] }, + { merchant: '喜茶', descriptions: ['多肉葡萄', '芝芝莓莓'], amountRange: [18, 32] }, + ], + 交通出行: [ + { merchant: '高德打车', descriptions: ['打车订单', '快车'], amountRange: [12, 35] }, + { merchant: '滴滴出行', descriptions: ['快车', '打车'], amountRange: [15, 40] }, + { merchant: '武汉地铁', descriptions: ['地铁充值', '乘车'], amountRange: [50, 100] }, + { merchant: '哈啰单车', descriptions: ['骑行', '单车'], amountRange: [1.5, 5] }, + { merchant: '中国石化', descriptions: ['加油', '油费'], amountRange: [200, 400] }, + ], + 日用百货: [ + { merchant: '朴朴超市', descriptions: ['日用品', '商品订单'], amountRange: [30, 80] }, + { merchant: '盒马鲜生', descriptions: ['生鲜蔬果', '日用品'], amountRange: [50, 150] }, + { merchant: '沃尔玛', descriptions: ['日用品采购', '超市购物'], amountRange: [80, 200] }, + { merchant: '名创优品', descriptions: ['日用品', '小商品'], amountRange: [20, 60] }, + { merchant: '屈臣氏', descriptions: ['洗护用品', '化妆品'], amountRange: [50, 150] }, + ], + 充值缴费: [ + { merchant: '武汉供电公司', descriptions: ['电费', '电费缴费'], amountRange: [50, 200] }, + { merchant: '武汉燃气集团', descriptions: ['燃气费', '天然气'], amountRange: [100, 300] }, + { merchant: '中国移动', descriptions: ['话费充值', '手机充值'], amountRange: [50, 100] }, + { merchant: '中国联通', descriptions: ['话费充值', '手机充值'], amountRange: [50, 100] }, + { merchant: '中国电信', descriptions: ['宽带续费', '话费'], amountRange: [100, 200] }, + ], + 服饰鞋包: [ + { merchant: '优衣库', descriptions: ['衣服', '裤子', '外套'], amountRange: [100, 500] }, + { merchant: 'ZARA', descriptions: ['衣服', '外套'], amountRange: [200, 600] }, + { merchant: 'Nike', descriptions: ['运动鞋', '运动服'], amountRange: [300, 800] }, + { merchant: '淘宝', descriptions: ['服装', '鞋子'], amountRange: [80, 300] }, + ], + 数码电器: [ + { merchant: '京东', descriptions: ['数码配件', '电子产品'], amountRange: [50, 500] }, + { merchant: '天猫', descriptions: ['手机配件', '电子产品'], amountRange: [30, 200] }, + { merchant: '苹果官网', descriptions: ['配件', '保护壳'], amountRange: [100, 300] }, + ], + 文化休闲: [ + { merchant: '腾讯视频', descriptions: ['VIP会员', '会员续费'], amountRange: [25, 30] }, + { merchant: '爱奇艺', descriptions: ['VIP会员', '会员'], amountRange: [25, 30] }, + { merchant: 'B站', descriptions: ['大会员', '会员'], amountRange: [25, 25] }, + { merchant: '万达影城', descriptions: ['电影票', '观影'], amountRange: [40, 100] }, + { merchant: '书店', descriptions: ['书籍', '购书'], amountRange: [30, 100] }, + ], + 运动健身: [ + { merchant: '迪卡侬', descriptions: ['运动装备', '健身用品'], amountRange: [100, 300] }, + { merchant: '健身房', descriptions: ['月卡', '私教课'], amountRange: [200, 500] }, + { merchant: '携程', descriptions: ['滑雪', '运动场馆'], amountRange: [150, 400] }, + ], + 医疗健康: [ + { merchant: '药店', descriptions: ['药品', '保健品'], amountRange: [30, 100] }, + { merchant: '医院', descriptions: ['挂号费', '门诊'], amountRange: [20, 100] }, + ], + 家居家装: [ + { merchant: '淘宝', descriptions: ['家居用品', '四件套', '收纳'], amountRange: [50, 200] }, + { merchant: '宜家', descriptions: ['家具', '家居'], amountRange: [100, 500] }, + ], +}; + +const paymentMethods = ['微信支付', '支付宝', '招商银行信用卡', '花呗', '工商银行储蓄卡']; + +// 生成单条支出记录 +function generateExpenseRecord(date: Date, category: string): BillRecord { + const categoryMerchants = merchants[category as keyof typeof merchants] || merchants['餐饮美食']; + const merchantInfo = categoryMerchants[Math.floor(Math.random() * categoryMerchants.length)]; + const description = merchantInfo.descriptions[Math.floor(Math.random() * merchantInfo.descriptions.length)]; + const amount = randomAmount(merchantInfo.amountRange[0], merchantInfo.amountRange[1]); + const paymentMethod = paymentMethods[Math.floor(Math.random() * paymentMethods.length)]; + const hour = Math.floor(Math.random() * 14) + 8; // 8:00 - 22:00 + const minute = Math.floor(Math.random() * 60); + + return { + time: formatDateTime(date, hour, minute), + category, + merchant: merchantInfo.merchant, + description, + income_expense: '支出', + amount, + payment_method: paymentMethod, + status: '交易成功', + remark: '', + needs_review: '', + }; +} + +// 生成收入记录 +function generateIncomeRecord(date: Date): BillRecord { + const incomeTypes = [ + { category: '退款', merchant: '淘宝', description: '商品退款', amountRange: [30, 200] }, + { category: '退款', merchant: '京东', description: '退货退款', amountRange: [50, 300] }, + { category: '其他收入', merchant: '微信红包', description: '红包', amountRange: [5, 100] }, + { category: '其他收入', merchant: '支付宝', description: '余额宝收益', amountRange: [1, 20] }, + { category: '其他收入', merchant: '微信转账', description: '朋友转账', amountRange: [100, 500] }, + ]; + + const incomeInfo = incomeTypes[Math.floor(Math.random() * incomeTypes.length)]; + const amount = randomAmount(incomeInfo.amountRange[0], incomeInfo.amountRange[1]); + const hour = Math.floor(Math.random() * 14) + 8; + const minute = Math.floor(Math.random() * 60); + + return { + time: formatDateTime(date, hour, minute), + category: incomeInfo.category, + merchant: incomeInfo.merchant, + description: incomeInfo.description, + income_expense: '收入', + amount, + payment_method: incomeInfo.category === '退款' ? '原路退回' : '微信零钱', + status: incomeInfo.category === '退款' ? '退款成功' : '已到账', + remark: '', + needs_review: '', + }; +} + +// 生成一天的记录 +function generateDayRecords(date: Date): BillRecord[] { + const records: BillRecord[] = []; + const dayOfWeek = date.getDay(); + const isWeekend = dayOfWeek === 0 || dayOfWeek === 6; + + // 每天固定的支出 + const categories = Object.keys(merchants); + + // 工作日:早餐、午餐、晚餐、交通 + // 周末:可能有更多消费 + const baseExpenses = isWeekend ? + Math.floor(Math.random() * 4) + 4 : // 周末 4-7 笔 + Math.floor(Math.random() * 3) + 3; // 工作日 3-5 笔 + + // 必有餐饮 + records.push(generateExpenseRecord(date, '餐饮美食')); + records.push(generateExpenseRecord(date, '餐饮美食')); + + // 可能有交通 + if (Math.random() > 0.3) { + records.push(generateExpenseRecord(date, '交通出行')); + } + + // 随机其他消费 + for (let i = 0; i < baseExpenses - 2; i++) { + const randomCategory = categories[Math.floor(Math.random() * categories.length)]; + records.push(generateExpenseRecord(date, randomCategory)); + } + + // 10% 概率有收入 + if (Math.random() < 0.1) { + records.push(generateIncomeRecord(date)); + } + + return records; +} + +// 生成指定日期范围的所有记录 +function generateRecords(startDate: Date, endDate: Date): BillRecord[] { + const records: BillRecord[] = []; + const currentDate = new Date(startDate); + + while (currentDate <= endDate) { + records.push(...generateDayRecords(new Date(currentDate))); + currentDate.setDate(currentDate.getDate() + 1); + } + + // 按时间排序(最新的在前) + return records.sort((a, b) => new Date(b.time).getTime() - new Date(a.time).getTime()); +} + +// 生成从2025年10月1日到2026年1月8日的数据 +const startDate = new Date(2025, 9, 1); // 2025-10-01 +const endDate = new Date(2026, 0, 8); // 2026-01-08 + +/** 演示数据(支付宝 + 微信支付混合数据,覆盖约100天) */ +export const demoRecords: BillRecord[] = generateRecords(startDate, endDate);