diff --git a/.helloagents/CHANGELOG.md b/.helloagents/CHANGELOG.md index e2df0b7..2b99b49 100644 --- a/.helloagents/CHANGELOG.md +++ b/.helloagents/CHANGELOG.md @@ -13,3 +13,17 @@ - **[admin-frontend]**: 将登录页、主布局和仪表盘重构为 Apple 风格,并移除高成本视觉装饰以缓解页面卡顿 — by yinjianm - 方案: [202604210400_admin-frontend-apple-performance-refresh](archive/2026-04/202604210400_admin-frontend-apple-performance-refresh/) - 决策: admin-frontend-apple-performance-refresh#D001(采用 Apple 风格并优先性能减法), admin-frontend-apple-performance-refresh#D002(保留逻辑层只替换视图皮层) + +## [0.1.2] - 2026-04-21 + +### 快速修改 +- **[admin-frontend]**: 修正仪表盘金额显示单位,将后端返回的“分”统一转换为“元”后再格式化 — by yinjianm + - 类型: 快速修改(无方案包) + - 文件: admin-frontend/src/utils/dashboard.ts:75 + +## [0.2.0] - 2026-04-21 + +### 新增 +- **[admin-frontend]**: 新增用户管理工作台、抽屉表单、用户操作菜单,以及“用户管理 / 工单管理”导航与路由骨架 — by yinjianm + - 方案: [202604210441_admin-frontend-user-management](archive/2026-04/202604210441_admin-frontend-user-management/) + - 决策: admin-frontend-user-management#D001(新增用户采用两段式创建), admin-frontend-user-management#D002(先补齐用户与工单入口结构) diff --git a/.helloagents/INDEX.md b/.helloagents/INDEX.md index 0b8de79..9b89f88 100644 --- a/.helloagents/INDEX.md +++ b/.helloagents/INDEX.md @@ -11,11 +11,11 @@ active_package: 无 - 类型: PHP Laravel 主仓 + `admin-frontend` Vue3 管理端前端 - 当前重点模块: `admin-frontend` -- 最新归档: `202604210326_admin-frontend-composio-dashboard` +- 最新归档: `202604210441_admin-frontend-user-management` ## 活跃模块 -- [admin-frontend](modules/admin-frontend.md): 管理端登录、主布局、仪表盘与管理 API 前端封装 +- [admin-frontend](modules/admin-frontend.md): 管理端登录、主布局、仪表盘、用户管理与管理 API 前端封装 ## 归档与变更 diff --git a/.helloagents/archive/2026-04/202604210441_admin-frontend-user-management/.status.json b/.helloagents/archive/2026-04/202604210441_admin-frontend-user-management/.status.json new file mode 100644 index 0000000..e5c1c6c --- /dev/null +++ b/.helloagents/archive/2026-04/202604210441_admin-frontend-user-management/.status.json @@ -0,0 +1 @@ +{"status":"completed","completed":8,"failed":0,"pending":0,"total":8,"done":8,"percent":100,"current":"已完成用户管理页面、路由与构建验证,待归档方案包","updated_at":"2026-04-21 05:05:00"} diff --git a/.helloagents/archive/2026-04/202604210441_admin-frontend-user-management/proposal.md b/.helloagents/archive/2026-04/202604210441_admin-frontend-user-management/proposal.md new file mode 100644 index 0000000..b3754ae --- /dev/null +++ b/.helloagents/archive/2026-04/202604210441_admin-frontend-user-management/proposal.md @@ -0,0 +1,208 @@ +# 变更提案: admin-frontend-user-management + +## 元信息 +```yaml +类型: 新功能 +方案类型: implementation +优先级: P1 +状态: 已完成 +创建: 2026-04-21 +``` + +--- + +## 1. 需求 + +### 背景 +当前 `admin-frontend` 已完成登录、主布局与仪表盘,但业务路由仍只有 `/dashboard`。用户本轮明确要求继续沿 [.claude/plan/admin-frontend-login.md](/E:/code/php/Xboard-new/.claude/plan/admin-frontend-login.md) 推进,按照 [apple/DESIGN.md](/E:/code/php/Xboard-new/apple/DESIGN.md) 的 Apple 风格,为后台补齐“用户管理 / 工单管理”入口,且优先完整实现“用户管理”页面。参考图已经给出了目标交互形态,包括左侧菜单分组、用户列表、行内更多操作菜单,以及右侧抽屉式新增/编辑用户表单。 + +### 目标 +- 在管理端新增“用户管理”业务页,完成菜单、路由、页面与真实接口接入。 +- 让用户管理页具备可用的搜索、分页、状态展示、更多操作菜单,以及新增/编辑用户抽屉。 +- 预留“工单管理”菜单与路由入口,使后台导航结构与参考图对齐,但本轮不展开工单业务实现。 + +### 约束条件 +```yaml +时间约束: 本轮只完整实现“用户管理”,工单管理仅补路由入口和占位页 +性能约束: 保持当前轻量 Apple 风格,不新增重型表格或状态管理依赖 +兼容性约束: 保持现有 Vue3 + TypeScript + Vite + Element Plus 栈与 hash 路由模式 +业务约束: 后端接口沿用现有 `/api/v2/{secure_path}/user/*`、`/plan/fetch`,不改 Laravel API +``` + +### 验收标准 +- [ ] 管理端左侧导航新增“用户管理”分组,包含“用户管理”和“工单管理”两个入口。 +- [ ] 用户管理页可通过真实接口完成列表读取、分页、基础筛选、状态/套餐/流量展示。 +- [ ] 用户管理页支持新增用户、编辑用户、复制订阅地址、重置密钥、封禁和删除等操作入口,并带明确确认反馈。 +- [ ] `admin-frontend` 构建通过,新增页面在桌面和移动端都能正常访问。 + +--- + +## 2. 方案 + +### 技术方案 +本轮采用“扩展现有管理壳层 + 新增用户管理业务模块”的方案: + +1. 扩展管理端数据层 + 在 `src/types/api.d.ts` 中补充用户、套餐、分页与表单类型;在 `src/api/admin.ts` 中新增用户列表、用户详情、套餐列表、用户创建/更新/重置密钥/封禁/删除等请求封装。 + +2. 新增用户管理视图 + 在 `src/views/users/` 下拆分页面与抽屉组件。列表页负责搜索、表格、分页、更多操作菜单;抽屉组件负责新增/编辑表单。视觉上延续 Apple 风格的浅灰画布、白色内容区与单一蓝色交互重点。 + +3. 对齐后端真实创建能力 + 后端 `user/generate` 只能直接创建基础字段(邮箱、密码、套餐、到期时间),无法一次性写入完整表单字段。因此新增用户时采用“两段式”流程: + - 先调用 `user/generate` 创建基础账号 + - 再按邮箱回查用户 ID,并调用 `user/update` 补齐流量、余额、佣金、权限、限速、设备数、备注等扩展字段 + +4. 补齐导航与路由 + 将当前仅有仪表盘的侧边栏调整为分组导航,新增 `/users` 路由和 `/tickets` 占位路由;本轮仅实现 `UsersView` 的完整业务功能。 + +### 影响范围 +```yaml +涉及模块: + - admin-frontend/src/router: 新增用户管理与工单管理路由 + - admin-frontend/src/layouts: 调整侧边栏菜单结构与导航文案 + - admin-frontend/src/api: 扩展用户与套餐相关请求 + - admin-frontend/src/types: 新增用户管理数据类型 + - admin-frontend/src/views/users: 新增用户列表页与表单抽屉 + - admin-frontend/src/views/tickets: 新增工单管理占位页 +预计变更文件: 7-9 +``` + +### 风险评估 +| 风险 | 等级 | 应对 | +|------|------|------| +| 后端新增用户接口字段不足,无法一次提交完整表单 | 中 | 前端采用 `generate + fetch + update` 两段式创建流程 | +| 参考图中的“在线设备”字段后端列表接口未直接返回 | 中 | 本轮优先展示可用业务字段,设备相关展示使用已有限制字段和可退化文案 | +| 用户删除属于破坏性操作 | 中 | 在前端增加显式确认和操作完成提示,避免误删 | + +--- + +## 3. 技术设计(可选) + +> 涉及路由扩展、API映射与表单编排,需填写。 + +### 架构设计 +```mermaid +flowchart TD + A[AdminLayout 侧边栏菜单] --> B[UsersView 用户管理页] + A --> C[TicketsPlaceholderView 工单占位页] + B --> D[UserToolbar 搜索与操作] + B --> E[UserTable 列表与更多操作] + B --> F[UserFormDrawer 新增/编辑抽屉] + B --> G[admin.ts 用户管理接口] + F --> G + G --> H[/user/fetch] + G --> I[/user/generate] + G --> J[/user/update] + G --> K[/user/resetSecret] + G --> L[/user/ban] + G --> M[/user/destroy] + G --> N[/plan/fetch] +``` + +### API设计 +#### ANY /api/v2/{secure_path}/user/fetch +- **请求**: `current`, `pageSize`, `filter[]`, `sort[]` +- **响应**: `{ data: UserListItem[], total: number }` + +#### GET /api/v2/{secure_path}/user/getUserInfoById +- **请求**: `id` +- **响应**: 单个用户详情,含邀请人信息 + +#### POST /api/v2/{secure_path}/user/generate +- **请求**: `email_prefix`, `email_suffix`, `password`, `plan_id`, `expired_at` +- **响应**: `success(true)` 或批量结果 + +#### POST /api/v2/{secure_path}/user/update +- **请求**: `id` + 用户扩展字段(余额、佣金、流量、权限、限速、设备数、备注等) +- **响应**: `success(true)` + +#### GET /api/v2/{secure_path}/plan/fetch +- **请求**: 无 +- **响应**: 套餐列表,用于表单选择和表格展示 + +### 数据模型 +| 字段 | 类型 | 说明 | +|------|------|------| +| AdminUserListItem | object | 用户列表行数据,包含套餐、邀请人、权限组、流量、状态等 | +| AdminUserFormModel | object | 抽屉表单模型,覆盖新增/编辑时的基础与扩展字段 | +| AdminPlanOption | object | 订阅计划选项,用于表单下拉与列表展示 | +| AdminPaginationResult | object | 用户列表分页结果 | + +--- + +## 4. 核心场景 + +> 执行完成后同步到对应模块文档 + +### 场景: 浏览用户列表 +**模块**: UsersView +**条件**: 管理员已登录并进入 `/users` +**行为**: 页面读取用户列表、套餐信息并渲染搜索栏、表格、分页 +**结果**: 管理员可快速查看用户状态、流量、到期时间与套餐 + +### 场景: 新增用户 +**模块**: UserFormDrawer / admin.ts +**条件**: 管理员在用户管理页点击“创建用户” +**行为**: 管理员填写抽屉表单,前端先生成基础账号,再补写扩展字段 +**结果**: 新用户创建成功,列表自动刷新并提示结果 + +### 场景: 编辑或执行行内操作 +**模块**: UsersView / UserFormDrawer +**条件**: 列表中存在目标用户 +**行为**: 管理员打开更多菜单执行编辑、复制订阅地址、重置密钥、封禁或删除 +**结果**: 对应操作完成并反馈到列表状态 + +--- + +## 5. 技术决策 + +> 本方案涉及的技术决策,归档后成为决策的唯一完整记录 + +### admin-frontend-user-management#D001: 新增用户采用“两段式创建”以兼容现有后端接口 +**日期**: 2026-04-21 +**状态**: ✅采纳 +**背景**: 参考表单包含余额、佣金、流量、角色、限速、设备数、备注等字段,但后端 `user/generate` 仅支持基础创建字段。 +**选项分析**: +| 选项 | 优点 | 缺点 | +|------|------|------| +| A: 仅用 `user/generate` 并缩减表单字段 | 实现简单 | 无法对齐参考页字段深度 | +| B: 先 `generate`,再按邮箱回查并 `update` | 能保留完整表单能力 | 前端流程更复杂 | +**决策**: 选择方案 B +**理由**: 既不改后端,也能最大化还原参考抽屉中的管理能力。 +**影响**: `admin.ts`、`UsersView`、`UserFormDrawer` + +### admin-frontend-user-management#D002: 先补齐用户/工单路由结构,但本轮仅交付完整用户页 +**日期**: 2026-04-21 +**状态**: ✅采纳 +**背景**: 用户要求“添加用户管理、工单管理路由,先完成用户管理”,既要有完整导航结构,又要控制当前实现范围。 +**选项分析**: +| 选项 | 优点 | 缺点 | +|------|------|------| +| A: 只加用户管理,不处理工单入口 | 实现最小 | 与用户点名的导航结构不一致 | +| B: 同时补齐用户/工单入口,工单先占位 | 与目标结构一致,后续扩展顺滑 | 需要新增一个占位页面 | +**决策**: 选择方案 B +**理由**: 先把信息架构铺平,后续实现工单页时不必再改侧边栏和路由骨架。 +**影响**: `router/index.ts`、`layouts/AdminLayout.vue` + +--- + +## 6. 成果设计 + +> 含视觉产出的任务由 DESIGN Phase2 填充。非视觉任务整节标注"N/A"。 + +### 设计方向 +- **美学基调**: Apple Admin Ledger。像 Apple 系统设置和内部运营面板的结合体,用极少的颜色和干净层级承载高密度数据。 +- **记忆点**: 浅灰页面基底上嵌入一块大尺寸白色数据工作台,右侧抽屉像系统级面板一样滑入。 +- **参考**: 用户提供的用户列表、操作菜单、抽屉表单和侧边栏截图 + [apple/DESIGN.md](/E:/code/php/Xboard-new/apple/DESIGN.md) + +### 视觉要素 +- **配色**: 背景 `#f5f5f7`、表格/抽屉 `#ffffff`、标题 `#1d1d1f`、强调蓝 `#0071e3`、危险态 `#c93428` +- **字体**: 延续当前系统字体栈 `-apple-system`, `BlinkMacSystemFont`, `SF Pro Display`, `SF Pro Text`, `Helvetica Neue`, Arial, sans-serif` +- **布局**: 页面顶部为标题与操作条,中部为单块白色表格工作区;抽屉从右侧进入,表单按字段组自然分段 +- **动效**: 仅保留表格 hover、高亮状态和抽屉进出动画,避免复杂动效 +- **氛围**: 轻边框、软阴影、玻璃顶栏,避免深色重装饰和泛滥卡片分割 + +### 技术约束 +- **可访问性**: 所有状态色均保留文字标签;删除、封禁等危险操作必须有二次确认 +- **响应式**: 窄屏下工具栏折行、表格横向滚动、抽屉宽度自适应到视口 diff --git a/.helloagents/archive/2026-04/202604210441_admin-frontend-user-management/tasks.md b/.helloagents/archive/2026-04/202604210441_admin-frontend-user-management/tasks.md new file mode 100644 index 0000000..745485b --- /dev/null +++ b/.helloagents/archive/2026-04/202604210441_admin-frontend-user-management/tasks.md @@ -0,0 +1,62 @@ +# 任务清单: admin-frontend-user-management + +> **@status:** completed | 2026-04-21 05:02 + +```yaml +@feature: admin-frontend-user-management +@created: 2026-04-21 +@status: completed +@mode: R2 +``` + +## 进度概览 + +| 完成 | 失败 | 跳过 | 总数 | +|------|------|------|------| +| 8 | 0 | 0 | 8 | + +--- + +## 任务列表 + +### 1. 数据层与类型 + +- [√] 1.1 在 `admin-frontend/src/types/api.d.ts` 中补充用户管理、套餐、分页和表单类型 | depends_on: [] +- [√] 1.2 在 `admin-frontend/src/api/admin.ts` 中新增用户列表、详情、创建、更新、套餐、重置密钥、封禁、删除等接口封装 | depends_on: [1.1] + +### 2. 用户管理视图 + +- [√] 2.1 新增 `admin-frontend/src/views/users/UserFormDrawer.vue`,实现新增/编辑用户抽屉表单和两段式创建流程 | depends_on: [1.1,1.2] +- [√] 2.2 新增 `admin-frontend/src/views/users/UsersView.vue`,实现搜索、列表、分页、状态展示和更多操作菜单 | depends_on: [1.1,1.2] +- [√] 2.3 在 `admin-frontend/src/views/users/UsersView.vue` 中完成抽屉联动、复制订阅地址、重置密钥、封禁、删除与刷新反馈 | depends_on: [2.1,2.2] + +### 3. 导航与路由 + +- [√] 3.1 在 `admin-frontend/src/router/index.ts` 与 `admin-frontend/src/layouts/AdminLayout.vue` 中补齐“用户管理 / 工单管理”菜单和路由结构 | depends_on: [2.2] +- [√] 3.2 新增 `admin-frontend/src/views/tickets/TicketsView.vue` 作为工单管理占位页,保持后续扩展入口稳定 | depends_on: [3.1] + +### 4. 验收 + +- [√] 4.1 完成 `admin-frontend` 构建验证并修正新增页面引入的问题 | depends_on: [2.3,3.2] + +--- + +## 执行日志 + +| 时间 | 任务 | 状态 | 备注 | +|------|------|------|------| +| 2026-04-21 04:41 | 方案包初始化 | completed | 已确认本轮完整实现用户管理,工单管理仅补入口与占位页 | +| 2026-04-21 04:49 | 1.1 / 1.2 | completed | 已补齐用户管理类型定义与 admin API 封装 | +| 2026-04-21 04:54 | 2.1 / 2.2 / 2.3 | completed | 已完成用户列表页、抽屉表单和更多操作菜单联动 | +| 2026-04-21 04:56 | 3.1 / 3.2 | completed | 已补齐用户管理 / 工单管理菜单与路由,并新增工单占位页 | +| 2026-04-21 05:00 | 4.1 | completed | `npm run build` 通过;使用管理员账号验证登录后已跳转 `/users`,静态 preview 下真实数据请求因缺少 Laravel 运行环境未完成 | + +--- + +## 执行备注 + +> 记录执行过程中的重要说明、决策变更、风险提示等 + +- 本轮新增用户需要兼容后端 `user/generate` 的字段限制,前端会做创建后补写。 +- 参考图中的工单管理仅作为下一步入口,本轮不实现工单列表、会话与回复逻辑。 +- 真实数据接口联调受当前终端无 `php` 运行环境限制,浏览器验证覆盖到登录成功、路由跳转和页面结构渲染,未覆盖 Laravel 注入的 `window.settings` 与真实后台数据返回。 diff --git a/.helloagents/archive/_index.md b/.helloagents/archive/_index.md index 8c61c4d..ca8e9ef 100644 --- a/.helloagents/archive/_index.md +++ b/.helloagents/archive/_index.md @@ -7,6 +7,7 @@ | 时间戳 | 名称 | 类型 | 涉及模块 | 决策 | 结果 | |--------|------|------|---------|------|------| +| 202604210441 | admin-frontend-user-management | - | - | - | ✅完成 | | 202604210400 | admin-frontend-apple-performance-refresh | implementation | admin-frontend | admin-frontend-apple-performance-refresh#D001,#D002 | ✅完成 | | 202604210326 | admin-frontend-composio-dashboard | implementation | admin-frontend | admin-frontend-composio-dashboard#D001,#D002 | ✅完成 | | 202604180040 | optimize-docker-publish-workflow | - | - | - | ✅完成 | @@ -17,6 +18,7 @@ ## 按月归档 ### 2026-04 +- [202604210441_admin-frontend-user-management](./2026-04/202604210441_admin-frontend-user-management/) - 新增用户管理工作台、抽屉表单、用户操作菜单,以及“用户管理 / 工单管理”导航与路由骨架 - [202604210400_admin-frontend-apple-performance-refresh](./2026-04/202604210400_admin-frontend-apple-performance-refresh/) - Apple 风格重构登录页、主布局和仪表盘,并移除高成本装饰层以缓解卡顿 - [202604210326_admin-frontend-composio-dashboard](./2026-04/202604210326_admin-frontend-composio-dashboard/) - 深色 Composio 风格管理端仪表盘、登录回跳与真实统计数据接入 diff --git a/.helloagents/context.md b/.helloagents/context.md index 69da77d..e90159b 100644 --- a/.helloagents/context.md +++ b/.helloagents/context.md @@ -17,6 +17,13 @@ - `stat/getTrafficRank` - `system/getSystemStatus` - `system/getQueueStats` +- 管理端用户管理现已接入: + - `user/fetch` + - `user/generate` + - `user/update` + - `user/resetSecret` + - `user/destroy` + - `plan/fetch` ## 项目概述 @@ -27,10 +34,11 @@ ## 开发约定 - 管理端路由使用 Hash 模式 +- 管理端当前业务路由包含 `/dashboard`、`/users` 与 `/tickets` - Bearer Token 存储于 `sessionStorage/localStorage` - `admin-frontend` 的视觉方向当前以 Apple 风格为基线,优先纯色分区、系统字体栈和低装饰成本 ## 当前约束 -- 本地预览环境默认缺少真实 `secure_path` 与管理员凭证 +- 本地静态 preview 环境默认缺少 Laravel 注入的 `window.settings` 与真实管理 API,受保护页面只能验证结构与跳转,不能替代完整联调 - 后端接口契约以仓库内 Controller/Route 为准,不在前端推断字段 diff --git a/.helloagents/modules/_index.md b/.helloagents/modules/_index.md index a145521..3d2875d 100644 --- a/.helloagents/modules/_index.md +++ b/.helloagents/modules/_index.md @@ -2,4 +2,4 @@ | 模块名 | 说明 | 最近更新 | |--------|------|----------| -| [admin-frontend](admin-frontend.md) | 管理端前端登录、布局、仪表盘与管理 API 封装 | 2026-04-21 | +| [admin-frontend](admin-frontend.md) | 管理端前端登录、布局、仪表盘、用户管理与管理 API 封装 | 2026-04-21 | diff --git a/.helloagents/modules/admin-frontend.md b/.helloagents/modules/admin-frontend.md index 305367d..e4170c6 100644 --- a/.helloagents/modules/admin-frontend.md +++ b/.helloagents/modules/admin-frontend.md @@ -3,8 +3,8 @@ ## 职责 - 提供 Vue3 管理端登录页、认证状态、路由守卫和主布局 -- 封装管理端统计/系统状态接口 -- 渲染后台仪表盘、排行、队列和系统运行状态 +- 封装管理端统计/系统状态、用户管理和套餐查询接口 +- 渲染后台仪表盘、用户管理工作台,以及预留的工单管理入口 ## 行为规范 @@ -12,11 +12,14 @@ - 受保护路由在未登录时会自动附加 `redirect` 查询参数 - API 基础路径使用 `/api/v2/{secure_path}`,其中 `secure_path` 来自运行时配置 - 仪表盘以真实后端接口返回值为准,不在前端伪造业务统计 +- 用户管理页通过真实后端 `user/fetch`、`user/update`、`user/generate`、`user/resetSecret`、`user/destroy` 与 `plan/fetch` 完成数据读写 +- 新增用户时采用“先 generate,后按邮箱回查并 update”的两段式流程,以兼容后端基础创建接口 - 当前首页视觉基线为 Apple 风格:纯色分区、系统字体栈、单一蓝色强调和轻量层次 - 性能优化优先级高于装饰性表达,避免远程字体、全局模糊背景和固定特效层 ## 依赖关系 - 依赖 `src/api/client.ts` 处理 axios 与认证头 +- 依赖 `src/utils/users.ts` 负责用户管理表单转换、筛选组装和状态计算 - 依赖 Laravel 注入的 `window.settings` - 构建输出到 `public/assets/admin` diff --git a/.helloagents/plan/202604210515_admin-frontend-ticket-management/proposal.md b/.helloagents/plan/202604210515_admin-frontend-ticket-management/proposal.md new file mode 100644 index 0000000..d3ea9c6 --- /dev/null +++ b/.helloagents/plan/202604210515_admin-frontend-ticket-management/proposal.md @@ -0,0 +1,126 @@ +# 变更提案: admin-frontend-ticket-management + +## 元信息 +```yaml +类型: 新功能/修复/重构/优化 +方案类型: implementation +优先级: P0/P1/P2/P3 +状态: 草稿 +创建: 2026-04-21 +``` + +--- + +## 1. 需求 + +### 背景 +{为什么需要这个变更} + +### 目标 +{要达成什么目标} + +### 约束条件 +```yaml +时间约束: {如有} +性能约束: {如有} +兼容性约束: {如有} +业务约束: {如有} +``` + +### 验收标准 +- [ ] {标准1} +- [ ] {标准2} + +--- + +## 2. 方案 + +### 技术方案 +{简要描述实现方式} + +### 影响范围 +```yaml +涉及模块: + - {模块1}: {影响说明} +预计变更文件: {数量} +``` + +### 风险评估 +| 风险 | 等级 | 应对 | +|------|------|------| +| {风险} | 高/中/低 | {措施} | + +--- + +## 3. 技术设计(可选) + +> 涉及架构变更、API设计、数据模型变更时填写 + +### 架构设计 +```mermaid +flowchart TD + A[组件A] --> B[组件B] +``` + +### API设计 +#### {METHOD} {路径} +- **请求**: {结构} +- **响应**: {结构} + +### 数据模型 +| 字段 | 类型 | 说明 | +|------|------|------| +| {字段} | {类型} | {说明} | + +--- + +## 4. 核心场景 + +> 执行完成后同步到对应模块文档 + +### 场景: {场景名称} +**模块**: {所属模块} +**条件**: {前置条件} +**行为**: {操作描述} +**结果**: {预期结果} + +--- + +## 5. 技术决策 + +> 本方案涉及的技术决策,归档后成为决策的唯一完整记录 + +### admin-frontend-ticket-management#D001: {决策标题} +**日期**: 2026-04-21 +**状态**: ✅采纳 / ❌废弃 / ⏸搁置 +**背景**: {为什么需要这个决策} +**选项分析**: +| 选项 | 优点 | 缺点 | +|------|------|------| +| A: {方案A} | {优点} | {缺点} | +| B: {方案B} | {优点} | {缺点} | +**决策**: 选择方案{X} +**理由**: {详细理由} +**影响**: {对哪些模块有影响} + +--- + +## 6. 成果设计 + +> 含视觉产出的任务由 DESIGN Phase2 填充。非视觉任务整节标注"N/A"。 + +### 设计方向 +- **美学基调**: {鲜明的美学方向名称+具体描述 — 禁止"现代简洁"等空泛词} +- **记忆点**: {这个设计最令人难忘的一个特征} +- **参考**: {URL/截图/风格描述/无} + +### 视觉要素 +- **配色**: {主色+强调色+背景色,色值或方向} +- **字体**: {展示字体(标题)+正文字体配对+选择理由,避免 Arial/Inter/Roboto 等通用字体} +- **布局**: {整体结构+空间策略} +- **动效**: {入场动画/状态切换/交互反馈的动态效果策略} +- **氛围**: {背景处理/纹理/阴影/透明叠层等纵深细节} + +### 技术约束 +- **可访问性**: {对比度/语义化/导航要求/N/A} +- **响应式**: {断点/适配策略/N/A} diff --git a/.helloagents/plan/202604210515_admin-frontend-ticket-management/tasks.md b/.helloagents/plan/202604210515_admin-frontend-ticket-management/tasks.md new file mode 100644 index 0000000..4f542f3 --- /dev/null +++ b/.helloagents/plan/202604210515_admin-frontend-ticket-management/tasks.md @@ -0,0 +1,41 @@ +# 任务清单: admin-frontend-ticket-management + +```yaml +@feature: admin-frontend-ticket-management +@created: 2026-04-21 +@status: pending +@mode: {R2|R3} +``` + +## 进度概览 + +| 完成 | 失败 | 跳过 | 总数 | +|------|------|------|------| +| 0 | 0 | 0 | X | + +--- + +## 任务列表 + +### 1. {阶段/模块名称} + +- [ ] 1.1 在 `{文件路径}` 中实现 {具体功能} +- [ ] 1.2 在 `{文件路径}` 中实现 {具体功能} + - 依赖: 1.1 + +### 2. {阶段/模块名称} + +- [ ] 2.1 {任务描述} + +--- + +## 执行日志 + +| 时间 | 任务 | 状态 | 备注 | +|------|------|------|------| + +--- + +## 执行备注 + +> 记录执行过程中的重要说明、决策变更、风险提示等 diff --git a/.helloagents/user/.kb_sync_needed b/.helloagents/user/.kb_sync_needed index ff24fd4..26db3d0 100644 --- a/.helloagents/user/.kb_sync_needed +++ b/.helloagents/user/.kb_sync_needed @@ -1 +1 @@ -2026-04-21T03:13:51.504502 \ No newline at end of file +2026-04-21T04:32:43.019652 \ No newline at end of file diff --git a/admin-frontend/.env.production b/admin-frontend/.env.production index 864a3a6..ac91b54 100644 --- a/admin-frontend/.env.production +++ b/admin-frontend/.env.production @@ -1,2 +1,2 @@ VITE_API_BASE_URL=/api/v2 -VITE_ADMIN_PATH= +VITE_ADMIN_PATH=adminadmin diff --git a/admin-frontend/src/api/admin.ts b/admin-frontend/src/api/admin.ts index 617d929..1202c41 100644 --- a/admin-frontend/src/api/admin.ts +++ b/admin-frontend/src/api/admin.ts @@ -1,5 +1,11 @@ import { adminClient } from './client' import type { + AdminPaginationResult, + AdminPlanOption, + AdminUserFetchParams, + AdminUserGeneratePayload, + AdminUserListItem, + AdminUserUpdatePayload, ApiResponse, DashboardStats, OrderTrendData, @@ -14,6 +20,25 @@ function unwrap(url: string, params?: Record): Promise res.data) } +function unwrapPost(url: string, data?: Record): Promise> { + return adminClient + .post>(url, data) + .then((res) => res.data) +} + +function splitEmail(email: string): { email_prefix: string; email_suffix: string } { + const normalized = email.trim() + const atIndex = normalized.lastIndexOf('@') + if (atIndex <= 0 || atIndex === normalized.length - 1) { + throw new Error('请输入有效的邮箱地址') + } + + return { + email_prefix: normalized.slice(0, atIndex), + email_suffix: normalized.slice(atIndex + 1), + } +} + export function getDashboardStats(): Promise> { return unwrap('/stat/getStats') } @@ -53,3 +78,39 @@ export function getSystemStatus(): Promise> { export function getQueueStats(): Promise> { return unwrap('/system/getQueueStats') } + +export function getPlans(): Promise> { + return unwrap('/plan/fetch') +} + +export function fetchUsers(params: AdminUserFetchParams): Promise> { + return adminClient + .get>('/user/fetch', { params }) + .then((res) => res.data) +} + +export function getUserById(id: number): Promise> { + return unwrap('/user/getUserInfoById', { id }) +} + +export function createUser(payload: AdminUserGeneratePayload): Promise> { + const email = splitEmail(payload.email) + return unwrapPost('/user/generate', { + ...email, + password: payload.password, + plan_id: payload.plan_id, + expired_at: payload.expired_at, + }) +} + +export function updateUser(payload: AdminUserUpdatePayload): Promise> { + return unwrapPost('/user/update', payload as unknown as Record) +} + +export function resetUserSecret(id: number): Promise> { + return unwrapPost('/user/resetSecret', { id }) +} + +export function deleteUser(id: number): Promise> { + return unwrapPost('/user/destroy', { id }) +} diff --git a/admin-frontend/src/layouts/AdminLayout.vue b/admin-frontend/src/layouts/AdminLayout.vue index de1cb77..8163059 100644 --- a/admin-frontend/src/layouts/AdminLayout.vue +++ b/admin-frontend/src/layouts/AdminLayout.vue @@ -5,9 +5,12 @@ import { useAuthStore } from '@/stores/auth' import { useAppStore } from '@/stores/app' import { Odometer, + Tickets, SwitchButton, Fold, Expand, + User, + UserFilled, } from '@element-plus/icons-vue' const route = useRoute() @@ -24,6 +27,11 @@ const menuItems = [ { index: '/dashboard', title: '仪表盘', icon: Odometer }, ] +const managementItems = [ + { index: '/users', title: '用户管理', icon: User }, + { index: '/tickets', title: '工单管理', icon: Tickets }, +] + function syncViewport() { isMobile.value = window.innerWidth < 960 if (isMobile.value) { @@ -66,20 +74,37 @@ onBeforeUnmount(() => { - + - - - + > + + + + + + + + + + + + @@ -191,6 +216,16 @@ onBeforeUnmount(() => { color: #0071e3; } +.admin-menu :deep(.el-sub-menu__title) { + border-radius: 12px; + color: var(--xboard-text-secondary); + height: 44px; +} + +.admin-menu :deep(.el-sub-menu .el-menu-item) { + margin-left: 8px; +} + .admin-stage { background: #f5f5f7; } diff --git a/admin-frontend/src/router/index.ts b/admin-frontend/src/router/index.ts index ee4088a..b0859d1 100644 --- a/admin-frontend/src/router/index.ts +++ b/admin-frontend/src/router/index.ts @@ -23,6 +23,18 @@ const routes: RouteRecordRaw[] = [ component: () => import('@/views/dashboard/DashboardView.vue'), meta: { title: '仪表盘', kicker: 'Overview' }, }, + { + path: 'users', + name: 'Users', + component: () => import('@/views/users/UsersView.vue'), + meta: { title: '用户管理', kicker: 'Users' }, + }, + { + path: 'tickets', + name: 'Tickets', + component: () => import('@/views/tickets/TicketsView.vue'), + meta: { title: '工单管理', kicker: 'Tickets' }, + }, ], }, ] diff --git a/admin-frontend/src/types/api.d.ts b/admin-frontend/src/types/api.d.ts index f24fdc0..a93bfcd 100644 --- a/admin-frontend/src/types/api.d.ts +++ b/admin-frontend/src/types/api.d.ts @@ -116,6 +116,111 @@ export interface QueueStats { wait?: QueueWaitEntry[] } +export interface AdminPaginationResult { + data: T[] + total: number +} + +export interface AdminGroupOption { + id: number + name: string +} + +export interface AdminPlanOption { + id: number + name: string + sort?: number + transfer_enable?: number | null + group_id?: number | null + users_count?: number + active_users_count?: number + group?: AdminGroupOption | null +} + +export interface AdminUserRef { + id: number + email: string +} + +export interface AdminUserListItem { + id: number + email: string + token: string + uuid: string + plan_id: number | null + group_id: number | null + transfer_enable: number + u: number + d: number + total_used: number + expired_at: number | null + balance: number + commission_balance: number + commission_rate: number | null + commission_type: number | null + discount: number | null + speed_limit: number | null + device_limit: number | null + remarks: string | null + banned: boolean + is_admin: boolean + is_staff: boolean + created_at: number + updated_at: number + subscribe_url: string + plan?: AdminPlanOption | null + group?: AdminGroupOption | null + invite_user?: AdminUserRef | null +} + +export interface AdminUserFilter { + id: string + value: string | number | boolean | Array + logic?: 'and' | 'or' +} + +export interface AdminUserSort { + id: string + desc: boolean +} + +export interface AdminUserFetchParams { + current: number + pageSize: number + filter?: AdminUserFilter[] + sort?: AdminUserSort[] +} + +export interface AdminUserGeneratePayload { + email: string + password: string + plan_id?: number | null + expired_at?: number | null +} + +export interface AdminUserUpdatePayload { + id: number + email?: string + password?: string + transfer_enable?: number + expired_at?: number | null + banned?: boolean | number + plan_id?: number | null + commission_rate?: number | null + discount?: number | null + is_admin?: boolean + is_staff?: boolean + u?: number + d?: number + balance?: number + commission_type?: number | null + commission_balance?: number + remarks?: string | null + speed_limit?: number | null + device_limit?: number | null + invite_user_email?: string | null +} + declare global { interface Window { settings?: { diff --git a/admin-frontend/src/types/components.d.ts b/admin-frontend/src/types/components.d.ts index ad9dbec..e0dc6a8 100644 --- a/admin-frontend/src/types/components.d.ts +++ b/admin-frontend/src/types/components.d.ts @@ -16,15 +16,33 @@ declare module 'vue' { ElButton: typeof import('element-plus/es')['ElButton'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] ElContainer: typeof import('element-plus/es')['ElContainer'] + ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] + ElDrawer: typeof import('element-plus/es')['ElDrawer'] + ElDropdown: typeof import('element-plus/es')['ElDropdown'] + ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] + ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] ElForm: typeof import('element-plus/es')['ElForm'] ElFormItem: typeof import('element-plus/es')['ElFormItem'] ElHeader: typeof import('element-plus/es')['ElHeader'] ElIcon: typeof import('element-plus/es')['ElIcon'] ElInput: typeof import('element-plus/es')['ElInput'] + ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] ElMain: typeof import('element-plus/es')['ElMain'] ElMenu: typeof import('element-plus/es')['ElMenu'] ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] + ElOption: typeof import('element-plus/es')['ElOption'] + ElPagination: typeof import('element-plus/es')['ElPagination'] + ElProgress: typeof import('element-plus/es')['ElProgress'] + ElSelect: typeof import('element-plus/es')['ElSelect'] + ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] + ElSwitch: typeof import('element-plus/es')['ElSwitch'] + ElTable: typeof import('element-plus/es')['ElTable'] + ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] + ElTag: typeof import('element-plus/es')['ElTag'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] } + export interface GlobalDirectives { + vLoading: typeof import('element-plus/es')['ElLoadingDirective'] + } } diff --git a/admin-frontend/src/utils/users.ts b/admin-frontend/src/utils/users.ts new file mode 100644 index 0000000..8bf1b72 --- /dev/null +++ b/admin-frontend/src/utils/users.ts @@ -0,0 +1,207 @@ +import type { AdminUserListItem, AdminUserUpdatePayload } from '@/types/api' + +export interface UserStatusMeta { + label: string + type: 'success' | 'danger' | 'warning' | 'info' +} + +export interface UserFormModel { + id?: number + email: string + password: string + uploadGb: number | null + downloadGb: number | null + totalTrafficGb: number | null + expiredAt: number | null + planId: number | null + banned: boolean + commissionType: number | null + commissionRate: number | null + discount: number | null + speedLimit: number | null + deviceLimit: number | null + balance: number | null + commissionBalance: number | null + inviteUserEmail: string + isAdmin: boolean + isStaff: boolean + remarks: string +} + +export const COMMISSION_TYPE_OPTIONS = [ + { label: '跟随系统', value: 0 }, + { label: '周期返佣', value: 1 }, + { label: '一次性返佣', value: 2 }, +] as const + +const GIGABYTE = 1024 ** 3 + +function toNumber(value: unknown): number { + const numeric = Number(value) + return Number.isFinite(numeric) ? numeric : 0 +} + +export function bytesToGigabytes(value: unknown): number | null { + const numeric = toNumber(value) + if (numeric <= 0) { + return null + } + + return Number((numeric / GIGABYTE).toFixed(2)) +} + +export function gigabytesToBytes(value: number | null | undefined): number { + const numeric = Number(value) + if (!Number.isFinite(numeric) || numeric <= 0) { + return 0 + } + + return Math.round(numeric * GIGABYTE) +} + +export function normalizeTimestampSeconds(value: number | string | null | undefined): number | null { + if (value === null || value === undefined || value === '') { + return null + } + + const numeric = Number(value) + return Number.isFinite(numeric) && numeric > 0 ? Math.floor(numeric) : null +} + +export function splitEmailAddress(email: string): { prefix: string; suffix: string } | null { + const normalized = email.trim() + const atIndex = normalized.lastIndexOf('@') + if (atIndex <= 0 || atIndex === normalized.length - 1) { + return null + } + + return { + prefix: normalized.slice(0, atIndex), + suffix: normalized.slice(atIndex + 1), + } +} + +export function getUserUsagePercent(user: Pick): number { + const total = toNumber(user.transfer_enable) + if (total <= 0) { + return 0 + } + + return Math.min(100, Number(((toNumber(user.total_used) / total) * 100).toFixed(1))) +} + +export function getUserStatusMeta(user: Pick): UserStatusMeta { + if (user.banned) { + return { label: '封禁', type: 'danger' } + } + + if (!user.plan_id) { + return { label: '未订阅', type: 'info' } + } + + if (user.expired_at && user.expired_at < Math.floor(Date.now() / 1000)) { + return { label: '已到期', type: 'warning' } + } + + return { label: '正常', type: 'success' } +} + +export function createEmptyUserForm(): UserFormModel { + return { + email: '', + password: '', + uploadGb: null, + downloadGb: null, + totalTrafficGb: null, + expiredAt: null, + planId: null, + banned: false, + commissionType: 0, + commissionRate: null, + discount: null, + speedLimit: null, + deviceLimit: null, + balance: null, + commissionBalance: null, + inviteUserEmail: '', + isAdmin: false, + isStaff: false, + remarks: '', + } +} + +export function toUserFormModel(user?: AdminUserListItem | null): UserFormModel { + if (!user) { + return createEmptyUserForm() + } + + return { + id: user.id, + email: user.email, + password: '', + uploadGb: bytesToGigabytes(user.u), + downloadGb: bytesToGigabytes(user.d), + totalTrafficGb: bytesToGigabytes(user.transfer_enable), + expiredAt: normalizeTimestampSeconds(user.expired_at), + planId: user.plan_id, + banned: Boolean(user.banned), + commissionType: user.commission_type ?? 0, + commissionRate: user.commission_rate ?? null, + discount: user.discount ?? null, + speedLimit: user.speed_limit ?? null, + deviceLimit: user.device_limit ?? null, + balance: user.balance ?? null, + commissionBalance: user.commission_balance ?? null, + inviteUserEmail: user.invite_user?.email ?? '', + isAdmin: Boolean(user.is_admin), + isStaff: Boolean(user.is_staff), + remarks: user.remarks ?? '', + } +} + +export function toUserUpdatePayload(form: UserFormModel): AdminUserUpdatePayload { + return { + id: Number(form.id), + email: form.email.trim(), + password: form.password.trim() || undefined, + u: gigabytesToBytes(form.uploadGb), + d: gigabytesToBytes(form.downloadGb), + transfer_enable: gigabytesToBytes(form.totalTrafficGb), + expired_at: normalizeTimestampSeconds(form.expiredAt), + plan_id: form.planId, + banned: form.banned, + commission_type: form.commissionType, + commission_rate: form.commissionRate, + discount: form.discount, + speed_limit: form.speedLimit, + device_limit: form.deviceLimit, + balance: form.balance ?? 0, + commission_balance: form.commissionBalance ?? 0, + invite_user_email: form.inviteUserEmail.trim() || null, + is_admin: form.isAdmin, + is_staff: form.isStaff, + remarks: form.remarks.trim() || null, + } +} + +export function buildUserFilters(keyword: string, status: string, planId: string): Array<{ id: string; value: string | number[] }> { + const filters: Array<{ id: string; value: string | number[] }> = [] + + if (keyword.trim()) { + filters.push({ id: 'email', value: keyword.trim() }) + } + + if (status === 'active') { + filters.push({ id: 'banned', value: [0] }) + } + + if (status === 'banned') { + filters.push({ id: 'banned', value: [1] }) + } + + if (planId && planId !== 'all') { + filters.push({ id: 'plan_id', value: [Number(planId)] }) + } + + return filters +} diff --git a/admin-frontend/src/views/tickets/TicketsView.vue b/admin-frontend/src/views/tickets/TicketsView.vue new file mode 100644 index 0000000..96615f8 --- /dev/null +++ b/admin-frontend/src/views/tickets/TicketsView.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/admin-frontend/src/views/users/UserFormDrawer.vue b/admin-frontend/src/views/users/UserFormDrawer.vue new file mode 100644 index 0000000..5fb6944 --- /dev/null +++ b/admin-frontend/src/views/users/UserFormDrawer.vue @@ -0,0 +1,322 @@ + + + + + diff --git a/admin-frontend/src/views/users/UsersView.vue b/admin-frontend/src/views/users/UsersView.vue new file mode 100644 index 0000000..feab564 --- /dev/null +++ b/admin-frontend/src/views/users/UsersView.vue @@ -0,0 +1,478 @@ + + + + +