feat: 添加 /api/review-stats 端点和仪表盘实时数据集成
This commit is contained in:
487
server/README.md
Normal file
487
server/README.md
Normal file
@@ -0,0 +1,487 @@
|
||||
# BillAI 服务器 API 文档
|
||||
|
||||
## 概述
|
||||
|
||||
BillAI 服务器是一个基于 Go 的后端服务,用于处理账单的上传、清洗、分析和存储。提供 REST API 接口供前端调用。
|
||||
|
||||
**服务地址**: `http://localhost:8080`
|
||||
|
||||
---
|
||||
|
||||
## API 接口列表
|
||||
|
||||
### 1. 健康检查
|
||||
|
||||
**端点**: `GET /health`
|
||||
|
||||
**功能**: 检查服务器健康状态
|
||||
|
||||
**参数**: 无
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 账单管理 API
|
||||
|
||||
### 2. 上传账单文件
|
||||
|
||||
**端点**: `POST /api/upload`
|
||||
|
||||
**功能**: 上传支付宝或微信账单文件进行清洗和分析
|
||||
|
||||
**请求方式**: `multipart/form-data`
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 参数名 | 类型 | 必需 | 说明 |
|
||||
|-------|------|------|------|
|
||||
| file | File | ✓ | 账单文件(CSV格式) |
|
||||
| type | string | ✓ | 账单类型:`alipay` 或 `wechat` |
|
||||
| format | string | - | 输出格式:`csv` 或 `json`(默认: csv) |
|
||||
| year | number | - | 账单年份(用于数据验证) |
|
||||
| month | number | - | 账单月份(用于数据验证) |
|
||||
|
||||
**响应示例** (成功):
|
||||
```json
|
||||
{
|
||||
"result": true,
|
||||
"message": "账单处理完成",
|
||||
"data": {
|
||||
"bill_type": "alipay",
|
||||
"raw_count": 50,
|
||||
"cleaned_count": 48,
|
||||
"duplicate_count": 2,
|
||||
"needs_review_count": 5,
|
||||
"file_name": "20260110_150405_alipay_1.csv"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例** (全部重复):
|
||||
```json
|
||||
{
|
||||
"result": true,
|
||||
"message": "文件中的 50 条记录全部已存在,无需重复导入",
|
||||
"data": {
|
||||
"bill_type": "alipay",
|
||||
"raw_count": 0,
|
||||
"cleaned_count": 0,
|
||||
"duplicate_count": 50
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例** (失败):
|
||||
```json
|
||||
{
|
||||
"result": false,
|
||||
"message": "账单类型无效,仅支持 alipay 或 wechat"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 获取账单列表
|
||||
|
||||
**端点**: `GET /api/bills`
|
||||
|
||||
**功能**: 获取清洗后的账单数据,支持分页、筛选和排序
|
||||
|
||||
**请求参数** (Query):
|
||||
|
||||
| 参数名 | 类型 | 必需 | 说明 |
|
||||
|-------|------|------|------|
|
||||
| page | number | - | 页码(从1开始,默认: 1) |
|
||||
| page_size | number | - | 每页数量(默认: 20,最大: 100) |
|
||||
| start_date | string | - | 开始日期(YYYY-MM-DD 格式) |
|
||||
| end_date | string | - | 结束日期(YYYY-MM-DD 格式) |
|
||||
| category | string | - | 交易分类筛选 |
|
||||
| type | string | - | 账单来源:`alipay`、`wechat` 或 `manual` |
|
||||
| income_expense | string | - | 收支类型:`收入` 或 `支出` |
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"result": true,
|
||||
"data": {
|
||||
"total": 150,
|
||||
"page": 1,
|
||||
"page_size": 20,
|
||||
"pages": 8,
|
||||
"total_expense": 5250.50,
|
||||
"total_income": 8000.00,
|
||||
"bills": [
|
||||
{
|
||||
"_id": "507f1f77bcf86cd799439011",
|
||||
"bill_type": "alipay",
|
||||
"transaction_id": "2021123456789",
|
||||
"merchant_order_no": "2021123456",
|
||||
"time": "2026-01-10T10:30:00Z",
|
||||
"category": "餐饮美食",
|
||||
"merchant": "星巴克",
|
||||
"description": "咖啡",
|
||||
"income_expense": "支出",
|
||||
"amount": 28.00,
|
||||
"pay_method": "支付宝",
|
||||
"status": "交易成功",
|
||||
"remark": "员工补贴",
|
||||
"review_level": "",
|
||||
"created_at": "2026-01-10T10:30:00Z",
|
||||
"updated_at": "2026-01-10T10:30:00Z",
|
||||
"source_file": "20260110_150405_alipay_1.csv",
|
||||
"upload_batch": "20260110_150405"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**:
|
||||
```json
|
||||
{
|
||||
"result": false,
|
||||
"message": "数据库未连接"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. 手动创建账单
|
||||
|
||||
**端点**: `POST /api/bills/manual`
|
||||
|
||||
**功能**: 手动添加单条或批量账单,支持去重
|
||||
|
||||
**请求方式**: `application/json`
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"bills": [
|
||||
{
|
||||
"time": "2026-01-10 14:30:00",
|
||||
"category": "餐饮美食",
|
||||
"merchant": "测试餐厅",
|
||||
"description": "午餐",
|
||||
"income_expense": "支出",
|
||||
"amount": 50.00,
|
||||
"pay_method": "支付宝",
|
||||
"status": "交易成功",
|
||||
"remark": "测试账单"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**请求参数说明**:
|
||||
|
||||
| 参数名 | 类型 | 必需 | 说明 |
|
||||
|-------|------|------|------|
|
||||
| bills | Array | ✓ | 账单数组 |
|
||||
| bills[].time | string | ✓ | 交易时间(格式: `YYYY-MM-DD HH:mm:ss`) |
|
||||
| bills[].category | string | ✓ | 交易分类 |
|
||||
| bills[].income_expense | string | ✓ | 收支类型:`收入` 或 `支出` |
|
||||
| bills[].amount | number | ✓ | 金额(>0) |
|
||||
| bills[].merchant | string | - | 交易对方(可选) |
|
||||
| bills[].description | string | - | 商品说明 |
|
||||
| bills[].pay_method | string | - | 支付方式 |
|
||||
| bills[].status | string | - | 交易状态(默认: `交易成功`) |
|
||||
| bills[].remark | string | - | 备注 |
|
||||
|
||||
**响应示例** (成功):
|
||||
```json
|
||||
{
|
||||
"result": true,
|
||||
"message": "创建成功",
|
||||
"data": {
|
||||
"success": 1,
|
||||
"failed": 0,
|
||||
"duplicates": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例** (部分失败):
|
||||
```json
|
||||
{
|
||||
"result": true,
|
||||
"message": "创建成功",
|
||||
"data": {
|
||||
"success": 3,
|
||||
"failed": 1,
|
||||
"duplicates": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**:
|
||||
```json
|
||||
{
|
||||
"result": false,
|
||||
"message": "时间格式错误: 2026-01-10"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. 获取需要复核的记录
|
||||
|
||||
**端点**: `GET /api/review`
|
||||
|
||||
**功能**: 获取清洗过程中标记为需要人工复核的记录
|
||||
|
||||
**请求参数** (Query):
|
||||
|
||||
| 参数名 | 类型 | 必需 | 说明 |
|
||||
|-------|------|------|------|
|
||||
| file | string | ✓ | 输出文件名(来自上传接口返回值) |
|
||||
| page | number | - | 页码(默认: 1) |
|
||||
| page_size | number | - | 每页数量(默认: 20) |
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"result": true,
|
||||
"data": {
|
||||
"total": 5,
|
||||
"page": 1,
|
||||
"page_size": 20,
|
||||
"high_count": 2,
|
||||
"low_count": 3,
|
||||
"records": [
|
||||
{
|
||||
"row": 15,
|
||||
"time": "2026-01-10 10:30:00",
|
||||
"merchant": "未知商户",
|
||||
"amount": 100.00,
|
||||
"category": "未分类",
|
||||
"review_level": "HIGH",
|
||||
"issue": "无法识别的商户名称",
|
||||
"suggestion": "请手动确认或修改商户名称"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. 获取月度统计数据
|
||||
|
||||
**端点**: `GET /api/monthly-stats`
|
||||
|
||||
**功能**: 获取每个月的收入、支出统计(不受日期筛选影响,返回全部月份数据)
|
||||
|
||||
**请求参数**: 无
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"result": true,
|
||||
"data": [
|
||||
{
|
||||
"month": "2025-12",
|
||||
"expense": 8234.50,
|
||||
"income": 15000.00
|
||||
},
|
||||
{
|
||||
"month": "2026-01",
|
||||
"expense": 5250.50,
|
||||
"income": 8000.00
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**:
|
||||
```json
|
||||
{
|
||||
"result": false,
|
||||
"message": "数据库未连接"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据模型
|
||||
|
||||
### 账单数据 (CleanedBill)
|
||||
|
||||
```go
|
||||
type CleanedBill struct {
|
||||
ID primitive.ObjectID `bson:"_id,omitempty" json:"_id"`
|
||||
BillType string `bson:"bill_type" json:"bill_type"` // alipay/wechat/manual
|
||||
TransactionID string `bson:"transaction_id" json:"transaction_id"`
|
||||
MerchantOrderNo string `bson:"merchant_order_no" json:"merchant_order_no"`
|
||||
Time time.Time `bson:"time" json:"time"`
|
||||
Category string `bson:"category" json:"category"`
|
||||
Merchant string `bson:"merchant" json:"merchant"`
|
||||
Description string `bson:"description" json:"description"`
|
||||
IncomeExpense string `bson:"income_expense" json:"income_expense"` // 收入/支出
|
||||
Amount float64 `bson:"amount" json:"amount"`
|
||||
PayMethod string `bson:"pay_method" json:"pay_method"`
|
||||
Status string `bson:"status" json:"status"`
|
||||
Remark string `bson:"remark" json:"remark"`
|
||||
ReviewLevel string `bson:"review_level" json:"review_level"` // HIGH/LOW/""
|
||||
CreatedAt time.Time `bson:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `bson:"updated_at" json:"updated_at"`
|
||||
SourceFile string `bson:"source_file" json:"source_file"`
|
||||
UploadBatch string `bson:"upload_batch" json:"upload_batch"`
|
||||
}
|
||||
```
|
||||
|
||||
### 月度统计 (MonthlyStat)
|
||||
|
||||
```go
|
||||
type MonthlyStat struct {
|
||||
Month string `bson:"month" json:"month"` // YYYY-MM
|
||||
Expense float64 `bson:"expense" json:"expense"` // 支出总额
|
||||
Income float64 `bson:"income" json:"income"` // 收入总额
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误处理
|
||||
|
||||
所有 API 响应都遵循以下格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"result": boolean,
|
||||
"message": "错误或成功消息",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
### 常见错误码
|
||||
|
||||
| HTTP 状态码 | result | 说明 |
|
||||
|-----------|--------|------|
|
||||
| 200 | true | 请求成功 |
|
||||
| 200 | false | 业务逻辑错误(如参数不合法) |
|
||||
| 400 | false | 请求参数错误 |
|
||||
| 404 | false | 资源不存在 |
|
||||
| 500 | false | 服务器内部错误 |
|
||||
|
||||
---
|
||||
|
||||
## 时区说明
|
||||
|
||||
- **存储**: 所有时间在数据库中按 **UTC 时区** 存储(ISO 8601 格式)
|
||||
- **输入**: 手动添加账单时,时间按 **本地时区** 解析(使用 `time.ParseInLocation`)
|
||||
- **输出**: API 返回的时间均为 ISO 8601 UTC 格式
|
||||
|
||||
### 时间格式
|
||||
|
||||
- 手动创建账单时: `YYYY-MM-DD HH:mm:ss`(本地时间)
|
||||
- 日期筛选参数: `YYYY-MM-DD`(本地日期)
|
||||
- API 返回时间: ISO 8601 UTC 格式(如 `2026-01-10T10:30:00Z`)
|
||||
|
||||
---
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 上传账单文件
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/upload \
|
||||
-F "file=@statement.csv" \
|
||||
-F "type=alipay" \
|
||||
-F "format=csv"
|
||||
```
|
||||
|
||||
### 查询账单列表
|
||||
|
||||
```bash
|
||||
curl "http://localhost:8080/api/bills?page=1&page_size=20&start_date=2026-01-01&end_date=2026-01-10&type=alipay"
|
||||
```
|
||||
|
||||
### 手动添加账单
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/bills/manual \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"bills": [
|
||||
{
|
||||
"time": "2026-01-10 14:30:00",
|
||||
"category": "餐饮美食",
|
||||
"merchant": "星巴克",
|
||||
"income_expense": "支出",
|
||||
"amount": 28.00,
|
||||
"pay_method": "支付宝"
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
### 获取月度统计
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/api/monthly-stats
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 配置
|
||||
|
||||
服务器配置文件位于 `config.yaml`,主要配置项:
|
||||
|
||||
```yaml
|
||||
server:
|
||||
port: "8080"
|
||||
|
||||
mongodb:
|
||||
uri: "mongodb://admin:password@localhost:27017"
|
||||
database: "billai"
|
||||
collections:
|
||||
raw: "bills_raw"
|
||||
cleaned: "bills_cleaned"
|
||||
|
||||
analyzer:
|
||||
mode: "http" # http 或 process
|
||||
url: "http://analyzer:8001"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 开发信息
|
||||
|
||||
### 目录结构
|
||||
|
||||
```
|
||||
server/
|
||||
├── main.go # 程序入口
|
||||
├── router/ # 路由配置
|
||||
├── handler/ # 请求处理器
|
||||
├── service/ # 业务逻辑
|
||||
├── repository/ # 数据访问层
|
||||
├── database/ # 数据库连接
|
||||
├── model/ # 数据模型
|
||||
├── adapter/ # 外部服务适配器
|
||||
├── config/ # 配置管理
|
||||
└── README.md # 本文件
|
||||
```
|
||||
|
||||
### 主要依赖
|
||||
|
||||
- **Gin**: Web 框架
|
||||
- **MongoDB Driver**: 数据库驱动
|
||||
- **Go 1.21**: 运行环境
|
||||
|
||||
---
|
||||
|
||||
## 版本历史
|
||||
|
||||
| 版本 | 日期 | 说明 |
|
||||
|-----|------|------|
|
||||
| 1.0.0 | 2026-01-10 | 初始版本,支持账单上传、查询、手动添加和统计 |
|
||||
|
||||
@@ -198,3 +198,43 @@ func MonthlyStats(c *gin.Context) {
|
||||
Data: stats,
|
||||
})
|
||||
}
|
||||
|
||||
// ReviewStats 获取待复核数据统计
|
||||
func ReviewStats(c *gin.Context) {
|
||||
repo := repository.GetRepository()
|
||||
|
||||
// 从MongoDB查询所有需要复核的账单
|
||||
bills, err := repo.GetBillsNeedReview()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.ReviewResponse{
|
||||
Result: false,
|
||||
Message: "查询失败: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
highCount := 0
|
||||
lowCount := 0
|
||||
totalCount := 0
|
||||
|
||||
// 统计各等级的复核记录
|
||||
for _, bill := range bills {
|
||||
totalCount++
|
||||
if bill.ReviewLevel == "HIGH" {
|
||||
highCount++
|
||||
} else if bill.ReviewLevel == "LOW" {
|
||||
lowCount++
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.ReviewResponse{
|
||||
Result: true,
|
||||
Message: "获取成功",
|
||||
Data: &model.ReviewData{
|
||||
Total: totalCount,
|
||||
High: highCount,
|
||||
Low: lowCount,
|
||||
Records: nil, // 统计接口不返回具体记录
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -55,5 +55,8 @@ func setupAPIRoutes(r *gin.Engine) {
|
||||
|
||||
// 月度统计(全部数据)
|
||||
api.GET("/monthly-stats", handler.MonthlyStats)
|
||||
|
||||
// 待复核数据统计
|
||||
api.GET("/review-stats", handler.ReviewStats)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,3 +318,14 @@ export async function createManualBills(bills: ManualBillInput[]): Promise<Creat
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// 获取待复核数据统计
|
||||
export async function fetchReviewStats(): Promise<ReviewResponse> {
|
||||
const response = await fetch(`${API_BASE}/api/review-stats`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { uploadBill, type UploadResponse, type BillType } from '$lib/api';
|
||||
import { onMount } from 'svelte';
|
||||
import { uploadBill, fetchMonthlyStats, fetchReviewStats, fetchBills, type UploadResponse, type BillType } from '$lib/api';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
@@ -23,37 +24,145 @@
|
||||
let uploadResult: UploadResponse | null = $state(null);
|
||||
let errorMessage = $state('');
|
||||
|
||||
// 模拟统计数据
|
||||
const stats = [
|
||||
// 实时统计数据
|
||||
let stats = $state([
|
||||
{
|
||||
title: '本月支出',
|
||||
value: '¥12,580.00',
|
||||
change: '+12.5%',
|
||||
trend: 'up',
|
||||
description: '较上月增加'
|
||||
value: '¥0.00',
|
||||
change: '+0%',
|
||||
trend: 'up' as const,
|
||||
description: '加载中...'
|
||||
},
|
||||
{
|
||||
title: '本月收入',
|
||||
value: '¥25,000.00',
|
||||
change: '+8.2%',
|
||||
trend: 'up',
|
||||
description: '较上月增加'
|
||||
value: '¥0.00',
|
||||
change: '+0%',
|
||||
trend: 'up' as const,
|
||||
description: '加载中...'
|
||||
},
|
||||
{
|
||||
title: '待复核',
|
||||
value: '23',
|
||||
change: '-15%',
|
||||
trend: 'down',
|
||||
value: '0',
|
||||
change: '+0%',
|
||||
trend: 'up' as const,
|
||||
description: '需要人工确认'
|
||||
},
|
||||
{
|
||||
title: '已处理账单',
|
||||
value: '156',
|
||||
change: '+25%',
|
||||
trend: 'up',
|
||||
value: '0',
|
||||
change: '+0%',
|
||||
trend: 'up' as const,
|
||||
description: '累计处理记录'
|
||||
},
|
||||
];
|
||||
]);
|
||||
|
||||
// 加载真实统计数据
|
||||
async function loadStats() {
|
||||
try {
|
||||
const now = new Date();
|
||||
const currentMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
|
||||
|
||||
// 计算上月
|
||||
const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
||||
const previousMonth = `${lastMonth.getFullYear()}-${String(lastMonth.getMonth() + 1).padStart(2, '0')}`;
|
||||
|
||||
console.log('Current month:', currentMonth);
|
||||
console.log('Previous month:', previousMonth);
|
||||
|
||||
// 获取月度统计数据
|
||||
const monthlyResponse = await fetchMonthlyStats();
|
||||
console.log('Monthly response:', monthlyResponse);
|
||||
const monthlyStats = monthlyResponse.data || [];
|
||||
console.log('Monthly stats:', monthlyStats);
|
||||
|
||||
// 获取待复核统计
|
||||
const reviewResponse = await fetchReviewStats();
|
||||
console.log('Review response:', reviewResponse);
|
||||
const reviewTotal = reviewResponse.data?.total || 0;
|
||||
console.log('Review total:', reviewTotal);
|
||||
|
||||
// 获取已处理账单数量
|
||||
const billsResponse = await fetchBills({ page_size: 1 });
|
||||
console.log('Bills response:', billsResponse);
|
||||
const billTotal = billsResponse.data?.total || 0;
|
||||
console.log('Bill total:', billTotal);
|
||||
|
||||
// 提取当月和上月的数据
|
||||
const currentData = monthlyStats.find(m => m.month === currentMonth);
|
||||
const previousData = monthlyStats.find(m => m.month === previousMonth);
|
||||
|
||||
console.log('Current data:', currentData);
|
||||
console.log('Previous data:', previousData);
|
||||
|
||||
// 计算支出变化百分比
|
||||
const currentExpense = currentData?.expense || 0;
|
||||
const previousExpense = previousData?.expense || 0;
|
||||
const expenseChange = previousExpense > 0
|
||||
? ((currentExpense - previousExpense) / previousExpense * 100).toFixed(1)
|
||||
: 0;
|
||||
const expenseTrend = parseFloat(expenseChange.toString()) >= 0 ? 'up' : 'down';
|
||||
|
||||
// 计算收入变化百分比
|
||||
const currentIncome = currentData?.income || 0;
|
||||
const previousIncome = previousData?.income || 0;
|
||||
const incomeChange = previousIncome > 0
|
||||
? ((currentIncome - previousIncome) / previousIncome * 100).toFixed(1)
|
||||
: 0;
|
||||
const incomeTrend = parseFloat(incomeChange.toString()) >= 0 ? 'up' : 'down';
|
||||
|
||||
// 格式化金额
|
||||
const formatAmount = (amount: number) => {
|
||||
return `¥${amount.toFixed(2)}`;
|
||||
};
|
||||
|
||||
const formatChange = (change: number | string) => {
|
||||
const changeNum = typeof change === 'string' ? parseFloat(change) : change;
|
||||
const sign = changeNum >= 0 ? '+' : '';
|
||||
return `${sign}${changeNum.toFixed(1)}%`;
|
||||
};
|
||||
|
||||
const newStats = [
|
||||
{
|
||||
title: '本月支出',
|
||||
value: formatAmount(currentExpense),
|
||||
change: formatChange(expenseChange),
|
||||
trend: expenseTrend,
|
||||
description: '较上月' + (expenseTrend === 'up' ? '增加' : '减少')
|
||||
},
|
||||
{
|
||||
title: '本月收入',
|
||||
value: formatAmount(currentIncome),
|
||||
change: formatChange(incomeChange),
|
||||
trend: incomeTrend,
|
||||
description: '较上月' + (incomeTrend === 'up' ? '增加' : '减少')
|
||||
},
|
||||
{
|
||||
title: '待复核',
|
||||
value: reviewTotal.toString(),
|
||||
change: '+0%',
|
||||
trend: 'up' as const,
|
||||
description: '需要人工确认'
|
||||
},
|
||||
{
|
||||
title: '已处理账单',
|
||||
value: billTotal.toString(),
|
||||
change: '+0%',
|
||||
trend: 'up' as const,
|
||||
description: '累计处理记录'
|
||||
},
|
||||
];
|
||||
|
||||
console.log('New stats:', newStats);
|
||||
stats = newStats;
|
||||
} catch (err) {
|
||||
console.error('Failed to load stats:', err);
|
||||
// 保持默认状态
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadStats();
|
||||
});
|
||||
|
||||
function handleDragOver(e: DragEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
Reference in New Issue
Block a user