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:
clz
2026-04-02 17:52:38 +08:00
parent c4d8c2e105
commit ee163e123d
9 changed files with 259 additions and 80 deletions

View File

@@ -2,9 +2,11 @@
Guidelines for AI coding agents working on BillAI - a microservices bill analysis system. 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 ## Architecture
- `web/` - SvelteKit 5 + TailwindCSS 4 + TypeScript (Frontend, port 3000) - `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) - `analyzer/` - Python 3.12 + FastAPI (Data cleaning, port 8001)
SvelteKit proxies `/api/*` requests to Go backend via `web/src/routes/api/[...path]/+server.ts`. 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/server.py` | FastAPI entry |
| `analyzer/cleaners/base.py` | BaseCleaner ABC | | `analyzer/cleaners/base.py` | BaseCleaner ABC |
| `analyzer/category.py` | Category inference | | `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

View File

@@ -5,6 +5,7 @@ go 1.24.0
require ( require (
github.com/gin-gonic/gin v1.9.1 github.com/gin-gonic/gin v1.9.1
github.com/golang-jwt/jwt/v5 v5.3.0 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 github.com/yeka/zip v0.0.0-20231116150916-03d6312748a9
go.mongodb.org/mongo-driver v1.13.1 go.mongodb.org/mongo-driver v1.13.1
golang.org/x/text v0.34.0 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/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/xuri/efp v0.0.1 // 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/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
golang.org/x/arch v0.3.0 // indirect golang.org/x/arch v0.3.0 // indirect

View File

@@ -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.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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/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.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 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 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44=
github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ= github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 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-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-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.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 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= 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/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-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-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-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.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 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= 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-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.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 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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-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.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.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 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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= 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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 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.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 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= 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= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View 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,
})
}

View File

@@ -46,6 +46,9 @@ func setupAPIRoutes(r *gin.Engine) {
api.POST("/auth/login", handler.Login) api.POST("/auth/login", handler.Login)
api.GET("/auth/validate", handler.ValidateToken) api.GET("/auth/validate", handler.ValidateToken)
// 公开接口(无需登录)
api.GET("/changelog", handler.GetChangelog)
// 需要登录的 API // 需要登录的 API
authed := api.Group("/") authed := api.Group("/")
authed.Use(middleware.AuthRequired()) authed.Use(middleware.AuthRequired())

125
server/service/changelog.go Normal file
View 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)
}

View 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))
}
}

View File

@@ -5,7 +5,7 @@ import type { UIBill } from '$lib/models/bill';
// API 配置 - 使用相对路径,由 SvelteKit 代理到后端 // API 配置 - 使用相对路径,由 SvelteKit 代理到后端
const API_BASE = ''; 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); const headers = new Headers(init.headers);
if (browser) { if (browser) {

View File

@@ -3,77 +3,48 @@
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import Calendar from '@lucide/svelte/icons/calendar'; import Calendar from '@lucide/svelte/icons/calendar';
import Tag from '@lucide/svelte/icons/tag'; import Tag from '@lucide/svelte/icons/tag';
import { onMount } from 'svelte';
import { apiFetch } from '$lib/api';
let { open = $bindable(false) } = $props(); let { open = $bindable(false) } = $props();
// Changelog 内容(从 CHANGELOG.md 解析或硬编码) interface ChangelogEntry {
const changelog = [ version: string;
{ date: string;
version: '1.3.1', changes: Record<string, string[]>;
date: '2026-01-26', }
changes: {
优化: [ let changelog = $state<ChangelogEntry[]>([]);
'版本号显示优化 - 侧边栏版本号按钮样式改进', let isLoading = $state(false);
'移至次级导航区域,与其他菜单项样式一致', let error = $state<string | null>(null);
'更新日志改用 Sheet 组件(右侧滑出),替代底部 Drawer',
'统一暗色主题下的视觉效果' // 获取更新日志
] async function fetchChangelog() {
isLoading = true;
error = null;
try {
const response = await apiFetch('/api/changelog');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
} }
}, const data = await response.json();
{ if (data.result && Array.isArray(data.data)) {
version: '1.3.0', changelog = data.data;
date: '2026-01-26', } else {
changes: { throw new Error('Invalid response format');
新增: [
'京东账单支持 - 支持京东白条账单上传和清洗',
'自动识别京东账单类型(交易流水 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 接口'
]
} }
} 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> </script>
<Sheet.Root bind:open> <Sheet.Root bind:open>
@@ -85,7 +56,20 @@
</Sheet.Description> </Sheet.Description>
</Sheet.Header> </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"> <div class="space-y-8">
{#each changelog as release} {#each changelog as release}
<div class="space-y-3"> <div class="space-y-3">
@@ -120,7 +104,8 @@
</div> </div>
{/each} {/each}
</div> </div>
</div> {/if}
</div>
<Sheet.Footer class="border-t pt-4"> <Sheet.Footer class="border-t pt-4">
<Button variant="outline" onclick={() => (open = false)} class="w-full">关闭</Button> <Button variant="outline" onclick={() => (open = false)} class="w-full">关闭</Button>