Files
wecom_it_smart_desk/docs/02-技术文档/技术架构/技术方案-REQ-用户-007-自助诊断面板优化-v1.0.md
T
Simon facc04aa65 chore: docs 结构整改 + compose 双目录对齐(合并重建提交)
本提交为 .git 对象库损坏后的重建提交,内容等价于原先三个本地提交
(5e2fd4c2 / 57a53c98 / 5d7e1873)的累积结果,未做任何额外改动。

一、docs 结构整改(整改 #14)
根因:重构时新结构为 untracked 文件,执行 git stash(未带 -u)未纳入,
随后 git reset 拉回 HEAD 旧 tracked 树,导致旧树复活、新旧两棵目录
树并存于 docs/,共 791 文件、双分类体系冲突。

修复动作:
- b2 同名异主题文件改名迁移保全 9 个
- C 类 39 个孤立文件按主题正确归类
- A/B1 类 222 个重复文件删除(新结构已有内容副本)
- 9 个旧独有空目录删除
- 270 处内部引用按 verified 映射改写
- 整改记录 #14 登记于 04-运维文档/部署运维

结果:docs 791 → 569 文件,顶层仅规范 8 类 + 治理文件,单树恢复。
残留:约 20 处指向从未存在文件的陈旧死链,归入独立文档卫生任务。

二、compose 双目录对齐(消除踩坑 A)
- docker-compose.yml:nginx 前端挂载全部由根目录 frontend-*/dist
  改为 src/frontend-*/dist(h5 / agent / admin / terminal)
- docker-compose.dev.yml:dev 服务 build context 与卷同步改 src/
- 效果:本地 docker compose up 不再把根目录 stale dist 挂回,
  与线上一致,分叉隐患消除(已 docker compose config 校验通过)

防复发铁律:
- 重构须提交;仓库修复须 git stash -u 或先 commit
- 新结构须 git add 并提交,避免再次 untracked 复活
- H5 改动只动 src/frontend-h5/,禁改根目录遗留 frontend-*/
2026-08-07 22:31:32 +08:00

735 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 技术方案 - 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 走 runSingleriskItem 模拟 800ms |
| `RightPanel.vue` | 64-102 | accordion-item__header 含 🩺 + 标题 + actions 区 | ✅ v2.3.3 外层 actions 区 |
| `RightPanel.vue` | 100 | `<SelfDiagnosis ref="selfDiagRef" />` | ✅ 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
<template>
<div class="chat-view">
<!-- 现有左栏 -->
<div class="chat-view__left" :class="...">
<ChatPanel />
</div>
<!-- 新增移动端 toggle 切换按钮 isMobile 时显示 -->
<div v-if="isMobile" class="chat-view__mobile-toggle">
<button
:class="['mobile-toggle__btn', { 'mobile-toggle__btn--active': !isMobilePanelOpen }]"
@click="isMobilePanelOpen = false"
>
💬 对话
</button>
<button
:class="['mobile-toggle__btn', { 'mobile-toggle__btn--active': isMobilePanelOpen }]"
@click="isMobilePanelOpen = true"
>
🩺 信息
</button>
</div>
<!-- 现有拖拽手柄仅桌面端 -->
<div v-if="showRightPanel && !isMobile" class="chat-view__resize-handle" ...>
...
</div>
<!-- 右栏显示条件改造 -->
<div v-if="showRightPanel" class="chat-view__right">
<RightPanel />
</div>
</div>
</template>
```
### 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
<template>
<div class="self-diag">
<!-- REQ-用户-007 v2.3.3: 自助诊断标题栏🩺 自助诊断 + [5W·0F] + 一键诊断移至外层 RightPanel.vue accordion-item__header -->
<!-- 本组件仅渲染 6 Tab + 内容区 -->
<!-- 6 Tab 横向并排标签页 -->
<div class="self-diag__tabs">
<button
v-for="tab in tabs"
:key="tab.key"
class="tab-btn"
:class="{
'tab-btn--active': activeTabKey === tab.key,
'tab-btn--has-abnormal': tab.badge > 0
}"
@click="activeTabKey = tab.key"
>
<span class="tab-btn__label">{{ tab.shortLabel || tab.label }}</span>
<span v-if="tab.badge > 0" class="tab-btn__badge">{{ tab.badge }}</span>
</button>
</div>
<!-- Tab 内容区统一 .diag-item 模板 -->
<div class="self-diag__content">
<div class="diag-list">
<!-- 诊断条目网络/系统 Tab -->
<div
v-for="item in currentTab.diagItems"
:key="item.key"
class="diag-item"
:class="`diag-item--${diagStatusLevel(item.status)}`"
>
<div class="diag-item__header" @click="toggleDiag(item.key)">
<span class="diag-item__icon">{{ diagItemIcon(item) }}</span>
<span class="diag-item__name">{{ item.label }}</span>
<span class="diag-item__status" :class="`diag-item__status--${diagStatusLevel(item.status)}`">
{{ diagStatusText(item.status) }}
</span>
<button
class="diag-item__btn"
:disabled="item.status === 'running'"
@click.stop="runSingleDiag(item)"
>
{{ item.status === 'running' ? '...' : '检测' }}
</button>
<span class="diag-item__toggle">{{ diagExpanded.has(item.key) ? '▴' : '▾' }}</span>
</div>
<div v-if="diagExpanded.has(item.key) && item.detail" class="diag-item__detail">
{{ item.detail }}
</div>
</div>
<!-- 风险条目账号/设备/安全/合规 Tab -->
<div
v-for="item in currentTab.riskItems"
:key="item.key"
class="diag-item"
:class="[
`diag-item--${item.level}`,
{ 'diag-item--pending': item.level === 'pending' }
]"
>
<div class="diag-item__header" @click="toggleRiskItem(item.key)">
<span class="diag-item__icon">{{ item.icon }}</span>
<span class="diag-item__name">{{ item.label }}</span>
<span class="diag-item__status" :class="`diag-item__status--${item.level}`">
{{ item.statusText }}
</span>
<button
class="diag-item__btn"
:disabled="item.level === 'pending'"
@click.stop="runSingleDiag(item)"
>
检测
</button>
<span class="diag-item__toggle">{{ expandedRisks.has(item.key) ? '▴' : '▾' }}</span>
</div>
<div v-if="expandedRisks.has(item.key)" class="diag-item__detail">
<div v-for="line in item.detailLines" :key="line" class="detail-line">{{ line }}</div>
<button v-if="item.actionText" class="diag-item__action" @click.stop="handleRiskAction(item)">
{{ item.actionText }}
</button>
</div>
</div>
</div>
</div>
</div>
</template>
```
### 5.3 脚本关键改动
#### 5.3.1 tabs computed 必须返回 shortLabelv2.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 computedv2.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 APIv2.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
<template>
<div class="queue-waiting">
<!-- 改造position-card 是否渲染由 showPosition 控制 -->
<div
v-if="props.showPosition !== false"
class="queue-waiting__position-card"
:class="positionCardClass"
>
...
</div>
<!-- 答题区始终渲染 -->
<div v-show="props.showQuiz" class="queue-waiting__quiz-section">
...
</div>
</div>
</template>
```
### 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
<template>
<div class="right-panel">
<!-- 手风琴区域 -->
<div class="right-panel__accordion">
<!-- 设备信息默认折叠 -->
<div class="accordion-item" :class="{ 'accordion-item--active': activeAccordion === 'device' }">
<div class="accordion-item__header" @click="toggleAccordion('device')">
<span class="accordion-item__icon">💻</span>
<span class="accordion-item__title">设备信息</span>
<span class="accordion-item__toggle">{{ activeAccordion === 'device' ? '▾' : '▸' }}</span>
</div>
<div v-show="activeAccordion === 'device'" class="accordion-item__content">
<BasicInfoCard />
</div>
</div>
<!-- 自助诊断默认折叠 v2.3.3: 标题栏含徽章+一键诊断 -->
<div class="accordion-item" :class="{ 'accordion-item--active': activeAccordion === 'diagnosis' }">
<div class="accordion-item__header" @click="toggleAccordion('diagnosis')">
<span class="accordion-item__icon">🩺</span>
<span class="accordion-item__title">自助诊断</span>
<!-- v2.3.3: 异常徽章 + 一键诊断按钮从 SelfDiagnosis 提升至本外层标题栏 -->
<div class="accordion-item__header-actions" @click.stop>
<span v-if="selfDiagRef?.showCelebrate"
class="header__total-badge header__total-badge--celebrate"
title="所有异常已处理">
<span class="badge-icon"></span>
今日安全
</span>
<span v-else-if="selfDiagRef"
class="header__total-badge header__total-badge--graded"
:title="`共 ${selfDiagRef.totalBadgeByLevel?.w ?? 0} 警告 ${selfDiagRef.totalBadgeByLevel?.f ?? 0} 失败`">
<span class="badge-segment badge-segment--W">{{ selfDiagRef.totalBadgeByLevel?.w ?? 0 }}W</span>
<span class="badge-sep">·</span>
<span class="badge-segment badge-segment--F">{{ selfDiagRef.totalBadgeByLevel?.f ?? 0 }}F</span>
</span>
<button v-if="selfDiagRef?.hasAnyDiagItems"
class="header__btn"
:disabled="selfDiagRef.isRunningAll || (selfDiagRef.totalBadge > 0)"
@click="selfDiagRef.runAllDiag">
{{ selfDiagRef.isRunningAll ? '诊断中...' : '一键诊断' }}
</button>
</div>
<span class="accordion-item__toggle">{{ activeAccordion === 'diagnosis' ? '▾' : '▸' }}</span>
</div>
<div v-show="activeAccordion === 'diagnosis'" class="accordion-item__content">
<SelfDiagnosis ref="selfDiagRef" />
</div>
</div>
</div>
<div class="right-panel__divider"></div>
<!-- 智能推荐区域 -->
<div class="right-panel__recommend-section" :class="{ 'accordion-item--active': isRecommendExpanded }">
<div class="accordion-item__header" @click="toggleRecommend">
<span class="accordion-item__icon"></span>
<span class="accordion-item__title">智能推荐</span>
<span class="accordion-item__toggle">{{ isRecommendExpanded ? '▾' : '▸' }}</span>
<span v-if="recommendStore.unreadCount > 0" class="recommend-badge">{{ recommendStore.unreadCount }}</span>
</div>
<div v-show="isRecommendExpanded" class="recommend-content">
<DynamicRecommend />
</div>
</div>
<div class="right-panel__divider"></div>
<!-- 排队等待 v2.3.6: margin-top: auto + 未排队占位 -->
<div class="right-panel__queue-section">
<div class="queue-header-v3">
<div class="queue-header-v3__left" @click="toggleQueue">
<span class="queue-header-v3__icon"></span>
<span class="queue-header-v3__title">排队等待</span>
<!-- v2.3: 位置/LV/ 全部合并到标题栏 -->
<!-- v2.3.6: 不在排队时显示占位 -->
<span v-if="isQueued && queuePosition > 0" class="qh-position-inline">
#<b>{{ queuePosition }}</b><small>前面{{ aheadCount }}</small>
</span>
<span v-else class="qh-position-inline qh-position-inline--idle">
<small>未排队</small>
</span>
<span v-if="pointsInfo" class="qh-lv-inline">
LV.<b>{{ pointsInfo.level_index }}</b>
</span>
<span v-if="pointsInfo" class="qh-points-inline">
<b>{{ pointsInfo.points }}</b><i></i>
</span>
</div>
<div class="queue-header-v3__actions">
<button class="quiz-toggle-btn" @click="toggleQuiz">
{{ isQuizExpanded ? '收起答题' : '答题挑战' }}
</button>
</div>
</div>
<div v-show="isQueueExpanded" class="right-panel__queue-content">
<QueueWaiting
ref="queueWaitingRef"
:show-quiz="isQuizExpanded"
:show-position="false"
/>
</div>
</div>
</div>
</template>
```
### 7.2 脚本关键改动
```typescript
// v2.3.3: SelfDiagnosis ref 用于读取徽章数据 + 触发一键诊断
const selfDiagRef = ref<InstanceType<typeof SelfDiagnosis> | 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 APItotalBadge / 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 推断 emoji8 类规则)
**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.vuev2.3.3 删除 self-diag__header + defineExpose + tabs shortLabel 修复);§七 重写 RightPanel.vuev2.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 |