diff --git a/.helloagents/CHANGELOG.md b/.helloagents/CHANGELOG.md index 1be9abd..faafde2 100644 --- a/.helloagents/CHANGELOG.md +++ b/.helloagents/CHANGELOG.md @@ -1,5 +1,19 @@ # CHANGELOG +## [0.5.19] - 2026-04-27 + +### 新增 +- **[admin-frontend]**: 为工单工作台回复区补齐图片拖拽上传与剪贴板粘贴上传,统一复用现有图片上传接口和 Markdown 图片插入逻辑,并将超大工单工作台组件拆分出上传 composable 与独立 SCSS 样式文件 — by yinjianm + - 方案: [202604272310_ticket-chat-image-dnd-paste-upload](archive/2026-04/202604272310_ticket-chat-image-dnd-paste-upload/) + - 决策: ticket-chat-image-dnd-paste-upload#D001(统一图片入口到现有 Markdown 上传链路) + +## [0.5.18] - 2026-04-27 + +### 快速修改 +- **[admin-frontend]**: 调整节点管理状态筛选口径,“在线节点”现在同时包含显式在线与待同步节点,顶部在线节点统计同步采用相同口径;“离线节点”仍只匹配显式离线节点 — by yinjianm + - 类型: 快速修改(无方案包) + - 文件: admin-frontend/src/utils/nodes.ts:12-176 + ## [0.5.17] - 2026-04-25 ### 修复 diff --git a/.helloagents/INDEX.md b/.helloagents/INDEX.md index 792586c..a289d0b 100644 --- a/.helloagents/INDEX.md +++ b/.helloagents/INDEX.md @@ -3,7 +3,7 @@ ```yaml kb_version: 2 project: Xboard-new -updated_at: 2026-04-25 +updated_at: 2026-04-27 active_package: 无 ``` @@ -11,7 +11,7 @@ active_package: 无 - 类型: PHP Laravel 主仓 + `admin-frontend` Vue3 管理端前端 - 当前重点模块: `admin-frontend`、`order-payment`、`subscription-protocols` -- 最新归档: `202604250002_order-payment-snapshot` +- 最新归档: `202604272310_ticket-chat-image-dnd-paste-upload` ## 活跃模块 diff --git a/.helloagents/archive/2026-04/202604272310_ticket-chat-image-dnd-paste-upload/.status.json b/.helloagents/archive/2026-04/202604272310_ticket-chat-image-dnd-paste-upload/.status.json new file mode 100644 index 0000000..597fe07 --- /dev/null +++ b/.helloagents/archive/2026-04/202604272310_ticket-chat-image-dnd-paste-upload/.status.json @@ -0,0 +1,10 @@ +{ + "status": "completed", + "completed": 4, + "failed": 0, + "pending": 0, + "total": 4, + "percent": 100, + "current": "工单聊天图片拖拽上传、粘贴上传与前端构建验证已完成", + "updated_at": "2026-04-27 23:18:00" +} diff --git a/.helloagents/archive/2026-04/202604272310_ticket-chat-image-dnd-paste-upload/proposal.md b/.helloagents/archive/2026-04/202604272310_ticket-chat-image-dnd-paste-upload/proposal.md new file mode 100644 index 0000000..c40d953 --- /dev/null +++ b/.helloagents/archive/2026-04/202604272310_ticket-chat-image-dnd-paste-upload/proposal.md @@ -0,0 +1,165 @@ +# 变更提案: ticket-chat-image-dnd-paste-upload + +## 元信息 +```yaml +类型: 新功能 +方案类型: implementation +优先级: P2 +状态: 已确认 +创建: 2026-04-27 +``` + +--- + +## 1. 需求 + +### 背景 +`admin-frontend` 工单工作台当前已支持通过按钮选择图片,并将上传后的 URL 以 Markdown 图片语法插入回复框。客服处理截图类问题时,图片常来自本地文件拖入或剪贴板截图,单一按钮选择会降低连续回复效率。 + +### 目标 +- 在 `admin-frontend/src/views/tickets/TicketWorkspaceDialog.vue` 的工单聊天回复区域支持拖拽图片上传。 +- 在回复框聚焦时支持从剪贴板粘贴图片上传。 +- 复用现有 `uploadImage()`、图片类型校验、10MB 大小限制和 Markdown 图片插入方式。 +- 保留原有点击上传入口,并补齐上传中、拖拽悬停、失败提示等状态反馈。 +- 将超大 SFC 的样式拆分到同目录 SCSS 文件,避免继续扩大 533 行组件文件。 + +### 约束条件 +```yaml +时间约束: 无 +性能约束: 不引入新依赖;拖拽/粘贴只处理图片文件,不做额外预览缓存 +兼容性约束: 保持 Vue3 + Element Plus + Vite 现有技术栈;保留原有 /upload/rest/upload 上传接口 +业务约束: 不修改后端工单回复语义,不改变 replyTicket payload,仅插入 Markdown 图片链接 +``` + +### 验收标准 +- [ ] 点击“上传图片”仍可上传图片并插入 `![image](url)`。 +- [ ] 拖拽图片到回复区域可上传并插入 Markdown 图片。 +- [ ] 在回复框中粘贴剪贴板图片可上传并插入 Markdown 图片。 +- [ ] 非图片文件、超过 10MB 图片会被拒绝并展示明确提示。 +- [ ] 上传中回复区有可见状态,重复上传不会破坏现有回复内容。 +- [ ] `npm run build` 在 `admin-frontend` 通过。 + +--- + +## 2. 方案 + +### 技术方案 +在 `TicketWorkspaceDialog.vue` 中抽出统一的 `uploadReplyImages(files, source)` 流程:从 Element Plus 上传、拖拽事件、粘贴事件统一收敛到同一校验与上传路径。回复区外层增加 drag/drop 事件和视觉状态,`ElInput` 增加 paste 事件。上传成功后继续以 Markdown 图片语法追加到 `replyMessage`,失败时按具体来源给出错误提示。样式迁移到 `TicketWorkspaceDialog.scss`,并新增拖拽激活态、上传提示行和响应式细节。 + +### 影响范围 +```yaml +涉及模块: + - admin-frontend: 工单工作台回复区图片上传交互 +预计变更文件: 4 +``` + +### 风险评估 +| 风险 | 等级 | 应对 | +|------|------|------| +| 粘贴文本时误触上传逻辑 | 低 | 仅当 clipboardData 中存在 image File 时拦截默认行为,否则保持原粘贴行为 | +| 多图拖拽上传时状态混乱 | 中 | 顺序上传并复用同一个上传状态,逐个插入 Markdown,失败时提示具体失败原因 | +| 样式拆分造成 scoped 样式失效 | 低 | 使用项目已有 `` 模式;组件文件降到 400 行以下。 + - 验证方式: 文件行数检查 + `npm run build` + - depends_on: [1.2] + +### 3. 验证与知识库同步 + +- [√] 3.1 运行前端构建并同步知识库 + - 预期变更: 执行 `npm run build`;更新 `.helloagents/modules/admin-frontend.md`、方案包任务状态和 CHANGELOG/归档信息。 + - 完成标准: 构建通过或明确记录阻断原因;知识库反映工单工作台支持点击、拖拽和粘贴图片上传。 + - 验证方式: `npm run build` + 文件检查 + - depends_on: [2.1] + +--- + +## 执行日志 + +| 时间 | 任务 | 状态 | 备注 | +|------|------|------|------| +| 2026-04-27 23:10:00 | 方案设计 | in_progress | 已创建方案包并确认唯一实现路径 | +| 2026-04-27 23:15:00 | 1.1 / 1.2 | completed | 已抽出 `useTicketReplyImages`,统一点击、拖拽、粘贴图片上传链路 | +| 2026-04-27 23:16:00 | 2.1 | completed | 已拆出 `TicketWorkspaceDialog.scss`,组件文件降至 326 行 | +| 2026-04-27 23:18:00 | 3.1 | completed | `npm run build` 已通过 | + +--- + +## 执行备注 + +- 当前工作树已有 `public/assets/admin` 未提交改动,本任务不覆盖该路径。 +- 本地上传端到端依赖 `/upload/rest/upload` 可用;构建验证不能替代真实上传环境人工核对。 +- `npm run build` 会刷新 `public/assets/admin` 构建产物;该路径在执行前已经是未提交状态。 diff --git a/.helloagents/archive/_index.md b/.helloagents/archive/_index.md index 8bb22ea..0ada825 100644 --- a/.helloagents/archive/_index.md +++ b/.helloagents/archive/_index.md @@ -7,6 +7,7 @@ | 时间戳 | 名称 | 类型 | 涉及模块 | 决策 | 结果 | |--------|------|------|---------|------|------| +| 202604272310 | ticket-chat-image-dnd-paste-upload | implementation | admin-frontend | ticket-chat-image-dnd-paste-upload#D001 | ✅完成 | | 202604250018 | admin-frontend-user-activity-status-filter | implementation | admin-frontend,backend | admin-frontend-user-activity-status-filter#D001,#D002,#D003 | ✅完成 | | 202604250002 | order-payment-snapshot | implementation | admin-frontend,order-payment | order-payment-snapshot#D001,#D002 | ✅完成 | | 202604242245 | admin-frontend-node-pagination-batch-edit | implementation | admin-frontend | admin-frontend-node-pagination-batch-edit#D001,#D002,#D003 | ✅完成 | @@ -36,6 +37,7 @@ ## 按月归档 ### 2026-04 +- [202604272310_ticket-chat-image-dnd-paste-upload](./2026-04/202604272310_ticket-chat-image-dnd-paste-upload/) - 为工单工作台回复区补齐图片拖拽上传与剪贴板粘贴上传,并将上传逻辑与样式从超大 SFC 中拆分 - [202604250018_admin-frontend-user-activity-status-filter](./2026-04/202604250018_admin-frontend-user-activity-status-filter/) - 为用户管理高级筛选新增“活跃状态”条件,并在后端补齐 `activity_status` 复合过滤规则,支持按活跃 / 非活跃筛选用户 - [202604250002_order-payment-snapshot](./2026-04/202604250002_order-payment-snapshot/) - 补齐订单支付成功快照保存链路,并在后台订单详情中集中展示支付渠道、支付方法、平台订单号、商户订单号、实付金额与支付 IP - [202604242245_admin-frontend-node-pagination-batch-edit](./2026-04/202604242245_admin-frontend-node-pagination-batch-edit/) - 为节点管理工作台补齐本地分页、父/子节点筛选、单节点置顶,以及仅对已勾选节点生效的批量修改 diff --git a/.helloagents/modules/admin-frontend.md b/.helloagents/modules/admin-frontend.md index 7720a09..27f8ac8 100644 --- a/.helloagents/modules/admin-frontend.md +++ b/.helloagents/modules/admin-frontend.md @@ -17,6 +17,7 @@ - 受保护路由在未登录时会自动附加 `redirect` 查询参数 - API 基础路径使用 `/api/v2/{secure_path}`,其中 `secure_path` 来自运行时配置 - 工单工作台现允许对已关闭工单继续回复;管理员发送新消息后会提示“发送并重开”,并通过统一后端语义把工单状态重新开启 +- 工单工作台回复区支持点击选择、拖拽放下和剪贴板粘贴三种图片上传入口,统一复用 `/upload/rest/upload` 图片上传和 Markdown 图片链接插入逻辑;上传期间会禁用发送入口,避免图片链接尚未写入时提前回复 - 仪表盘以真实后端接口返回值为准,不在前端伪造业务统计 - 仪表盘“收入趋势”支持在同一张趋势图中切换“按金额 / 按数量”,数量模式同步切换摘要卡片、Y 轴标签与最近记录 - 仪表盘“作业详情”支持打开失败作业报错弹窗,集中查看 Horizon 失败作业的报错摘要、失败时间与队列信息 @@ -94,6 +95,7 @@ - 依赖 `src/utils/notices.ts` 负责公告表单转换、内容摘要、排序与显示字段归一化 - 依赖 `src/utils/systemConfig.ts` 负责系统配置字段元信息、默认值、回填与保存序列化 - 依赖 `src/utils/routes.ts` 负责路由动作映射、匹配规则序列化、节点引用摘要与搜索过滤 +- 依赖 `src/views/tickets/useTicketReplyImages.ts` 收敛工单回复区图片点击上传、拖拽上传、粘贴上传、文件校验和 Markdown 插入 - 依赖 Laravel 后端 `TicketService::reply()` 提供工单“再次回复自动重开”的统一业务语义 - 依赖 Laravel 注入的 `window.settings` - 构建输出到 `public/assets/admin` diff --git a/admin-frontend/src/utils/nodes.ts b/admin-frontend/src/utils/nodes.ts index b01c848..0ec6929 100644 --- a/admin-frontend/src/utils/nodes.ts +++ b/admin-frontend/src/utils/nodes.ts @@ -9,6 +9,8 @@ export interface NodeStatusMeta { tagType: 'success' | 'warning' | 'danger' | 'info' } +type NodeStatusClass = NodeStatusMeta['dotClass'] + const NODE_TYPE_LABELS: Record = { shadowsocks: 'Shadowsocks', trojan: 'Trojan', @@ -64,6 +66,10 @@ export function getNodeStatusMeta(node: AdminNodeItem): NodeStatusMeta { } } +function isNodeOnlineStatus(status: NodeStatusClass): boolean { + return status === 'online' || status === 'pending' +} + export function getNodeIdLabel(node: AdminNodeItem): string { return node.parent_id ? `${node.id} → ${node.parent_id}` : String(node.id) } @@ -146,7 +152,7 @@ export function filterNodes( } const nodeStatus = getNodeStatusMeta(node).dotClass - if (normalizedStatus === 'online' && nodeStatus !== 'online') { + if (normalizedStatus === 'online' && !isNodeOnlineStatus(nodeStatus)) { return false } @@ -167,7 +173,7 @@ export function filterNodes( } export function countOnlineNodes(nodes: AdminNodeItem[]): number { - return nodes.filter((node) => getNodeStatusMeta(node).dotClass === 'online').length + return nodes.filter((node) => isNodeOnlineStatus(getNodeStatusMeta(node).dotClass)).length } export function countVisibleNodes(nodes: AdminNodeItem[]): number { diff --git a/admin-frontend/src/views/tickets/TicketWorkspaceDialog.scss b/admin-frontend/src/views/tickets/TicketWorkspaceDialog.scss new file mode 100644 index 0000000..f1e9ac4 --- /dev/null +++ b/admin-frontend/src/views/tickets/TicketWorkspaceDialog.scss @@ -0,0 +1,270 @@ +.workspace-header { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 16px; +} + +.workspace-title p { + font-size: 11px; + letter-spacing: 0.22em; + text-transform: uppercase; + color: var(--xboard-text-muted); +} + +.workspace-title h2 { + font-size: 34px; + line-height: 1.08; + color: var(--xboard-text-strong); +} + +.workspace-header__actions { + display: flex; + align-items: center; + gap: 10px; +} + +.ghost-action { + color: var(--xboard-link); +} + +.danger-action { + color: var(--xboard-danger); +} + +.workspace-shell { + display: grid; + grid-template-columns: 280px minmax(0, 1fr); + min-height: 70vh; + border: 1px solid var(--xboard-border); + border-radius: 24px; + overflow: hidden; +} + +.workspace-sidebar { + display: grid; + grid-template-rows: auto 1fr; + border-right: 1px solid var(--xboard-border); + background: #fbfbfd; +} + +.sidebar-header { + display: grid; + gap: 12px; + padding: 20px; + border-bottom: 1px solid var(--xboard-border); +} + +.sidebar-list { + display: grid; + align-content: start; + gap: 8px; + padding: 16px; + overflow-y: auto; +} + +.sidebar-ticket { + border: 1px solid transparent; + background: #ffffff; + border-radius: 18px; + padding: 16px; + text-align: left; + display: grid; + gap: 8px; + cursor: pointer; + transition: 0.2s ease; +} + +.sidebar-ticket.active { + border-color: rgba(0, 113, 227, 0.24); + background: rgba(0, 113, 227, 0.08); +} + +.sidebar-ticket__row, +.sidebar-ticket__meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.sidebar-ticket span, +.sidebar-ticket small { + color: var(--xboard-text-muted); +} + +.workspace-main { + display: grid; + grid-template-rows: auto 1fr auto; + background: #ffffff; + min-height: 0; +} + +.conversation-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + padding: 20px 24px; + border-bottom: 1px solid var(--xboard-border); +} + +.conversation-header h3 { + font-size: 32px; + line-height: 1.08; + color: var(--xboard-text-strong); +} + +.conversation-meta { + display: flex; + flex-wrap: wrap; + gap: 14px; + margin-top: 8px; + color: var(--xboard-text-muted); +} + +.conversation-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.message-thread { + display: grid; + align-content: start; + gap: 16px; + padding: 24px; + overflow-y: auto; + background: linear-gradient(180deg, #ffffff 0%, #fbfbfd 100%); +} + +.message-card { + display: grid; + gap: 10px; + max-width: min(720px, 100%); + padding: 18px 20px; + border-radius: 20px; + box-shadow: var(--xboard-shadow); +} + +.from-user { + background: #eef3fb; +} + +.from-admin { + background: #1d1d1f; + color: #ffffff; + margin-left: auto; +} + +.message-card__meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + font-size: 12px; +} + +.from-user .message-card__meta { + color: var(--xboard-text-muted); +} + +.from-admin .message-card__meta { + color: rgba(255, 255, 255, 0.72); +} + +.markdown-body :deep(p), +.markdown-body :deep(h1), +.markdown-body :deep(h2), +.markdown-body :deep(h3), +.markdown-body :deep(ul), +.markdown-body :deep(ol) { + margin: 0 0 10px; +} + +.markdown-body :deep(img) { + max-width: min(420px, 100%); + border-radius: 14px; +} + +.markdown-body :deep(a) { + color: inherit; + text-decoration: underline; +} + +.reply-box { + display: grid; + gap: 12px; + padding: 20px 24px; + border-top: 1px solid var(--xboard-border); + background: rgba(255, 255, 255, 0.92); + transition: background 0.18s ease, box-shadow 0.18s ease; +} + +.reply-box.is-drag-active { + background: #fbfdff; + box-shadow: inset 0 0 0 2px rgba(0, 113, 227, 0.34); +} + +.reply-box.is-uploading { + cursor: progress; +} + +.reply-box__hint, +.reply-box__drop-hint { + color: var(--xboard-text-muted); + font-size: 13px; + line-height: 1.5; +} + +.reply-box__drop-hint { + display: flex; + align-items: center; + gap: 6px; + min-height: 20px; + color: var(--xboard-link); + opacity: 0.78; + transition: opacity 0.18s ease, color 0.18s ease; +} + +.reply-box.is-drag-active .reply-box__drop-hint { + color: var(--xboard-primary); + opacity: 1; +} + +.reply-box__actions { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 12px; +} + +.workspace-empty { + display: grid; + place-items: center; + color: var(--xboard-text-muted); +} + +@media (max-width: 1023px) { + .workspace-shell { + grid-template-columns: 1fr; + } + + .workspace-sidebar { + border-right: 0; + border-bottom: 1px solid var(--xboard-border); + } + + .workspace-header, + .conversation-header { + flex-direction: column; + align-items: stretch; + } +} + +@media (max-width: 640px) { + .reply-box__actions { + justify-content: stretch; + flex-wrap: wrap; + } +} diff --git a/admin-frontend/src/views/tickets/TicketWorkspaceDialog.vue b/admin-frontend/src/views/tickets/TicketWorkspaceDialog.vue index f4ad24b..bd69ab4 100644 --- a/admin-frontend/src/views/tickets/TicketWorkspaceDialog.vue +++ b/admin-frontend/src/views/tickets/TicketWorkspaceDialog.vue @@ -1,7 +1,6 @@