# 技术方案 - H5 自助诊断面板优化 > **REQ 编号**: REQ-用户-007 > **版本**: v1.0-Live(前端 v2.3.6 已上线,2026-07-29) > **日期**: 2026-07-29 > **作者**: 宋献 (Simon) + Duckula > **依赖 PRD**:`docs/01-产品文档/05-用户端H5/PRD-REQ-用户-007-自助诊断面板优化-v1.0.md`(**11 项决策全部锁定,必须遵守**) > **依赖代码**(v2.3.6 生产部署版): > - `src/frontend-h5/src/components/assistant/SelfDiagnosis.vue`:内部仅 6 Tab + 内容区;通过 `defineExpose` 暴露 6 API > - `src/frontend-h5/src/components/assistant/QueueWaiting.vue`:v3.0 + 新增 `showPosition?: boolean` prop > - `src/frontend-h5/src/components/assistant/RightPanel.vue`:v2.3.6 —— accordion-item__header 含 actions 区 + queue-section margin-top: auto > - `src/frontend-h5/src/views/ChatView.vue`:移动端 `showRightPanel = !isMobile || isMobilePanelOpen` --- ## 一、现状分析 ### 1.1 现有代码事实(v2.3.6 已核验) | 文件 | 行号 | 现状 | v2.3.6 后状态 | |------|------|------|--------------| | `SelfDiagnosis.vue` | 1-109 | 仅渲染 6 Tab + 内容区,无独立标题栏 | ✅ v2.3.3 移除 `.self-diag__header` | | `SelfDiagnosis.vue` | 478-512 | `tabs` computed 返回 `{ key, label, shortLabel, badge }` | ✅ v2.3.2 补 `shortLabel` 字段 | | `SelfDiagnosis.vue` | 492-494 | `currentTab` computed 自动过滤 | ✅ 切 Tab 行为天然 | | `SelfDiagnosis.vue` | 524-544 | `totalBadge` / `totalBadgeByLevel` / `showCelebrate` | ✅ v2.3 新增全局徽章 3 computed | | `SelfDiagnosis.vue` | 683-690 | `defineExpose` 暴露 6 API | ✅ v2.3.3 暴露给 RightPanel | | `SelfDiagnosis.vue` | 616-628 | `diagItemIcon(item)` 函数 | ✅ v2.3.4 根据 label 推断 emoji | | `SelfDiagnosis.vue` | 631-646 | `runSingleDiag(item)` 统一方法 | ✅ v2.3.4 diagItem 走 runSingle,riskItem 模拟 800ms | | `RightPanel.vue` | 64-102 | accordion-item__header 含 🩺 + 标题 + actions 区 | ✅ v2.3.3 外层 actions 区 | | `RightPanel.vue` | 100 | `` | ✅ v2.3.3 通过 ref 读取数据 | | `RightPanel.vue` | 128-166 | queue-header-v3 + v-else "未排队" 占位 | ✅ v2.3.6 加入未排队占位 | | `RightPanel.vue` | 533-539 | `.right-panel__queue-section { margin-top: auto }` | ✅ v2.3.6 始终置底 | | `RightPanel.vue` | 330-339 | `.right-panel { overflow-y: auto }` | ✅ v2.3.5 内容自适应 | | `RightPanel.vue` | 355-358 | `.accordion-item--active` 无 max-height | ✅ v2.3.5 删除 80% 限制 | | `QueueWaiting.vue` | - | 新增 `showPosition?: boolean` prop | ✅ 默认 true(向后兼容) | | `ChatView.vue` | - | `isMobilePanelOpen` ref + 顶部双按钮 | ✅ v2.3 移动端 toggle | | `ChatView.vue` | - | `showRightPanel = !isMobile \|\| isMobilePanelOpen` | ✅ 默认左对话 | ### 1.2 关键约束(v2.3.6 已锁定) - **无后端改动**:所有数据由现有 WS 推送 + polling - **桌面端不变**:≥500px 维持双栏,不会引入新开关 - **Tab 切换天然**:当前 `currentTab` computed 已天然过滤,无需改 SelfDiagnosis.vue 核心逻辑 - **异常计数规则**:warning(W) + danger/fail(F) 计入;normal + pending(接入中) + idle(未执行) 不计入 - **标题栏位置**:v2.3.3 起在 RightPanel 外层,不再保留 SelfDiagnosis 内部标题栏 - **检测按钮**:v2.3.4 起每条诊断项都有独立 `检测` 按钮;diags 走真实检测,risks 模拟 800ms - **内容高度**:v2.3.5 起内容自然撑开 + 右栏整体可滚动;删除 max-height: 80% - **排队位置**:v2.3.6 起 `margin-top: auto` + 未排队占位 ## 二、目标与范围 ### 2.1 改造目标 | 目标 | 衡量标准 | |------|---------| | 标题栏异常徽章分级显示 | `5W · 0F` 双胶囊可见,hover 提示详情 | | 6 Tab 横向并排 | 桌面端 375px-720px 范围等分布局,激活态有蓝色下划线 | | 移动端 toggle 切换 | 默认左对话,点击 🩺 切到右栏,桌面端不受影响 | | queue 标题栏合并 | position/LV/分 内联展示,queue-content 仅保留答题区 | | 异常 + 接入混合 | M1 双角标并存,红数字 + 灰斜体 | ### 2.2 改造范围 - 前端 3 文件改动(ChatView.vue / QueueWaiting.vue / RightPanel.vue) - 1 文件补充(SelfDiagnosis.vue 加全局总徽章 computed) - 无后端 API 变更 - 无数据库迁移 - 无依赖升级 ## 三、架构设计 ### 3.1 组件依赖图 ``` ChatView.vue ├─ ChatPanel.vue (左对话) └─ RightPanel.vue (右栏) ├─ SelfDiagnosis.vue (本次:新增 totalBadge) │ ├─ 诊断 chips(执行类) │ └─ 风险项手风琴(展示类) ├─ DynamicRecommend.vue (智能推荐) └─ QueueWaiting.vue (本次:新增 showPosition prop) ├─ position-card (when showPosition !== false) └─ quiz-section (always) ``` ### 3.2 状态管理 | 状态 | 位置 | 说明 | |------|------|------| | `isMobile` | ChatView.vue | computed: `windowWidth < 500`(已有) | | `isMobilePanelOpen` | ChatView.vue | **新增**,ref: false,默认显示左对话 | | `activeTabKey` | SelfDiagnosis.vue | ref: 'network'(已有) | | `totalBadge` | SelfDiagnosis.vue | **新增**,computed: 各 Tab badge 求和 | | `isQueueExpanded` | RightPanel.vue | ref(已有) | | `isQuizExpanded` | RightPanel.vue | ref(已有) | ## 四、ChatView.vue 改动(移动端 toggle) ### 4.1 模板改动 ```vue 💬 对话 🩺 信息 ... ``` ### 4.2 脚本改动 ```typescript // 新增 state const isMobilePanelOpen = ref(false) // 默认 false 显示左对话 // 改造 showRightPanel const showRightPanel = computed(() => { if (!isMobile.value) return true // 桌面端始终显示 return isMobilePanelOpen.value // 移动端由 toggle 控制 }) ``` ### 4.3 样式改动 ```scss .chat-view__mobile-toggle { display: flex; gap: 6px; padding: 8px 12px; background: var(--bg-secondary); border-bottom: 1px solid var(--border-color); } .mobile-toggle__btn { flex: 1; height: 36px; border: 1px solid var(--border-color); border-radius: 18px; background: var(--bg-primary); color: var(--text-secondary); font-size: 13px; font-weight: 600; cursor: pointer; font-family: inherit; transition: all 0.2s; } .mobile-toggle__btn--active { background: var(--accent); color: #fff; border-color: var(--accent); } ``` ### 4.4 关注点 - 切换时**不卸载**左对话/右栏 DOM(用 v-show),保留滚动位置和输入文本 - 桌面端 ≥500px 时整个 `.chat-view__mobile-toggle` 不显示(v-if="isMobile") - 移动端切到右栏时,左对话用 `v-show="!isMobilePanelOpen"` 隐藏(不卸载) ## 五、SelfDiagnosis.vue 改动(v2.3.6 实际形态:仅渲染 Tab + 内容区) ### 5.1 v2.3.6 形态概览 **v2.3.3 起 SelfDiagnosis.vue 内部不再保留 `.self-diag__header` 标题栏**,组件仅负责: 1. 6 Tab 横向并排 2. 当前 Tab 的内容区(统一 `.diag-item` 模板) 3. 通过 `defineExpose` 暴露 6 API 给外层 RightPanel 读取数据/触发方法 ### 5.2 模板(最终版) ```vue {{ tab.shortLabel || tab.label }} {{ tab.badge }} {{ diagItemIcon(item) }} {{ item.label }} {{ diagStatusText(item.status) }} {{ item.status === 'running' ? '...' : '检测' }} {{ diagExpanded.has(item.key) ? '▴' : '▾' }} {{ item.detail }} {{ item.icon }} {{ item.label }} {{ item.statusText }} 检测 {{ expandedRisks.has(item.key) ? '▴' : '▾' }} {{ line }} {{ item.actionText }} ``` ### 5.3 脚本关键改动 #### 5.3.1 tabs computed 必须返回 shortLabel(v2.3.2 修复) ```typescript const tabs = computed(() => { return tabsData.value.map(tab => { const diagAbnormal = tab.diagItems.filter( d => d.status === 'warning' || d.status === 'fail' ).length const riskAbnormal = tab.riskItems.filter( r => r.level === 'warning' || r.level === 'danger' ).length return { key: tab.key, label: tab.label, shortLabel: tab.shortLabel, // ← v2.3.2 关键修复:不可漏此字段 badge: diagAbnormal + riskAbnormal, } }) }) ``` **踩坑**:v2.3 初始版 computed 仅返回 `{ key, label, badge }`,模板 fallback `tab.shortLabel || tab.label` 在 undefined 时静默回退到完整名(如"系统软件"),无报错。 #### 5.3.2 全局总徽章 3 computed(v2.3 新增) ```typescript const totalBadge = computed(() => { return tabs.value.reduce((sum, t) => sum + (t.badge || 0), 0) }) const totalBadgeByLevel = computed(() => { let w = 0, f = 0 tabsData.value.forEach(tab => { w += tab.diagItems.filter(d => d.status === 'warning').length w += tab.riskItems.filter(r => r.level === 'warning').length f += tab.diagItems.filter(d => d.status === 'fail').length f += tab.riskItems.filter(r => r.level === 'danger').length }) return { w, f } }) const showCelebrate = computed(() => totalBadgeByLevel.value.w === 0 && totalBadgeByLevel.value.f === 0 ) ``` #### 5.3.3 状态映射函数(v2.3.4) ```typescript /** DiagStatus → RiskLevel 映射(共用样式) */ function diagStatusLevel(status: DiagStatus): RiskLevel { switch (status) { case 'idle': return 'pending' case 'running': return 'pending' case 'pass': return 'normal' case 'warning': return 'warning' case 'fail': return 'danger' } } /** 根据 DiagItem 推断 emoji 图标 */ function diagItemIcon(item: DiagItem): string { const text = item.label if (text.includes('网络') || text.includes('连通') || text.includes('网关')) return '🌐' if (text.includes('DNS') || text.includes('解析')) return '📡' if (text.includes('证书') || text.includes('SSL')) return '🔒' // ... 8 类推断规则 return '📋' } ``` #### 5.3.4 单条检测统一方法(v2.3.4) ```typescript async function runSingleDiag(item: DiagItem | RiskItem) { // RiskItem: 模拟 800ms 后恢复 if ('level' in item) { const original = item.statusText const originalLevel = item.level item.statusText = '检测中...' item.level = 'pending' await new Promise(r => setTimeout(r, 800)) item.statusText = original item.level = originalLevel return } // DiagItem: 走原有 runSingle 流程 await runSingle(item.key) } ``` #### 5.3.5 defineExpose 暴露 6 API(v2.3.3 关键) ```typescript defineExpose({ totalBadge, // 全部 Tab badge 之和 totalBadgeByLevel, // { w: number, f: number } showCelebrate, // 0 异常达标动画触发 isRunningAll: isRunningAllExport, hasAnyDiagItems, runAllDiag, }) ``` ### 5.4 样式关键改动 - **删除** `.self-diag__header` 整块(标题栏已迁至外层) - **保留** `.self-diag__tabs`(6 Tab 横向并排,高度 30px) - **新增** `.diag-list` / `.diag-item` / `.diag-item__btn`(统一条目模板 + 检测按钮) - **删除** `.risk-list` / `.risk-item`(已并入 `.diag-item`) ## 六、QueueWaiting.vue 改动(新增 showPosition prop) ### 6.1 脚本改动 ```typescript const props = defineProps<{ showQuiz?: boolean showPosition?: boolean // 新增,默认 true(向后兼容) }>() ``` ### 6.2 模板改动 ```vue ... ... ``` ### 6.3 关注点 - 默认 `showPosition` 为 `true`(向后兼容现有调用方) - `RightPanel.vue` 传 `:show-position="false"` 即可隐藏位置卡片 - `props.showPosition !== false` 是宽松判断(兼容 undefined 视为 true) ## 七、RightPanel.vue 改动(v2.3.6 完整形态:4 节累计 v2.3.3 + v2.3.5 + v2.3.6) ### 7.1 模板(最终版) ```vue 💻 设备信息 {{ activeAccordion === 'device' ? '▾' : '▸' }} 🩺 自助诊断 ✓ 今日安全 {{ selfDiagRef.totalBadgeByLevel?.w ?? 0 }}W · {{ selfDiagRef.totalBadgeByLevel?.f ?? 0 }}F {{ selfDiagRef.isRunningAll ? '诊断中...' : '一键诊断' }} {{ activeAccordion === 'diagnosis' ? '▾' : '▸' }} ⚡ 智能推荐 {{ isRecommendExpanded ? '▾' : '▸' }} {{ recommendStore.unreadCount }} ⏳ 排队等待 #{{ queuePosition }}前面{{ aheadCount }}人 未排队 LV.{{ pointsInfo.level_index }} {{ pointsInfo.points }}分 {{ isQuizExpanded ? '收起答题' : '答题挑战' }} ``` ### 7.2 脚本关键改动 ```typescript // v2.3.3: SelfDiagnosis ref 用于读取徽章数据 + 触发一键诊断 const selfDiagRef = ref | null>(null) // v2.3: queue-section 位置/LV/分数据 const queuePosition = computed(() => queueWaitingRef.value?.currentPosition || 0) const aheadCount = computed(() => queueWaitingRef.value?.queueData?.queue?.ahead_count || 0) const pointsInfo = computed(() => queueWaitingRef.value?.pointsInfo || null) ``` ### 7.3 样式关键改动 ```scss /* v2.3.5: 右栏整体可滚动 */ .right-panel { display: flex; flex-direction: column; height: 100%; overflow-y: auto; /* v2.3.5 */ overflow-x: hidden; } /* v2.3.5: 删除 max-height 限制 */ .accordion-item--active { /* 无 max-height: 80% */ } /* v2.3.3: 外层标题栏 actions 区 */ .accordion-item__header-actions { display: inline-flex; align-items: center; gap: 6px; margin-left: auto; flex-shrink: 0; } /* v2.3.6: queue-section 始终置底 */ .right-panel__queue-section { flex-shrink: 0; margin-top: auto; /* v2.3.6 关键 */ background: var(--bg-primary, #fff); display: flex; flex-direction: column; } /* v2.3.6: 未排队占位 */ .qh-position-inline--idle small { font-style: italic; opacity: 0.85; } ``` ### 7.4 关注点 - **v2.3.3 actions 区**:徽章 + 一键诊断按钮放在 `.accordion-item__header-actions` 容器中,**`@click.stop` 阻止冒泡**触发手风琴折叠 - **v2.3.5 内容高度**:右栏 `overflow-y: auto`,accordion-item 不再设 `max-height` - **v2.3.6 置底**:`margin-top: auto` 在 flex column 容器中自动推至主轴底部,**不依赖中间区块 `flex: 1`** - **v2.3.6 占位**:v-else "未排队" 替换原本空荡的标题栏;`.qh-position-inline--idle` 斜体灰色样式 - **移动端**:<500px 时整 RightPanel 默认不渲染(v-if="showRightPanel" 且 `!isMobile`),需点击 🩺 信息按钮才显示 ## 八、验收标准 ### 8.1 功能验收 **v2.3 原始 7 项**: - [x] 桌面端 ≥500px:双栏正常,自助诊断 / 智能推荐 / 排队全部正常 - [x] 移动端 <500px:默认显示左对话;点击 🩺 切换显示右栏 - [x] queue 标题栏:显示位置 / LV / 分 三件套 - [x] queue-content:内不重复显示位置卡片 - [x] 异常徽章:5W·0F 分级显示 - [x] 异常+接入混合:显示 `安全 1•2`(红数字 + 灰斜体) - [x] 0 异常:达标动画 3 秒后渐隐 - [x] Tab 切换:只显示当前 Tab 数据,不显示其他 Tab **v2.3.3 标题栏外提(决策 ⑧)**: - [x] SelfDiagnosis 内部**无**独立标题栏 - [x] 徽章 + 一键诊断按钮在 RightPanel accordion-item__header 显示 - [x] actions 区 `@click.stop` 不触发折叠 - [x] defineExpose 暴露 6 API(totalBadge / totalBadgeByLevel / showCelebrate / isRunningAll / hasAnyDiagItems / runAllDiag) **v2.3.4 网络/系统 Tab 统一(决策 ⑨)**: - [x] 6 Tab 内所有条目(diagItems + riskItems)统一 `.diag-item` 模板 - [x] 每条诊断项有"检测"按钮;点击触发单条重检 - [x] diagItem 检测 800ms-1.5s 后返回 Mock 结果 - [x] riskItem 检测 800ms 模拟态后恢复 - [x] pending 条目检测按钮禁用 - [x] diagItemIcon 根据 label 推断 emoji(8 类规则) **v2.3.5 内容高度自适应(决策 ⑩)**: - [x] 删除 accordion-item--active max-height: 80% - [x] 右栏 overflow-y: auto 整体可滚动 - [x] 自助诊断展开后内容高度由条目数自然撑开 **v2.3.6 排队置底 + 未排队占位(决策 ⑪)**: - [x] queue-section margin-top: auto 始终贴底 - [x] 排队时标题栏 `#5 前面4人`(蓝色背景徽章) - [x] 未排队时标题栏 `未排队`(斜体灰色占位) - [x] LV/分信息按 pointsInfo 状态显示 ### 8.2 性能验收 - [x] Tab 切换响应 < 100ms - [x] 异常徽章实时刷新 < 200ms - [x] 移动端 toggle 切换 < 200ms ### 8.3 兼容性验收 - [x] Chrome / Edge / Safari 最新版 - [x] iOS Safari 14+ - [x] Android Chrome 8+ - [x] 屏宽 320px / 375px / 720px / 1920px ## 九、风险与回滚 | 风险 | 等级 | 缓解措施 | |------|------|---------| | 移动端 toggle 切换导致左对话消息丢失 | 高 | Vue v-show 切换(不卸载 DOM),保留输入栏文本和滚动位置 | | 异常徽章实时刷新失败(WS 断连) | 中 | 保留最后一次已知状态,15s 后重连重试 | | queue 标题栏紧凑导致小屏挤压 | 中 | flex-wrap: wrap 应对窄屏 | | 0 异常达标动画吸引注意力过度 | 低 | 3 秒后渐隐,不阻塞操作 | | 触达 0 异常时的统计埋点缺失 | 低 | v1.1 单独 PR,加埋点 | ## 十、部署方案 ### 10.1 部署方式 jumpserver-V2 skill(统一通道),参考项目记忆 MEMORY.md。 ### 10.2 部署顺序 1. 后端无改动(skip) 2. 前端 4 文件改动 → npm run build → jumpserver-V2 上传 → docker restart nginx 3. 浏览器端验证(agent-browser 加载 skill) ### 10.3 部署前置 - AST 静态校验(虽然 .vue 不是 .py,但 prettier 校验) - 累计 hash 变化确认(dist/assets/*.css) ### 10.4 v2.3.1 → v2.3.6 部署 hash 时间线 | 版本 | CSS hash | JS hash | 时间 | 关键变更 | |------|----------|---------|------|---------| | v2.3 首次 | `WYNGewev` | `CyKwWyjb` | 13:39 | 6 Tab 横向 + queue 合并 + 移动端 toggle | | v2.3.1 (fix1) | `B5iZHaWc` | `DWLS0ZMb` | 13:51 | 修复 Tab 网格 3×2 残留 | | v2.3.1 (fix2) | `CL_8bZ34` | `DRFXN8X-` | 13:56 | 修复 Tab 标签 4 字未用简称 | | v2.3.2 | `BBycvpph` | `CbImPO1p` | 14:35 | tabs computed 补 shortLabel + 恢复独立标题栏 | | v2.3.3 | `CCAA9XNU` | `DkUAW4Da` | 15:13 | 标题栏外提到 RightPanel | | v2.3.4 | `C8kd4YJI` | `DBVfD3sS` | 15:38 | 统一 .diag-item + 检测按钮 | | v2.3.5 | `CNWqSvBk` | `5JtvoGYc` | 15:51 | 删除 max-height + overflow-y: auto | | **v2.3.6** | **`GZiNwzZW`** | **`CAygTKBS`** | **16:56** | **margin-top: auto + 未排队占位** | ### 10.5 关键避坑(部署阶段) - **中英文双路径同步**:本地 Windows 路径 `D:\资料\03-项目开发\wecom_it_smart_desk\src\frontend-h5\` ≠ ASCII 路径 `D:\dev\wecom\src\frontend-h5\`,**两边必须同步修改** - **vite build 用 ASCII 路径**:中文路径下 `pnpm install` 卡死;复制项目到 `D:\dev\wecom` 后正常 - **build 产物路径**:`src/frontend-h5/dist/` - **docker restart 必须**:nginx bind mount 不会自动刷新新文件 - **缓存头**:nginx.conf HTML `no-cache, no-store, must-revalidate` + assets `immutable 365d` ## 十一、关联文档 - **PRD**:`docs/01-产品文档/05-用户端H5/PRD-REQ-用户-007-自助诊断面板优化-v1.0.md` - **任务说明书**:`docs/07-项目管理/任务说明书/任务说明书-REQ-用户-007-自助诊断面板优化.md` - **测试用例**:`docs/03-测试文档/03-功能测试用例/TC-REQ-用户-007-自助诊断面板优化.md` - **原型图**:`docs/01-产品文档/05-用户端H5/原型-REQ-用户-007-自助诊断面板优化-v1.0.html` - **设计文档**:`docs/02-技术文档/前端改造/设计-H5用户端实现概览-v1.0.md` - **现有代码**: - `src/frontend-h5/src/components/assistant/SelfDiagnosis.vue` - `src/frontend-h5/src/components/assistant/QueueWaiting.vue` - `src/frontend-h5/src/components/assistant/RightPanel.vue` - `src/frontend-h5/src/views/ChatView.vue` ## 十二、变更日志 | 版本 | 日期 | 变更内容 | 作者 | |------|------|---------|------| | v1.0 | 2026-07-29 12:35 | 初版:基于 PRD v1.0-Frozen 7 项决策,给出 3 文件 + 1 补充文件改动方案 + 部署方案 + 验收标准 | Duckula + 宋献 | | v1.0-Live | 2026-07-29 17:11 | 升级:§1.1 现状表补全 v2.3.6 实际状态;§五 重写 SelfDiagnosis.vue(v2.3.3 删除 self-diag__header + defineExpose + tabs shortLabel 修复);§七 重写 RightPanel.vue(v2.3.3 外提 + v2.3.5 overflow + v2.3.6 margin-top: auto);§八 验收补 v2.3.3-v2.3.6 4 节;§十 部署方案补 hash 时间线 + 避坑;§十二 补全 v2.3.1-v2.3.6 全部 hash | Duckula |