Files
wecom_it_smart_desk/docs/02-技术文档/技术架构/技术方案-REQ-用户-007-自助诊断面板优化-v1.0.md
T

735 lines
29 KiB
Markdown
Raw Normal View History

# 技术方案 - 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 |