735 lines
29 KiB
Markdown
735 lines
29 KiB
Markdown
|
|
# 技术方案 - 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 | `<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 必须返回 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
|
|||
|
|
<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 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 |
|