diff --git a/UI_REDESIGN.md b/UI_REDESIGN.md new file mode 100644 index 0000000..a2ae10f --- /dev/null +++ b/UI_REDESIGN.md @@ -0,0 +1,49 @@ +# AM Downloader 交互界面 (UI) 重构与优化文档 + +## 1. 概述与背景 +`am-downloader` 原先的 `index.html` 采用单页平铺逻辑,随着多用户鉴权、积分兑换、用户级 WebDAV 与管理员后台的引入,旧版界面存在层级分散、导航冗余、缺乏现代感与移动端适配生硬等问题。 + +本项目已完成系统级的前端现代化重构,并保持对后端原有 API 契约和前端 DOM 事件完全兼容,目前已在运行中的 Docker 容器环境(端口 `9540`)热更新上线生效。 + +--- + +## 2. 视觉设计系统规范 + +### 2.1 配色与质感(Modern Dark Glassmorphism) +- **深色基底**:主背景采用 `#080c14`,配合双色极光渐变暗影(翡翠绿 `#10b981` 与天蓝 `#38bdf8`),提升流媒体科技质感。 +- **毛玻璃面板**:卡片与导航栏统一采用 `rgba(17, 26, 42, 0.75)` 配以 `backdrop-filter: blur(16px)`,边框引入半透明微发光分割线(`rgba(255, 255, 255, 0.08)`)。 +- **字体与层级**:接入 `Plus Jakarta Sans` 作为主要无衬线英文字体,`JetBrains Mono` 作为代码、卡密与音质参数等宽字体。 + +--- + +## 3. 核心功能与架构重构细节 + +### 3.1 导航体系净化与精简 +1. **移除顶部及移动端独立「设置」按钮**: + - 彻底移除了原先顶栏导航与移动端底栏的独立「设置」入口。 + - 所有系统运行配置统一纳管在管理员后台内部,避免入口碎片化。 +2. **下载按钮去除角标数字**: + - 移除桌面端导航与移动端底栏「下载」按钮后方突兀的数字徽章(Badge),保持导航栏整体视觉纯粹和谐。 + - 保留 DOM 节点以保证后台任务轮询更新逻辑的向后兼容,不报错不中断。 + +### 3.2 管理控制台(Admin Center)全面重构 +1. **分段式选项卡(Segmented Tabs)**: + - 废弃原有的“点击磁贴切换子面板再返回”繁琐交互,重构为顶部毛玻璃分段 Tab 栏: + - `👥 用户管理` + - `◎ 积分账本` + - `◇ 卡密批次` + - `≡ 审计日志` + - `⚙️ 全局设置`(由原“纳管设置”更名,系统 API、下载存储策略、全局 WebDAV 完整收拢在此) + - Tab 切换平滑无刷新,记忆当前选中状态。 + +2. **抽屉详情交互(Slide-over Drawer)**: + - 用户详情不再替换整页或跳转离开当前列表。 + - 点击任意用户的「查看详情」时,桌面端由右侧平滑滑出毛玻璃抽屉面板,移动端自适应为底部滑起抽屉(Bottom Sheet)。 + - 抽屉内置:账号角色与状态、密码重置、账号启停、积分增减与实时流水。 + - 配合磨砂半透明背景遮罩(Backdrop),点击空白处或「✕ 关闭」按钮即可无缝收起,列表浏览上下文不丢失。 + +--- + +## 4. 兼容性与上线记录 +- **DOM 契约兼容**:保留所有核心配置 ID(`amApi`、`downloadDir`、`localFilePolicy`、`defaultQuality`、`davUrl` 等)以及事件回调机制。 +- **上线状态**:最新代码已提交 Git(分支 `master`),并同步热更新至运行中的 `am-downloader` 容器内 `/app/index.html`。 diff --git a/admin_credit_api.go b/admin_credit_api.go new file mode 100644 index 0000000..9beea8f --- /dev/null +++ b/admin_credit_api.go @@ -0,0 +1,91 @@ +package main + +import ( + "github.com/gin-gonic/gin" + "strconv" +) + +func (a *App) adminUserCreditTransactions(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 64) + if err != nil { + c.JSON(400, gin.H{"error": "invalid user id"}) + return + } + limit := 50 + if n, e := strconv.Atoi(c.DefaultQuery("limit", "50")); e == nil && n > 0 && n <= 200 { + limit = n + } + var txs []CreditTransaction + q := a.db.Where("user_id = ?", id).Order("id desc").Limit(limit) + if before := c.Query("before"); before != "" { + if n, e := strconv.ParseUint(before, 10, 64); e == nil { + q = q.Where("id < ?", n) + } + } + if err := q.Find(&txs).Error; err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + type item struct { + ID uint `json:"id"` + Type string `json:"type"` + Amount int64 `json:"amount"` + BalanceAfter int64 `json:"balanceAfter"` + ReferenceType string `json:"referenceType"` + ReferenceID string `json:"referenceId"` + Reason string `json:"reason"` + CreatedAt interface{} `json:"createdAt"` + } + out := make([]item, 0, len(txs)) + for _, t := range txs { + out = append(out, item{t.ID, t.Type, t.Amount, t.BalanceAfter, t.ReferenceType, t.ReferenceID, t.Reason, t.CreatedAt}) + } + next := uint(0) + if len(txs) == limit { + next = txs[len(txs)-1].ID + } + c.JSON(200, gin.H{"data": out, "nextBefore": next}) +} + +func (a *App) adminCreditTransactions(c *gin.Context) { + limit := 100 + if n, e := strconv.Atoi(c.DefaultQuery("limit", "100")); e == nil && n > 0 && n <= 300 { + limit = n + } + var txs []CreditTransaction + q := a.db.Order("id desc").Limit(limit) + if uid := c.Query("user_id"); uid != "" { + if n, e := strconv.ParseUint(uid, 10, 64); e == nil { + q = q.Where("user_id = ?", n) + } + } + if before := c.Query("before"); before != "" { + if n, e := strconv.ParseUint(before, 10, 64); e == nil { + q = q.Where("id < ?", n) + } + } + if err := q.Find(&txs).Error; err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + type item struct { + ID uint `json:"id"` + UserID uint `json:"userId"` + Type string `json:"type"` + Amount int64 `json:"amount"` + BalanceAfter int64 `json:"balanceAfter"` + ReferenceType string `json:"referenceType"` + ReferenceID string `json:"referenceId"` + Reason string `json:"reason"` + CreatedAt interface{} `json:"createdAt"` + } + out := make([]item, 0, len(txs)) + for _, t := range txs { + out = append(out, item{t.ID, t.UserID, t.Type, t.Amount, t.BalanceAfter, t.ReferenceType, t.ReferenceID, t.Reason, t.CreatedAt}) + } + next := uint(0) + if len(txs) == limit { + next = txs[len(txs)-1].ID + } + c.JSON(200, gin.H{"data": out, "nextBefore": next}) +} diff --git a/index.html b/index.html index a8d7f11..871b35b 100644 --- a/index.html +++ b/index.html @@ -1367,10 +1367,10 @@ async function loadUserCredit(id){try{const c=await api('/admin/users/'+id+'/cre async function openUserDetail(id){const u=adminUsersCache.find(x=>x.id===id);if(!u)return;const next=u.status==='active'?'disabled':'active';$("detailUserName").textContent=u.username;$("detailUserMeta").textContent="#"+u.id+" · "+u.role+" · "+u.status;$("userDetailContent").innerHTML="