feat(ui): 为工单对话页新增用户与订单跳转入口
在工单工作台对话页为当前工单用户增加“查看用户” 和“用户订单”入口,支持直接跳转到用户管理与订单管理 用户管理页新增 `user_id/user_email` 路由作用域, 进入后按用户 ID 精准筛选,并支持在重置筛选时清除 该作用域 同步更新 admin-frontend 模块文档、变更归档与测试环境 compose 配置
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
# CHANGELOG
|
||||
|
||||
## [0.6.24] - 2026-05-01
|
||||
|
||||
### 新增
|
||||
- **[admin-frontend]**: 为工单工作台对话页新增“查看用户 / 用户订单”跳转入口;当前工单用户可直接进入用户管理并按 `user_id` 精准筛选,或进入订单管理查看该用户订单 — by yinjianm
|
||||
- 方案: [202605011828_admin-ticket-user-order-links](archive/2026-05/202605011828_admin-ticket-user-order-links/)
|
||||
- 决策: admin-ticket-user-order-links#D001(使用路由 query 承载跨页面用户作用域)
|
||||
|
||||
## [0.6.23] - 2026-04-29
|
||||
|
||||
### 新增
|
||||
|
||||
@@ -14,26 +14,27 @@
|
||||
## 1. 需求
|
||||
|
||||
### 背景
|
||||
当前用户前端由 `routes/web.php` 的 `/` 入口渲染,用户侧接口分布在 `/api/v1/passport/*`、`/api/v1/user/*`、`/api/v1/client/*`、`/api/v2/client/*` 与部分 `/api/v1/guest/*`。节点通信接口位于 `/api/v1/server/*`,`mi-node` 需要保留这些 API。用户希望部署后默认可隐藏用户前端和用户相关 API,降低公网直接访问 `IP:端口` 时暴露用户站点特征的概率,并可在后台手动开启。
|
||||
当前用户前端由 `routes/web.php` 的 `/` 入口渲染,访问公网 `IP:端口/` 会直接返回主题 HTML,其中包含站点标题、描述、Logo 与主题资源路径。节点通信接口位于 `/api/v1/server/*`,`mi-node` 需要保留这些 API;订阅入口和用户 API 也需要保持原有访问边界。用户希望关闭开关时只隐藏默认首页渲染出的前端网页,避免通过 `/` 直接暴露站点特征,并可在后台手动开启。
|
||||
|
||||
### 目标
|
||||
- 新增后台可保存的 `frontend_enable` 开关,默认开启以保持升级兼容。
|
||||
- 开关关闭时,用户前端首页、用户订阅入口、登录注册等用户侧 API 返回 404。
|
||||
- 节点 API 白名单不受影响,`/api/v1/server/*` 继续可用。
|
||||
- 开关关闭时,仅用户前端首页 `/` 返回空 404,不输出站点标题、描述、Logo、主题脚本或 `window.settings`。
|
||||
- 订阅入口、用户 API、客户端 API、Guest API 与节点 API 不受该开关影响,继续保留原有鉴权和响应边界。
|
||||
- 管理后台路由与管理 API 不纳入本次变更范围。
|
||||
|
||||
### 约束条件
|
||||
```yaml
|
||||
兼容性约束: 默认值必须为开启,避免升级后自动关闭现有站点。
|
||||
业务约束: 节点通信接口不可被误拦截;管理后台不纳入处理范围。
|
||||
业务约束: 节点通信接口、订阅入口和用户 API 不可被误拦截;管理后台不纳入处理范围。
|
||||
实现约束: 用户主题源码不在仓内,隐藏用户前端优先在 Laravel 路由和中间件层完成。
|
||||
安全约束: 关闭时使用 404 隐藏响应,不输出开关状态或产品识别信息。
|
||||
```
|
||||
|
||||
### 验收标准
|
||||
- [ ] `frontend_enable` 默认开启时,用户前端、用户 API、订阅入口保持原有路由行为。
|
||||
- [ ] `frontend_enable` 关闭时,`/`、`/{subscribe_path}/{token}`、用户登录注册、用户端 API 返回 404。
|
||||
- [ ] `frontend_enable` 关闭时,`/api/v1/server/*` 节点 API 仍进入原有 `server` 中间件和控制器链路。
|
||||
- [ ] `frontend_enable` 关闭时,只有 `/` 返回空 404,不渲染用户主题 HTML,不暴露 `<title>`、`window.settings` 或主题资源。
|
||||
- [ ] `frontend_enable` 关闭时,`/{subscribe_path}/{token}`、用户登录注册、用户端 API、Guest API 与客户端 API 保持原有路由行为。
|
||||
- [ ] `frontend_enable` 关闭时,`/api/v1/server/*` 与 `/api/v2/server/*` 节点 API 仍进入原有中间件和控制器链路。
|
||||
- [ ] 后台系统配置页可以读取、切换并保存该开关。
|
||||
- [ ] 自动化测试覆盖关闭/开启两种状态下的核心路由边界。
|
||||
|
||||
@@ -42,13 +43,13 @@
|
||||
## 2. 方案
|
||||
|
||||
### 技术方案
|
||||
新增 `EnsureUserFrontendEnabled` 路由中间件,读取 `admin_setting('frontend_enable', 1)`。当开关关闭时返回空 404;开启时放行。将该中间件挂到用户前端入口、订阅入口和用户侧 API 路由;节点 API 和管理端路由不挂载。后台配置接口在 `site` 配置组返回并保存 `frontend_enable`,管理端系统配置页在站点设置中显示该开关。
|
||||
新增 `EnsureUserFrontendEnabled` 路由中间件,读取 `admin_setting('frontend_enable', 1)`。当开关关闭时返回空 404;开启时放行。该中间件只挂到用户前端首页 `/`;订阅入口、用户侧 API、节点 API 和管理端路由不挂载。后台配置接口在 `site` 配置组返回并保存 `frontend_enable`,管理端系统配置页在站点设置中显示该开关。
|
||||
|
||||
### 影响范围
|
||||
```yaml
|
||||
涉及模块:
|
||||
- Laravel Web 路由: 控制用户主题首页和订阅入口隐藏行为。
|
||||
- Laravel API 路由: 控制用户登录注册、用户端、客户端和部分公开用户接口隐藏行为。
|
||||
- Laravel Web 路由: 控制用户主题首页隐藏行为。
|
||||
- Laravel API 路由: 保留原有边界,测试确保未挂载用户前端开关。
|
||||
- 管理端系统配置: 暴露可保存的用户前端开关。
|
||||
- 测试: 增加路由边界 feature 测试。
|
||||
预计变更文件: 8-10
|
||||
@@ -58,16 +59,16 @@
|
||||
| 风险 | 等级 | 应对 |
|
||||
|------|------|------|
|
||||
| 误拦截节点 API 导致 `mi-node` 无法同步 | 高 | 不在 `ServerRoute` 上挂载新中间件,并增加节点 API 不返回 404 的测试 |
|
||||
| 关闭用户前端后现有订阅链接不可用 | 中 | 这是隐藏用户侧入口的预期行为;默认开启保持兼容 |
|
||||
| 公开回调接口被误关导致支付/Telegram 回调异常 | 中 | 只拦截 `guest/plan` 与 `guest/comm` 这类用户展示接口,保留 webhook/notify |
|
||||
| 关闭用户前端后现有订阅链接不可用 | 高 | 不在订阅入口上挂载新中间件,关闭开关只影响 `/` |
|
||||
| 用户 API 被误关导致客户端或登录流程异常 | 高 | 不在 API 路由类上挂载新中间件,并用路由中间件断言覆盖关键入口 |
|
||||
| 配置值布尔转换不一致 | 低 | 中间件用 `filter_var(..., FILTER_VALIDATE_BOOLEAN)` 统一识别 `0/1/true/false` |
|
||||
|
||||
### 方案取舍
|
||||
```yaml
|
||||
唯一方案理由: 路由中间件能在 Laravel 入口统一隐藏用户前端和用户 API,改动范围清晰,不依赖用户主题源码,也不影响节点/后台路由。
|
||||
唯一方案理由: 路由中间件能在 Laravel 首页入口隐藏用户主题 HTML,改动范围清晰,不依赖用户主题源码,也不影响订阅、API、节点或后台路由。
|
||||
放弃的替代路径:
|
||||
- Nginx 路径白名单: 部署层可行但不支持后台开关,且每台服务器配置成本高。
|
||||
- 修改用户主题前端: 仓库内只有用户主题编译产物,无法可靠覆盖 API 暴露问题。
|
||||
- 修改用户主题前端: 仓库内只有用户主题编译产物,且请求 `/` 时仍会返回可识别 HTML 壳。
|
||||
- 全局 API 中间件路径判断: 容易误伤管理 API 和回调接口,边界不如路由级挂载明确。
|
||||
回滚边界: 移除新增中间件、路由挂载、配置字段和测试即可恢复原行为;数据库中残留 `frontend_enable` 设置不会影响旧代码。
|
||||
```
|
||||
@@ -81,8 +82,9 @@
|
||||
flowchart TD
|
||||
A[后台系统配置] --> B[frontend_enable 设置]
|
||||
B --> C[EnsureUserFrontendEnabled]
|
||||
C -->|开启| D[用户前端/API 原流程]
|
||||
C -->|开启| D[用户首页原流程]
|
||||
C -->|关闭| E[404]
|
||||
J[订阅/API] --> K[原有路由边界]
|
||||
F[/api/v1/server/*] --> G[server 中间件]
|
||||
H[管理后台/API] --> I[原有后台保护]
|
||||
```
|
||||
@@ -98,7 +100,7 @@ flowchart TD
|
||||
### 数据模型
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `v2_settings.frontend_enable` | string/bool | 用户前端与用户侧 API 开关,默认 `1` |
|
||||
| `v2_settings.frontend_enable` | string/bool | 用户前端首页开关,默认 `1` |
|
||||
|
||||
---
|
||||
|
||||
@@ -107,8 +109,14 @@ flowchart TD
|
||||
### 场景: 用户入口隐藏
|
||||
**模块**: Laravel Web/API 路由
|
||||
**条件**: `frontend_enable=false`
|
||||
**行为**: 访问 `/`、订阅入口、用户登录注册、用户端 API
|
||||
**结果**: 返回 404,不渲染用户主题,不暴露用户侧 API 响应结构。
|
||||
**行为**: 访问 `/`
|
||||
**结果**: 返回空 404,不渲染用户主题,不输出 `<title>`、`window.settings`、站点描述、Logo 或主题脚本。
|
||||
|
||||
### 场景: 订阅和用户 API 保留
|
||||
**模块**: Laravel Web/API 路由
|
||||
**条件**: `frontend_enable=false`
|
||||
**行为**: 访问订阅入口、用户登录注册、用户端 API、Guest API 与客户端 API
|
||||
**结果**: 按原有路由、中间件和鉴权逻辑响应,不被用户前端开关改写成 404。
|
||||
|
||||
### 场景: 节点接口保留
|
||||
**模块**: 节点 API
|
||||
@@ -129,16 +137,16 @@ flowchart TD
|
||||
### user-frontend-access-toggle#D001: 使用路由级中间件控制用户入口
|
||||
**日期**: 2026-04-29
|
||||
**状态**: ✅采纳
|
||||
**背景**: 需要在应用代码内提供后台可控的隐藏能力,同时避免影响节点 API 和后台 API。
|
||||
**背景**: 需要在应用代码内提供后台可控的首页隐藏能力,同时避免影响订阅、用户 API、节点 API 和后台 API。
|
||||
**选项分析**:
|
||||
| 选项 | 优点 | 缺点 |
|
||||
|------|------|------|
|
||||
| A: 路由级中间件 | 边界清晰、可测试、默认兼容、能避开节点和后台路由 | 需要在多个用户路由类中显式挂载 |
|
||||
| A: 路由级中间件 | 边界清晰、可测试、默认兼容、能避开订阅、API、节点和后台路由 | 只能隐藏被挂载的入口 |
|
||||
| B: Nginx 白名单 | 部署快、应用代码少 | 无后台开关,部署环境差异大 |
|
||||
| C: 全局 API 中间件 | 集中处理 | 动态后台路径、回调接口和节点路径容易误判 |
|
||||
**决策**: 选择方案 A
|
||||
**理由**: 当前需求重点是“后台手动开启”和“节点 API 白名单”,路由级中间件能精确表达边界,并能用 feature test 验证。
|
||||
**影响**: `routes/web.php`、V1/V2 用户路由、中间件注册、后台配置映射和管理端系统配置表单。
|
||||
**理由**: 当前需求重点是“后台手动开启”和“只隐藏首页 HTML”,路由级中间件能精确表达边界,并能用 feature test 验证。
|
||||
**影响**: `routes/web.php`、中间件注册、后台配置映射和管理端系统配置表单。
|
||||
|
||||
---
|
||||
|
||||
@@ -148,7 +156,7 @@ flowchart TD
|
||||
verifyMode: test-first
|
||||
reviewerFocus:
|
||||
- app/Http/Middleware/EnsureUserFrontendEnabled.php
|
||||
- app/Http/Routes/V1/*.php 与 app/Http/Routes/V2/ClientRoute.php 的挂载边界
|
||||
- app/Http/Routes/V1/*.php 与 app/Http/Routes/V2/ClientRoute.php 不挂载 `user.frontend` 的边界
|
||||
- routes/web.php 中用户入口与管理入口隔离
|
||||
testerFocus:
|
||||
- vendor/bin/phpunit tests/Feature/UserFrontendAccessToggleTest.php
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
## LIVE_STATUS
|
||||
|
||||
```json
|
||||
{"status":"completed","completed":6,"failed":0,"pending":0,"total":6,"percent":100,"current":"用户前端访问开关、路由拦截、后台配置与验证已完成","updated_at":"2026-04-29 16:16:00"}
|
||||
{"status":"completed","completed":6,"failed":0,"pending":0,"total":6,"percent":100,"current":"用户前端首页访问开关、路由边界、后台配置与验证已完成","updated_at":"2026-04-29 16:20:00"}
|
||||
```
|
||||
|
||||
## 进度概览
|
||||
@@ -28,7 +28,7 @@
|
||||
### 1. 后端访问控制
|
||||
|
||||
- [√] 1.1 新增 `app/Http/Middleware/EnsureUserFrontendEnabled.php`
|
||||
- 预期变更: 读取 `frontend_enable` 设置,关闭时对用户入口返回 404,开启时放行。
|
||||
- 预期变更: 读取 `frontend_enable` 设置,关闭时对用户首页 `/` 返回空 404,开启时放行。
|
||||
- 完成标准: 中间件能正确识别 `true/false/1/0` 等设置值。
|
||||
- 验证方式: `vendor/bin/phpunit tests/Feature/UserFrontendAccessToggleTest.php`
|
||||
- depends_on: []
|
||||
@@ -39,9 +39,9 @@
|
||||
- 验证方式: `php artisan route:list` 不报中间件解析错误。
|
||||
- depends_on: [1.1]
|
||||
|
||||
- [√] 1.3 修改 `routes/web.php` 与用户侧 API 路由类
|
||||
- 预期变更: 用户首页、订阅入口、V1 Passport/User/Client、V2 Client、V1 Guest 的公开展示接口挂载 `user.frontend`;节点 API 与后台 API 不挂载。
|
||||
- 完成标准: `frontend_enable=false` 时用户侧入口返回 404,`/api/v1/server/*` 不被该中间件拦截。
|
||||
- [√] 1.3 修改 `routes/web.php` 并复核用户侧 API 路由类
|
||||
- 预期变更: 仅用户首页 `/` 挂载 `user.frontend`;订阅入口、V1 Passport/User/Client、V2 User/Client、V1 Guest、节点 API 与后台 API 不挂载。
|
||||
- 完成标准: `frontend_enable=false` 时 `/` 返回空 404,订阅/API/节点接口不被该中间件拦截。
|
||||
- 验证方式: `vendor/bin/phpunit tests/Feature/UserFrontendAccessToggleTest.php`
|
||||
- depends_on: [1.2]
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
### 3. 验证与同步
|
||||
|
||||
- [√] 3.1 新增并执行验证
|
||||
- 预期变更: 增加 `tests/Feature/UserFrontendAccessToggleTest.php`,覆盖默认开启、关闭隐藏、节点 API 不被隐藏。
|
||||
- 预期变更: 增加 `tests/Feature/UserFrontendAccessToggleTest.php`,覆盖默认开启、关闭隐藏首页、用户 API/订阅/节点 API 不被隐藏。
|
||||
- 完成标准: 相关 PHPUnit 测试通过;管理端构建通过或明确记录阻断原因。
|
||||
- 验证方式: `vendor/bin/phpunit tests/Feature/UserFrontendAccessToggleTest.php`、`npm --prefix admin-frontend run build`
|
||||
- depends_on: [1.3, 2.2]
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
# 变更提案: admin-ticket-user-order-links
|
||||
|
||||
## 元信息
|
||||
```yaml
|
||||
类型: 新功能
|
||||
方案类型: implementation
|
||||
优先级: P1
|
||||
状态: 已确认
|
||||
创建: 2026-05-01
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. 需求
|
||||
|
||||
### 背景
|
||||
管理端工单对话页已经能查看工单用户邮箱、流量日志和会话记录,但排查用户问题时还需要手动切到用户管理或订单管理,再按用户信息重新筛选。用户要求在工单管理的对话页面直接跳转到对应用户,以及该用户的订单列表。
|
||||
|
||||
### 目标
|
||||
- 在工单对话页为当前工单用户增加“查看用户”和“用户订单”入口。
|
||||
- “查看用户”跳转到用户管理页并精准定位当前用户。
|
||||
- “用户订单”跳转到订单管理页,并复用订单页已有 `user_id/user_email` 作用域筛选。
|
||||
- 不改后端接口,不引入新页面,不改变已有流量日志、回复、关闭工单行为。
|
||||
|
||||
### 约束条件
|
||||
```yaml
|
||||
时间约束: 无
|
||||
性能约束: 仅增加路由跳转和本地 computed,不增加额外接口请求
|
||||
兼容性约束: 复用 Vue Router hash 路由和 Element Plus 按钮风格
|
||||
业务约束: 后端返回的 AdminTicketDetail.user 为跳转真相源;缺少 user.id 时不展示跳转入口
|
||||
```
|
||||
|
||||
### 验收标准
|
||||
- [ ] 工单对话页当前工单存在用户 ID 时显示“查看用户”和“用户订单”入口。
|
||||
- [ ] 点击“查看用户”进入 `Users` 路由,用户列表按当前用户 ID 精准筛选,并显示清晰的已生效筛选摘要。
|
||||
- [ ] 点击“用户订单”进入 `SubscriptionOrders` 路由,订单页按当前用户 ID 精准筛选,并保留用户邮箱提示。
|
||||
- [ ] `admin-frontend` 构建或类型检查通过;若失败,必须确认失败是否由本次改动引入。
|
||||
|
||||
---
|
||||
|
||||
## 2. 方案
|
||||
|
||||
### 技术方案
|
||||
在 `TicketWorkspaceDialog.vue` 内引入 `useRouter` 和合适的 Element Plus 图标,为当前 `detail.user` 构造两个路由跳转方法:
|
||||
- `Users`:携带 `user_id` 和 `user_email`。
|
||||
- `SubscriptionOrders`:携带 `user_id` 和 `user_email`。
|
||||
|
||||
在 `useUserScopedActions.ts` 中补齐用户管理页的本人作用域:
|
||||
- 读取 `route.query.user_id/user_email`。
|
||||
- 生成 `AdminUserFilter`:`{ id: 'id', value: 'eq:{user_id}' }`。
|
||||
- 在筛选摘要中显示“用户:邮箱或 #ID”。
|
||||
- `clearScopedUserQuery()` 清除本人作用域。
|
||||
|
||||
在 `useUsersManagement.ts` 中把本人作用域合并到 `appliedFilters` 与 `appliedFilterSummaries`,并让重置筛选、路由监听同时处理 `user_id/user_email` 与既有 `invite_user_id/invite_user_email`。
|
||||
|
||||
### 影响范围
|
||||
```yaml
|
||||
涉及模块:
|
||||
- admin-frontend 工单工作台: 新增跨页面跳转入口
|
||||
- admin-frontend 用户管理: 新增 user_id/user_email 本人作用域筛选
|
||||
- admin-frontend 订单管理: 复用已有 user_id/user_email 订单作用域筛选,无需改动
|
||||
预计变更文件: 3
|
||||
```
|
||||
|
||||
### 风险评估
|
||||
| 风险 | 等级 | 应对 |
|
||||
|------|------|------|
|
||||
| 用户页原有邀请人作用域与新增本人作用域互相污染 | 中 | 分别维护 `scopedUser*` 和 `scopedInvite*`,清理函数显式删除各自 query |
|
||||
| 工单用户字段缺失导致跳转参数为空 | 低 | 入口仅在 `detail.user.id` 存在时展示,邮箱作为可选提示 |
|
||||
| 构建失败来自历史遗留问题 | 中 | 运行 `npm run build` 并记录具体失败归因 |
|
||||
|
||||
### 方案取舍
|
||||
```yaml
|
||||
唯一方案理由: 复用现有路由和列表筛选机制,改动最小,能保持用户/订单页各自的筛选真相源。
|
||||
放弃的替代路径:
|
||||
- 在工单页内嵌用户/订单抽屉: 会复制用户和订单工作台逻辑,增加维护成本。
|
||||
- 新增后端接口返回用户订单摘要: 当前需求是跳转到订单管理,不需要扩展 API。
|
||||
- 仅按邮箱关键字跳用户页: 精准度低,邮箱变化或模糊匹配时容易误命中。
|
||||
回滚边界: 回退 TicketWorkspaceDialog.vue、useUserScopedActions.ts、useUsersManagement.ts 的本次改动即可恢复原行为。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 技术设计
|
||||
|
||||
### 路由参数
|
||||
```yaml
|
||||
Users:
|
||||
name: Users
|
||||
query:
|
||||
user_id: 当前工单 user.id
|
||||
user_email: 当前工单 user.email
|
||||
SubscriptionOrders:
|
||||
name: SubscriptionOrders
|
||||
query:
|
||||
user_id: 当前工单 user.id
|
||||
user_email: 当前工单 user.email
|
||||
```
|
||||
|
||||
### 用户页筛选映射
|
||||
| query | AdminUserFilter | 摘要 |
|
||||
|------|------------------|------|
|
||||
| `user_id=123&user_email=a@b.com` | `{ id: 'id', value: 'eq:123' }` | `用户:a@b.com` |
|
||||
| `invite_user_id=123&invite_user_email=a@b.com` | `{ id: 'invite_user_id', value: 'eq:123' }` | `邀请人:a@b.com` |
|
||||
|
||||
---
|
||||
|
||||
## 4. 核心场景
|
||||
|
||||
### 场景: 工单对话跳转到用户与订单
|
||||
**模块**: admin-frontend
|
||||
**条件**: 管理员打开工单管理对话页,当前工单详情包含 `user.id`。
|
||||
**行为**: 管理员点击“查看用户”或“用户订单”。
|
||||
**结果**: 前者进入用户管理并精准筛选该用户,后者进入订单管理并展示该用户订单。
|
||||
|
||||
---
|
||||
|
||||
## 5. 技术决策
|
||||
|
||||
### admin-ticket-user-order-links#D001: 使用路由 query 承载跨页面用户作用域
|
||||
**日期**: 2026-05-01
|
||||
**状态**: 采纳
|
||||
**背景**: 工单、用户、订单三处均为管理端前端页面,订单页已经支持 `user_id/user_email` query 过滤,用户页只缺本人作用域。
|
||||
**选项分析**:
|
||||
| 选项 | 优点 | 缺点 |
|
||||
|------|------|------|
|
||||
| A: 复用路由 query | 与现有订单页一致,可刷新/分享,改动小 | 需要在用户页补齐 query 监听和清理 |
|
||||
| B: 全局 store 临时传参 | 不污染 URL | 刷新丢失状态,不利于定位 |
|
||||
| C: 工单页内嵌详情 | 一页完成更多操作 | 重复用户/订单页面逻辑,范围扩大 |
|
||||
**决策**: 选择方案 A。
|
||||
**理由**: 当前项目已有相同模式,URL 可见、可恢复,且不需要新增后端或全局状态。
|
||||
**影响**: 用户管理页新增 `user_id/user_email` 作用域,订单页沿用既有行为。
|
||||
|
||||
---
|
||||
|
||||
## 6. 验证策略
|
||||
|
||||
```yaml
|
||||
verifyMode: review-first
|
||||
reviewerFocus:
|
||||
- admin-frontend/src/views/tickets/TicketWorkspaceDialog.vue
|
||||
- admin-frontend/src/views/users/useUserScopedActions.ts
|
||||
- admin-frontend/src/views/users/useUsersManagement.ts
|
||||
testerFocus:
|
||||
- npm run build
|
||||
- 静态检查工单跳转目标 name 与 router/index.ts 一致
|
||||
- 静态检查用户页 user_id 筛选与订单页 user_id 筛选参数一致
|
||||
uiValidation: optional
|
||||
riskBoundary:
|
||||
- 不改 Laravel 后端接口
|
||||
- 不改订单页筛选契约
|
||||
- 不执行生产部署或推送
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 成果设计
|
||||
|
||||
### 设计方向
|
||||
- **美学基调**: 延续当前 Apple-style 管理后台的克制工具栏风格,新增入口以轻量文字按钮出现在对话页头部操作区。
|
||||
- **记忆点**: 工单头部形成“用户排查三联入口”:查看用户、用户订单、流量日志。
|
||||
- **参考**: 现有 `ghost-action` 按钮和 Element Plus 图标。
|
||||
|
||||
### 视觉要素
|
||||
- **配色**: 沿用 `var(--xboard-link)` 与现有危险操作红色,不增加新色。
|
||||
- **字体**: 沿用项目系统字体栈,保持管理端一致性。
|
||||
- **布局**: 头部右侧按钮横向排列,移动端沿现有 header 响应式折行。
|
||||
- **动效**: 复用现有按钮 hover/focus 反馈,不新增装饰性动效。
|
||||
- **氛围**: 不新增卡片或背景装饰,保持工单工作台信息密度。
|
||||
|
||||
### 技术约束
|
||||
- **可访问性**: 使用真实 `ElButton`,按钮文本明确。
|
||||
- **响应式**: 依赖现有 `.workspace-header__actions` flex wrap 行为,必要时补充 wrap。
|
||||
@@ -0,0 +1,73 @@
|
||||
# 任务清单: admin-ticket-user-order-links
|
||||
|
||||
> **@status:** completed | 2026-05-01 18:33
|
||||
|
||||
```yaml
|
||||
@feature: admin-ticket-user-order-links
|
||||
@created: 2026-05-01
|
||||
@status: completed
|
||||
@mode: R2
|
||||
```
|
||||
|
||||
## LIVE_STATUS
|
||||
|
||||
```json
|
||||
{"status":"completed","completed":4,"failed":0,"pending":0,"total":4,"percent":100,"current":"代码实现、构建验证和知识库同步已完成","updated_at":"2026-05-01 18:40:00"}
|
||||
```
|
||||
|
||||
## 进度概览
|
||||
|
||||
| 完成 | 失败 | 跳过 | 总数 |
|
||||
|------|------|------|------|
|
||||
| 4 | 0 | 0 | 4 |
|
||||
|
||||
---
|
||||
|
||||
## 任务列表
|
||||
|
||||
### 1. 工单对话页跳转入口
|
||||
|
||||
- [√] 1.1 修改 `admin-frontend/src/views/tickets/TicketWorkspaceDialog.vue`
|
||||
- 预期变更: 引入路由能力和图标,在对话页头部为当前工单用户增加“查看用户”和“用户订单”按钮。
|
||||
- 完成标准: 当前 `detail.user.id` 存在时两个按钮可见,点击后分别跳转到 `Users` 与 `SubscriptionOrders`,并携带 `user_id/user_email`。
|
||||
- 验证方式: 静态检查 route name 与 `router/index.ts` 一致;构建验证。
|
||||
- depends_on: []
|
||||
|
||||
### 2. 用户管理本人作用域
|
||||
|
||||
- [√] 2.1 修改 `admin-frontend/src/views/users/useUserScopedActions.ts`
|
||||
- 预期变更: 新增 `user_id/user_email` query 读取、本人精准筛选、摘要和清理函数,保留现有邀请人作用域。
|
||||
- 完成标准: `user_id` 会转换为 `{ id: 'id', value: 'eq:{id}' }`,`user_email` 仅用于显示摘要。
|
||||
- 验证方式: 静态检查 computed 输出和清理函数;构建验证。
|
||||
- depends_on: [1.1]
|
||||
|
||||
- [√] 2.2 修改 `admin-frontend/src/views/users/useUsersManagement.ts`
|
||||
- 预期变更: 将本人作用域合并进 `appliedFilters/appliedFilterSummaries`,重置筛选和路由监听覆盖 `user_id/user_email`。
|
||||
- 完成标准: 从工单跳转到用户页后会按用户 ID 精准筛选,重置筛选会清除本人和邀请人作用域。
|
||||
- 验证方式: 静态检查 fetchUsers 参数来源;构建验证。
|
||||
- depends_on: [2.1]
|
||||
|
||||
### 3. 验证与知识库同步
|
||||
|
||||
- [√] 3.1 执行前端验证并同步知识库
|
||||
- 预期变更: 运行 `npm run build`,根据结果更新 `.helloagents/modules/admin-frontend.md` 和 `CHANGELOG.md`。
|
||||
- 完成标准: 构建通过,或明确记录阻断原因及是否与本次改动相关;知识库反映新增工单跳转行为。
|
||||
- 验证方式: 命令输出、知识库 diff、自检本次修改文件。
|
||||
- depends_on: [1.1, 2.1, 2.2]
|
||||
|
||||
---
|
||||
|
||||
## 执行日志
|
||||
|
||||
| 时间 | 任务 | 状态 | 备注 |
|
||||
|------|------|------|------|
|
||||
| 2026-05-01 18:40 | DEVELOP | completed | npm run build 通过,知识库已同步 |
|
||||
| 2026-05-01 18:34 | DEVELOP | completed | 已完成 1.1、2.1、2.2 前端改动 |
|
||||
| 2026-05-01 18:28 | DESIGN | completed | 已创建方案包并填充规划 |
|
||||
|
||||
---
|
||||
|
||||
## 执行备注
|
||||
|
||||
- 订单页已有 `user_id/user_email` 作用域筛选,本次不修改订单页。
|
||||
- 本次不涉及后端接口和生产部署。
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
| 时间戳 | 名称 | 类型 | 涉及模块 | 决策 | 结果 |
|
||||
|--------|------|------|---------|------|------|
|
||||
| 202605011828 | admin-ticket-user-order-links | implementation | admin-frontend | admin-ticket-user-order-links#D001 | ✅完成 |
|
||||
| 202604291559 | user-frontend-access-toggle | implementation | user-frontend-access,admin-frontend | user-frontend-access-toggle#D001 | ✅完成 |
|
||||
| 202604290153 | parent-node-auto-visibility | - | - | - | ✅完成 |
|
||||
| 202604290132 | shared-node-traffic-limit | implementation | node-traffic-limit,admin-frontend | shared-node-traffic-limit#D001 | ✅完成 |
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
- API 基础路径使用 `/api/v2/{secure_path}`,其中 `secure_path` 来自运行时配置
|
||||
- 工单工作台现允许对已关闭工单继续回复;管理员发送新消息后会提示“发送并重开”,并通过统一后端语义把工单状态重新开启
|
||||
- 工单工作台回复区支持点击选择、拖拽放下和剪贴板粘贴三种图片上传入口,统一复用 `/upload/rest/upload` 图片上传和 Markdown 图片链接插入逻辑;上传期间会禁用发送入口,避免图片链接尚未写入时提前回复
|
||||
- 工单工作台对话页可从当前工单用户直接跳转到用户管理或订单管理;跳用户页时携带 `user_id/user_email` 并按用户 ID 精准筛选,跳订单页时复用订单页已有用户订单作用域
|
||||
- 仪表盘以真实后端接口返回值为准,不在前端伪造业务统计
|
||||
- 仪表盘“收入趋势”支持在同一张趋势图中切换“按金额 / 按数量”,数量模式同步切换摘要卡片、Y 轴标签与最近记录
|
||||
- 仪表盘“作业详情”支持打开失败作业报错弹窗,集中查看 Horizon 失败作业的报错摘要、失败时间与队列信息
|
||||
@@ -31,6 +32,7 @@
|
||||
- 工单页与订单页可读取 dashboard 来源查询参数:工单页支持 `focus=opening|closed|all`,订单页支持 `workbench=pending|commission`,并会显示低干扰入口提示
|
||||
- 用户管理页通过真实后端 `user/fetch`、`user/update`、`user/generate`、`user/dumpCSV`、`user/sendMail`、`user/ban`、`user/resetSecret`、`user/destroy` 与 `plan/fetch` 完成数据读写
|
||||
- 新增用户时采用“先 generate,后按邮箱回查并 update”的两段式流程,以兼容后端基础创建接口
|
||||
- 用户管理页可读取 `user_id/user_email` 路由查询并转换为 `{ id: 'id', value: 'eq:{user_id}' }` 精准筛选,筛选摘要显示目标用户并可通过重置筛选清除
|
||||
- 用户管理页现已补齐高级筛选弹窗,支持按邮箱、用户 ID、订阅、活跃状态、流量、已用流量、在线设备、到期时间、UUID、Token、账号状态和备注组合筛选;其中“活跃状态”按“有任意订阅 + 剩余流量大于 0 + 最后在线时间在半年内”为活跃规则
|
||||
- 用户管理页新增勾选 + 批量操作工作流,支持“发送邮件 / 导出 CSV / 批量封禁 / 恢复正常”,作用范围按“已勾选用户 > 当前筛选结果 > 全部用户”自动判定
|
||||
- 批量恢复正常沿用 `user/ban` 现有接口,通过 `banned=0|1` 兼容,不额外引入重复路由
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ChatLineRound, DataAnalysis, Picture, Search } from '@element-plus/icons-vue'
|
||||
import { ChatLineRound, DataAnalysis, Picture, Search, Tickets, User } from '@element-plus/icons-vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { closeTicket, fetchTickets, getTicketById, replyTicket } from '@/api/admin'
|
||||
import type { AdminTicketDetail, AdminTicketListItem } from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/dashboard'
|
||||
@@ -25,6 +26,7 @@ const emit = defineEmits<{
|
||||
updated: []
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
const loadingSidebar = ref(false)
|
||||
const loadingDetail = ref(false)
|
||||
const replying = ref(false)
|
||||
@@ -138,6 +140,34 @@ async function handleCloseTicket() {
|
||||
}
|
||||
}
|
||||
|
||||
async function openTicketUser() {
|
||||
if (!detail.value?.user?.id) {
|
||||
return
|
||||
}
|
||||
|
||||
await router.push({
|
||||
name: 'Users',
|
||||
query: {
|
||||
user_id: String(detail.value.user.id),
|
||||
user_email: detail.value.user.email,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function openTicketUserOrders() {
|
||||
if (!detail.value?.user?.id) {
|
||||
return
|
||||
}
|
||||
|
||||
await router.push({
|
||||
name: 'SubscriptionOrders',
|
||||
query: {
|
||||
user_id: String(detail.value.user.id),
|
||||
user_email: detail.value.user.email,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
resetReplyDragState()
|
||||
emit('update:visible', false)
|
||||
@@ -189,6 +219,24 @@ watch(
|
||||
</div>
|
||||
|
||||
<div class="workspace-header__actions">
|
||||
<ElButton
|
||||
v-if="detail?.user?.id"
|
||||
text
|
||||
class="ghost-action"
|
||||
@click="openTicketUser"
|
||||
>
|
||||
<ElIcon><User /></ElIcon>
|
||||
查看用户
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="detail?.user?.id"
|
||||
text
|
||||
class="ghost-action"
|
||||
@click="openTicketUserOrders"
|
||||
>
|
||||
<ElIcon><Tickets /></ElIcon>
|
||||
用户订单
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="detail?.user?.id"
|
||||
text
|
||||
|
||||
@@ -15,6 +15,19 @@ export function useUserScopedActions() {
|
||||
const trafficLogUserEmail = ref('')
|
||||
const resettingTrafficId = ref<number | null>(null)
|
||||
|
||||
const scopedUserId = computed(() => {
|
||||
const raw = route.query.user_id
|
||||
const value = Array.isArray(raw) ? raw[0] : raw
|
||||
const numeric = Number(value)
|
||||
return Number.isFinite(numeric) && numeric > 0 ? numeric : null
|
||||
})
|
||||
|
||||
const scopedUserEmail = computed(() => {
|
||||
const raw = route.query.user_email
|
||||
const value = Array.isArray(raw) ? raw[0] : raw
|
||||
return typeof value === 'string' ? value : ''
|
||||
})
|
||||
|
||||
const scopedInviteUserId = computed(() => {
|
||||
const raw = route.query.invite_user_id
|
||||
const value = Array.isArray(raw) ? raw[0] : raw
|
||||
@@ -34,6 +47,21 @@ export function useUserScopedActions() {
|
||||
: []
|
||||
))
|
||||
|
||||
const scopedUserFilters = computed<AdminUserFilter[]>(() => (
|
||||
scopedUserId.value
|
||||
? [{ id: 'id', value: `eq:${scopedUserId.value}` }]
|
||||
: []
|
||||
))
|
||||
|
||||
const scopedUserSummaries = computed(() => {
|
||||
if (!scopedUserId.value) {
|
||||
return []
|
||||
}
|
||||
|
||||
const label = scopedUserEmail.value || `用户 #${scopedUserId.value}`
|
||||
return [`用户:${label}`]
|
||||
})
|
||||
|
||||
const scopedInviteSummaries = computed(() => {
|
||||
if (!scopedInviteUserId.value) {
|
||||
return []
|
||||
@@ -43,6 +71,17 @@ export function useUserScopedActions() {
|
||||
return [`邀请人:${label}`]
|
||||
})
|
||||
|
||||
function clearScopedUserQuery() {
|
||||
if (!scopedUserId.value && !scopedUserEmail.value) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const nextQuery = { ...route.query }
|
||||
delete nextQuery.user_id
|
||||
delete nextQuery.user_email
|
||||
return router.replace({ name: 'Users', query: nextQuery })
|
||||
}
|
||||
|
||||
function clearScopedInviteQuery() {
|
||||
if (!scopedInviteUserId.value && !scopedInviteUserEmail.value) {
|
||||
return Promise.resolve()
|
||||
@@ -116,9 +155,13 @@ export function useUserScopedActions() {
|
||||
trafficLogUserId,
|
||||
trafficLogUserEmail,
|
||||
resettingTrafficId,
|
||||
scopedUserId,
|
||||
scopedUserFilters,
|
||||
scopedUserSummaries,
|
||||
scopedInviteUserId,
|
||||
scopedInviteFilters,
|
||||
scopedInviteSummaries,
|
||||
clearScopedUserQuery,
|
||||
clearScopedInviteQuery,
|
||||
openAssignOrder,
|
||||
handleAssignOrderSuccess,
|
||||
|
||||
@@ -66,6 +66,7 @@ export function useUsersManagement() {
|
||||
planFilter.value,
|
||||
advancedFilters.value,
|
||||
),
|
||||
...scopedActions.scopedUserFilters.value,
|
||||
...scopedActions.scopedInviteFilters.value,
|
||||
])
|
||||
|
||||
@@ -75,7 +76,10 @@ export function useUsersManagement() {
|
||||
planFilter.value,
|
||||
advancedFilters.value,
|
||||
plans.value,
|
||||
).concat(scopedActions.scopedInviteSummaries.value))
|
||||
).concat(
|
||||
scopedActions.scopedUserSummaries.value,
|
||||
scopedActions.scopedInviteSummaries.value,
|
||||
))
|
||||
|
||||
const batchActions = useUsersBatchActions({
|
||||
loading,
|
||||
@@ -148,9 +152,11 @@ export function useUsersManagement() {
|
||||
statusFilter.value = 'all'
|
||||
planFilter.value = 'all'
|
||||
advancedFilters.value = []
|
||||
void scopedActions.clearScopedInviteQuery().finally(() => {
|
||||
refreshUsers(true)
|
||||
})
|
||||
void scopedActions.clearScopedUserQuery()
|
||||
.then(() => scopedActions.clearScopedInviteQuery())
|
||||
.finally(() => {
|
||||
refreshUsers(true)
|
||||
})
|
||||
}
|
||||
|
||||
function clearAdvancedFilters() {
|
||||
@@ -280,6 +286,8 @@ export function useUsersManagement() {
|
||||
|
||||
watch(
|
||||
() => [
|
||||
scopedActions.route.query.user_id,
|
||||
scopedActions.route.query.user_email,
|
||||
scopedActions.route.query.invite_user_id,
|
||||
scopedActions.route.query.invite_user_email,
|
||||
],
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
services:
|
||||
user:
|
||||
image: ${VUE_XBOARD_THEME_MICAH_IMAGE:-ghcr.io/micah123321/vue-xboard-theme-micah:new}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
XBOARD_BACKEND_UPSTREAM: ${XBOARD_BACKEND_UPSTREAM:-http://web:7001}
|
||||
XBOARD_UPLOAD_UPSTREAM: ${XBOARD_UPLOAD_UPSTREAM:-https://pic.535888.xyz}
|
||||
ports:
|
||||
- "${ADMIN_PORT:-7003}:80"
|
||||
web:
|
||||
image: ${XBOARD_IMAGE:-ghcr.io/micah123321/xboard:new}
|
||||
restart: on-failure
|
||||
|
||||
Reference in New Issue
Block a user