feat: implement dynamic changelog loading from API
- Add GET /api/changelog endpoint to fetch changelog from CHANGELOG.md - Create service/changelog.go to parse CHANGELOG.md markdown file - Add handler/changelog.go to handle changelog requests - Update ChangelogDrawer component to fetch from API instead of hardcoded data - Export apiFetch from lib/api.ts for public use - Add changelog parser tests with 14 version entries verified
This commit is contained in:
11
AGENTS.md
11
AGENTS.md
@@ -2,9 +2,11 @@
|
||||
|
||||
Guidelines for AI coding agents working on BillAI - a microservices bill analysis system.
|
||||
|
||||
**Current Version:** 1.3.1 | Go: 1.24.0 | Node: 20+ | Python: 3.12+
|
||||
|
||||
## Architecture
|
||||
- `web/` - SvelteKit 5 + TailwindCSS 4 + TypeScript (Frontend, port 3000)
|
||||
- `server/` - Go 1.21 + Gin + MongoDB (API, port 8080)
|
||||
- `server/` - Go 1.24 + Gin + MongoDB (API, port 8080)
|
||||
- `analyzer/` - Python 3.12 + FastAPI (Data cleaning, port 8001)
|
||||
|
||||
SvelteKit proxies `/api/*` requests to Go backend via `web/src/routes/api/[...path]/+server.ts`.
|
||||
@@ -112,3 +114,10 @@ Upload: ZIP/XLSX → Extract → Convert UTF-8 CSV → Detect bill type → Dedu
|
||||
| `analyzer/server.py` | FastAPI entry |
|
||||
| `analyzer/cleaners/base.py` | BaseCleaner ABC |
|
||||
| `analyzer/category.py` | Category inference |
|
||||
|
||||
## Agent Guidelines
|
||||
- **Before coding:** Search codebase to understand existing patterns and dependencies
|
||||
- **Dependencies:** Check `package.json`/`go.mod`/`requirements.txt` before adding new packages
|
||||
- **Tests:** Always run relevant test suite before committing changes
|
||||
- **Git commits:** Provide clear messages explaining the "why" of changes
|
||||
- **File references:** Use `file_path:line_number` format when mentioning code locations
|
||||
|
||||
@@ -5,6 +5,7 @@ go 1.24.0
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/xuri/excelize/v2 v2.10.1
|
||||
github.com/yeka/zip v0.0.0-20231116150916-03d6312748a9
|
||||
go.mongodb.org/mongo-driver v1.13.1
|
||||
golang.org/x/text v0.34.0
|
||||
@@ -39,7 +40,6 @@ require (
|
||||
github.com/xdg-go/scram v1.1.2 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
github.com/xuri/efp v0.0.1 // indirect
|
||||
github.com/xuri/excelize/v2 v2.10.1 // indirect
|
||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
|
||||
@@ -67,9 +67,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44=
|
||||
github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
@@ -101,21 +101,18 @@ golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
|
||||
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
@@ -127,8 +124,6 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
@@ -139,8 +134,6 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
||||
26
server/handler/changelog.go
Normal file
26
server/handler/changelog.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"billai-server/service"
|
||||
)
|
||||
|
||||
// GetChangelog GET /api/changelog 获取版本变更日志
|
||||
func GetChangelog(c *gin.Context) {
|
||||
changelog, err := service.ParseChangelog()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"result": false,
|
||||
"message": "获取变更日志失败: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"result": true,
|
||||
"data": changelog,
|
||||
})
|
||||
}
|
||||
@@ -46,6 +46,9 @@ func setupAPIRoutes(r *gin.Engine) {
|
||||
api.POST("/auth/login", handler.Login)
|
||||
api.GET("/auth/validate", handler.ValidateToken)
|
||||
|
||||
// 公开接口(无需登录)
|
||||
api.GET("/changelog", handler.GetChangelog)
|
||||
|
||||
// 需要登录的 API
|
||||
authed := api.Group("/")
|
||||
authed.Use(middleware.AuthRequired())
|
||||
|
||||
125
server/service/changelog.go
Normal file
125
server/service/changelog.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ChangelogEntry 变更日志条目
|
||||
type ChangelogEntry struct {
|
||||
Version string `json:"version"`
|
||||
Date string `json:"date"`
|
||||
Changes map[string][]string `json:"changes"`
|
||||
}
|
||||
|
||||
// ParseChangelog 解析 CHANGELOG.md 文件
|
||||
func ParseChangelog() ([]ChangelogEntry, error) {
|
||||
// 获取项目根目录
|
||||
rootDir := os.Getenv("PROJECT_ROOT")
|
||||
if rootDir == "" {
|
||||
// 如果未设置,使用相对路径推测
|
||||
rootDir = ".."
|
||||
}
|
||||
|
||||
changelogPath := filepath.Join(rootDir, "CHANGELOG.md")
|
||||
|
||||
file, err := os.Open(changelogPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("打开 CHANGELOG.md 失败: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var entries []ChangelogEntry
|
||||
var currentEntry *ChangelogEntry
|
||||
var currentCategory string
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
// 匹配版本号行 ## [1.4.0] - 2026-03-23
|
||||
if strings.HasPrefix(line, "## [") && strings.Contains(line, "]") {
|
||||
// 保存前一个 entry
|
||||
if currentEntry != nil {
|
||||
entries = append(entries, *currentEntry)
|
||||
}
|
||||
|
||||
// 解析版本号和日期
|
||||
version, date := parseVersionLine(line)
|
||||
if version != "" && version != "Unreleased" {
|
||||
currentEntry = &ChangelogEntry{
|
||||
Version: version,
|
||||
Date: date,
|
||||
Changes: make(map[string][]string),
|
||||
}
|
||||
currentCategory = ""
|
||||
} else {
|
||||
currentEntry = nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 跳过 Unreleased 和其他非版本行
|
||||
if currentEntry == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 匹配分类行 ### 新增、### 优化等
|
||||
if strings.HasPrefix(line, "### ") {
|
||||
currentCategory = strings.TrimPrefix(line, "### ")
|
||||
if currentEntry.Changes[currentCategory] == nil {
|
||||
currentEntry.Changes[currentCategory] = []string{}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 匹配项目行 - 项目描述
|
||||
if strings.HasPrefix(line, "- ") && currentCategory != "" {
|
||||
item := strings.TrimPrefix(line, "- ")
|
||||
// 移除加粗标记和链接等
|
||||
item = cleanItem(item)
|
||||
currentEntry.Changes[currentCategory] = append(currentEntry.Changes[currentCategory], item)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存最后一个 entry
|
||||
if currentEntry != nil {
|
||||
entries = append(entries, *currentEntry)
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("扫描文件失败: %w", err)
|
||||
}
|
||||
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// parseVersionLine 解析版本行 ## [1.4.0] - 2026-03-23
|
||||
func parseVersionLine(line string) (version, date string) {
|
||||
// 提取版本号
|
||||
startIdx := strings.Index(line, "[")
|
||||
endIdx := strings.Index(line, "]")
|
||||
if startIdx >= 0 && endIdx > startIdx {
|
||||
version = line[startIdx+1 : endIdx]
|
||||
}
|
||||
|
||||
// 提取日期
|
||||
dateStartIdx := strings.LastIndex(line, "- ") + 2
|
||||
if dateStartIdx > 1 {
|
||||
date = strings.TrimSpace(line[dateStartIdx:])
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// cleanItem 清理项目描述(移除加粗标记等)
|
||||
func cleanItem(item string) string {
|
||||
// 移除加粗标记 **text**
|
||||
item = strings.ReplaceAll(item, "**", "")
|
||||
// 移除代码标记 `text`
|
||||
item = strings.ReplaceAll(item, "`", "")
|
||||
return strings.TrimSpace(item)
|
||||
}
|
||||
38
server/service/changelog_test.go
Normal file
38
server/service/changelog_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseChangelog(t *testing.T) {
|
||||
// 设置项目根目录
|
||||
os.Setenv("PROJECT_ROOT", "../..")
|
||||
|
||||
changelog, err := ParseChangelog()
|
||||
if err != nil {
|
||||
t.Fatalf("ParseChangelog failed: %v", err)
|
||||
}
|
||||
|
||||
if len(changelog) == 0 {
|
||||
t.Fatal("No changelog entries parsed")
|
||||
}
|
||||
|
||||
// 验证第一个条目(应该是 1.4.0)
|
||||
firstEntry := changelog[0]
|
||||
t.Logf("First entry: v%s - %s", firstEntry.Version, firstEntry.Date)
|
||||
|
||||
if firstEntry.Version != "1.4.0" {
|
||||
t.Errorf("Expected first version to be 1.4.0, got %s", firstEntry.Version)
|
||||
}
|
||||
|
||||
if len(firstEntry.Changes) == 0 {
|
||||
t.Error("First entry has no changes")
|
||||
}
|
||||
|
||||
// 打印所有版本
|
||||
t.Logf("Total versions: %d", len(changelog))
|
||||
for _, entry := range changelog {
|
||||
t.Logf(" - v%s: %d categories", entry.Version, len(entry.Changes))
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import type { UIBill } from '$lib/models/bill';
|
||||
// API 配置 - 使用相对路径,由 SvelteKit 代理到后端
|
||||
const API_BASE = '';
|
||||
|
||||
async function apiFetch(input: RequestInfo | URL, init: RequestInit = {}) {
|
||||
export async function apiFetch(input: RequestInfo | URL, init: RequestInit = {}) {
|
||||
const headers = new Headers(init.headers);
|
||||
|
||||
if (browser) {
|
||||
|
||||
@@ -3,77 +3,48 @@
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import Calendar from '@lucide/svelte/icons/calendar';
|
||||
import Tag from '@lucide/svelte/icons/tag';
|
||||
import { onMount } from 'svelte';
|
||||
import { apiFetch } from '$lib/api';
|
||||
|
||||
let { open = $bindable(false) } = $props();
|
||||
|
||||
// Changelog 内容(从 CHANGELOG.md 解析或硬编码)
|
||||
const changelog = [
|
||||
{
|
||||
version: '1.3.1',
|
||||
date: '2026-01-26',
|
||||
changes: {
|
||||
优化: [
|
||||
'版本号显示优化 - 侧边栏版本号按钮样式改进',
|
||||
'移至次级导航区域,与其他菜单项样式一致',
|
||||
'更新日志改用 Sheet 组件(右侧滑出),替代底部 Drawer',
|
||||
'统一暗色主题下的视觉效果'
|
||||
]
|
||||
interface ChangelogEntry {
|
||||
version: string;
|
||||
date: string;
|
||||
changes: Record<string, string[]>;
|
||||
}
|
||||
|
||||
let changelog = $state<ChangelogEntry[]>([]);
|
||||
let isLoading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// 获取更新日志
|
||||
async function fetchChangelog() {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await apiFetch('/api/changelog');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
},
|
||||
{
|
||||
version: '1.3.0',
|
||||
date: '2026-01-26',
|
||||
changes: {
|
||||
新增: [
|
||||
'京东账单支持 - 支持京东白条账单上传和清洗',
|
||||
'自动识别京东账单类型(交易流水 ZIP)',
|
||||
'解析京东白条账单 CSV 格式(含还款日期信息)',
|
||||
'京东专属分类映射配置',
|
||||
'支持京东外卖、京东平台商户等商户识别',
|
||||
'上传页面和账单列表页面添加"京东"选项'
|
||||
],
|
||||
优化: [
|
||||
'京东订单智能去重 - 上传京东账单时自动软删除其他来源中的京东订单',
|
||||
'分类推断复核等级优化 - 京东账单引入 LOW 复核等级',
|
||||
'京东平台商户关键词扩展'
|
||||
],
|
||||
技术改进: [
|
||||
'新增京东账单清理器',
|
||||
'新增京东专属配置',
|
||||
'后端新增软删除接口',
|
||||
'新增单元测试(11 个测试用例)'
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
version: '1.2.1',
|
||||
date: '2026-01-23',
|
||||
changes: {
|
||||
优化: [
|
||||
'智能复核快捷确认 - 在复核列表每行添加快捷确认按钮',
|
||||
'无需打开详情页面即可确认分类正确',
|
||||
'自动更新统计数据',
|
||||
'提升复核效率,支持快速批量确认'
|
||||
],
|
||||
文档: ['AGENTS.md 更新 - 精简为 150 行,专为 AI 编程助手设计']
|
||||
}
|
||||
},
|
||||
{
|
||||
version: '1.2.0',
|
||||
date: '2026-01-25',
|
||||
changes: {
|
||||
新增: [
|
||||
'账单删除功能 - 支持在账单详情抽屉中删除账单(软删除)',
|
||||
'删除按钮带二次确认,防止误操作',
|
||||
'已删除的账单在所有查询中自动过滤'
|
||||
],
|
||||
技术改进: [
|
||||
'后端 MongoDB 查询方法添加软删除过滤',
|
||||
'新增 DELETE /api/bills/:id 接口'
|
||||
]
|
||||
const data = await response.json();
|
||||
if (data.result && Array.isArray(data.data)) {
|
||||
changelog = data.data;
|
||||
} else {
|
||||
throw new Error('Invalid response format');
|
||||
}
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Failed to fetch changelog';
|
||||
console.error('Failed to fetch changelog:', err);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMount(() => {
|
||||
fetchChangelog();
|
||||
});
|
||||
</script>
|
||||
|
||||
<Sheet.Root bind:open>
|
||||
@@ -85,7 +56,20 @@
|
||||
</Sheet.Description>
|
||||
</Sheet.Header>
|
||||
|
||||
<div class="flex-1 overflow-y-auto py-6">
|
||||
<div class="flex-1 overflow-y-auto py-6">
|
||||
{#if isLoading}
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<div class="text-muted-foreground">加载中...</div>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<div class="text-destructive text-sm">{error}</div>
|
||||
</div>
|
||||
{:else if changelog.length === 0}
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<div class="text-muted-foreground">暂无更新日志</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-8">
|
||||
{#each changelog as release}
|
||||
<div class="space-y-3">
|
||||
@@ -120,7 +104,8 @@
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Sheet.Footer class="border-t pt-4">
|
||||
<Button variant="outline" onclick={() => (open = false)} class="w-full">关闭</Button>
|
||||
|
||||
Reference in New Issue
Block a user