fix: 修复微信账单金额解析问题(半角¥符号支持)

- 修复 parse_amount 函数同时支持全角¥和半角¥
- 新增 MonthRangePicker 日期选择组件
- 新增 /api/monthly-stats 接口获取月度统计
- 分析页面月度趋势使用全量数据
- 新增健康检查路由
This commit is contained in:
2026-01-10 19:21:24 +08:00
parent 9247e1ec7f
commit eb76c3a8dc
20 changed files with 597 additions and 44 deletions

View File

@@ -1,11 +1,10 @@
<script lang="ts">
import { onMount } from 'svelte';
import { fetchBills, checkHealth, type CleanedBill } from '$lib/api';
import { fetchBills, fetchMonthlyStats, checkHealth, type CleanedBill, type MonthlyStat } from '$lib/api';
import { Button } from '$lib/components/ui/button';
import { Badge } from '$lib/components/ui/badge';
import * as Card from '$lib/components/ui/card';
import { DateRangePicker } from '$lib/components/ui/date-range-picker';
import { formatLocalDate } from '$lib/utils';
import { MonthRangePicker } from '$lib/components/ui/month-range-picker';
import BarChart3 from '@lucide/svelte/icons/bar-chart-3';
import Loader2 from '@lucide/svelte/icons/loader-2';
import AlertCircle from '@lucide/svelte/icons/alert-circle';
@@ -25,7 +24,6 @@
// 数据处理服务
import {
calculateCategoryStats,
calculateMonthlyStats,
calculateDailyExpenseData,
calculateTotalStats,
calculatePieChartData,
@@ -37,15 +35,17 @@
// 分类数据
import { categories as allCategories } from '$lib/data/categories';
// 计算默认日期范围(本月)
// 计算默认日期范围(本月1日到今天
function getDefaultDates() {
const today = new Date();
const year = today.getFullYear();
const month = today.getMonth();
const startDate = formatLocalDate(new Date(year, month, 1));
const endDate = formatLocalDate(today);
return { startDate, endDate };
return {
startYear: today.getFullYear(),
startMonth: today.getMonth() + 1,
startDay: 1,
endYear: today.getFullYear(),
endMonth: today.getMonth() + 1,
endDay: today.getDate(),
};
}
const defaultDates = getDefaultDates();
@@ -53,12 +53,22 @@
let isLoading = $state(false);
let errorMessage = $state('');
let records: CleanedBill[] = $state([]);
let monthlyStats: MonthlyStat[] = $state([]); // 月度统计(全部数据)
let isDemo = $state(false);
let serverAvailable = $state(true);
// 时间范围筛选 - 初始化为默认值
let startDate: string = $state(defaultDates.startDate);
let endDate: string = $state(defaultDates.endDate);
// 日期范围筛选 - 初始化为默认值
let startYear = $state(defaultDates.startYear);
let startMonth = $state(defaultDates.startMonth);
let startDay = $state(defaultDates.startDay);
let endYear = $state(defaultDates.endYear);
let endMonth = $state(defaultDates.endMonth);
let endDay = $state(defaultDates.endDay);
// 辅助函数:将年月日转换为日期字符串
function toDateString(year: number, month: number, day: number): string {
return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
}
// 将 CleanedBill 转换为分析服务需要的格式
function toAnalysisRecords(bills: CleanedBill[]) {
@@ -79,7 +89,6 @@
// 派生分析数据
let analysisRecords = $derived(isDemo ? demoRecords : toAnalysisRecords(records));
let categoryStats = $derived(calculateCategoryStats(analysisRecords));
let monthlyStats = $derived(calculateMonthlyStats(analysisRecords));
let dailyExpenseData = $derived(calculateDailyExpenseData(analysisRecords));
let totalStats = $derived(calculateTotalStats(analysisRecords));
let pieChartData = $derived(calculatePieChartData(categoryStats, totalStats.expense));
@@ -113,19 +122,29 @@
return;
}
// 获取账单数据(带时间范围筛选)
const response = await fetchBills({
page_size: 10000,
start_date: startDate || undefined,
end_date: endDate || undefined,
});
if (response.result && response.data) {
records = response.data.bills || [];
// 并行获取数据:筛选后的账单 + 全部月度统计
const [billsResponse, monthlyResponse] = await Promise.all([
fetchBills({
page_size: 10000,
start_date: toDateString(startYear, startMonth, startDay),
end_date: toDateString(endYear, endMonth, endDay),
}),
fetchMonthlyStats(),
]);
// 处理账单数据
if (billsResponse.result && billsResponse.data) {
records = billsResponse.data.bills || [];
if (records.length === 0) {
errorMessage = '暂无账单数据';
}
} else {
errorMessage = response.message || '加载失败';
errorMessage = billsResponse.message || '加载失败';
}
// 处理月度统计数据
if (monthlyResponse.result && monthlyResponse.data) {
monthlyStats = monthlyResponse.data;
}
} catch (err) {
errorMessage = err instanceof Error ? err.message : '加载失败';
@@ -135,10 +154,30 @@
}
}
// 日期变化时重新加载
function onDateChange() {
// 日期变化时重新加载(只加载筛选后的数据,月度统计不变)
async function onDateChange() {
if (!isDemo) {
loadData();
isLoading = true;
errorMessage = '';
try {
const response = await fetchBills({
page_size: 10000,
start_date: toDateString(startYear, startMonth, startDay),
end_date: toDateString(endYear, endMonth, endDay),
});
if (response.result && response.data) {
records = response.data.bills || [];
if (records.length === 0) {
errorMessage = '暂无账单数据';
}
} else {
errorMessage = response.message || '加载失败';
}
} catch (err) {
errorMessage = err instanceof Error ? err.message : '加载失败';
} finally {
isLoading = false;
}
}
}
@@ -170,10 +209,14 @@
📊 示例数据
</Badge>
{:else}
<!-- 时间范围筛选 -->
<DateRangePicker
bind:startDate
bind:endDate
<!-- 日期范围筛选 -->
<MonthRangePicker
bind:startYear
bind:startMonth
bind:startDay
bind:endYear
bind:endMonth
bind:endDay
onchange={onDateChange}
/>
{/if}

View File

@@ -0,0 +1,47 @@
import { env } from '$env/dynamic/private';
import type { RequestHandler } from './$types';
// 服务端使用 Docker 内部地址,默认使用 localhost
const API_URL = env.API_URL || 'http://localhost:8080';
// 获取版本号(优先从环境变量,其次从 package.json
function getVersion(): string {
// 优先使用环境变量
if (process.env.npm_package_version) {
return process.env.npm_package_version;
}
// 回退到默认值
return '0.0.1';
}
const APP_VERSION = getVersion();
export const GET: RequestHandler = async ({ fetch }) => {
try {
const response = await fetch(`${API_URL}/health`);
const data = await response.json();
return new Response(
JSON.stringify({
status: data.status || 'ok',
version: APP_VERSION,
}),
{
status: response.status,
headers: { 'Content-Type': 'application/json' },
}
);
} catch (error) {
return new Response(
JSON.stringify({
status: 'error',
version: APP_VERSION,
}),
{
status: 503,
headers: { 'Content-Type': 'application/json' },
}
);
}
};