Files
wecom_it_smart_desk/docs/02-技术文档/技术架构/技术方案-REQ-用户-007-自助诊断面板优化-v1.0.md
T
Simon 44e77dcb0e chore(docs): docs/ 目录全面重新编号 + 重组
**重构前**(旧编号 02-11):
- docs/02-产品需求/      → 00 产品规划/PRD
- docs/03-技术架构/      → 01-05 子目录散落
- docs/04-原型设计/      → 01-02 产品设计(HTML 原型)
- docs/05-原型设计/      → screens/
- docs/06-测试素材/      → 02-E2E / 03-功能 / 04-版本测试
- docs/07-项目管理/      → 任务说明书/日报/计划
- docs/08-安全审计/      → 审计报告
- docs/09-堡垒运维/      → toolbox / deploy
- docs/10-项目管理/      → 任务说明书(重复)
- docs/11-历史归档/      → deploy-nas-archived

**重构后**(新编号 00-07,语义化):
- docs/00-产品开发流程与文档管理规范.md
- docs/00-版本迭代总览.md
- docs/01-产品文档/      (PRD/原型/认证/会话/AI 服务/坐席/集成)
- docs/02-技术文档/      (技术方案/架构图/重构记录/前端改造/实现配置)
- docs/03-测试文档/      (E2E/功能用例/版本报告/缺陷单)
- docs/04-运维文档/      (部署运维/运维指南)
- docs/05-运营文档/      (品牌推广/用户手册)
- docs/06-安全审计/      (审计报告)
- docs/07-项目管理/      (任务说明书/日报/计划/看板)

**净收益**:
- 目录编号与产品文档管理规范对齐(按文档阶段 01-07 编号)
- 消除 02-产品需求 与 10-项目管理 的编号重叠
- 子目录按文档类型分组(如 01-产品文档/00-产品规划、01-产品文档/01-认证与登录)
- 把运维/安全/项目管理从 0X 散落改为 04/06/07

合计 494 文件 + 78495 行 / - 14076 行
2026-08-03 18:46:55 +08:00

29 KiB
Raw Blame History

技术方案 - H5 自助诊断面板优化

REQ 编号: REQ-用户-007 版本: v1.0-Live(前端 v2.3.6 已上线,2026-07-29 日期: 2026-07-29 作者: 宋献 (Simon) + Duckula 依赖 PRDdocs/01-产品文档/05-用户端H5/PRD-REQ-用户-007-自助诊断面板优化-v1.0.md11 项决策全部锁定,必须遵守 依赖代码v2.3.6 生产部署版):

  • src/frontend-h5/src/components/assistant/SelfDiagnosis.vue:内部仅 6 Tab + 内容区;通过 defineExpose 暴露 6 API
  • src/frontend-h5/src/components/assistant/QueueWaiting.vuev3.0 + 新增 showPosition?: boolean prop
  • src/frontend-h5/src/components/assistant/RightPanel.vuev2.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 模板改动

<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 脚本改动

// 新增 state
const isMobilePanelOpen = ref(false)  // 默认 false 显示左对话

// 改造 showRightPanel
const showRightPanel = computed(() => {
  if (!isMobile.value) return true    // 桌面端始终显示
  return isMobilePanelOpen.value      // 移动端由 toggle 控制
})

4.3 样式改动

.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 模板(最终版)

<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 修复)

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 新增)

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

/** 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)

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 关键)

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 脚本改动

const props = defineProps<{
  showQuiz?: boolean
  showPosition?: boolean  // 新增,默认 true(向后兼容)
}>()

6.2 模板改动

<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 关注点

  • 默认 showPositiontrue(向后兼容现有调用方)
  • 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 模板(最终版)

<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 脚本关键改动

// 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 样式关键改动

/* 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: autoaccordion-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 项

  • 桌面端 ≥500px:双栏正常,自助诊断 / 智能推荐 / 排队全部正常
  • 移动端 <500px:默认显示左对话;点击 🩺 切换显示右栏
  • queue 标题栏:显示位置 / LV / 分 三件套
  • queue-content:内不重复显示位置卡片
  • 异常徽章:5W·0F 分级显示
  • 异常+接入混合:显示 安全 1•2(红数字 + 灰斜体)
  • 0 异常:达标动画 3 秒后渐隐
  • Tab 切换:只显示当前 Tab 数据,不显示其他 Tab

v2.3.3 标题栏外提(决策 ⑧)

  • SelfDiagnosis 内部独立标题栏
  • 徽章 + 一键诊断按钮在 RightPanel accordion-item__header 显示
  • actions 区 @click.stop 不触发折叠
  • defineExpose 暴露 6 APItotalBadge / totalBadgeByLevel / showCelebrate / isRunningAll / hasAnyDiagItems / runAllDiag

v2.3.4 网络/系统 Tab 统一(决策 ⑨)

  • 6 Tab 内所有条目(diagItems + riskItems)统一 .diag-item 模板
  • 每条诊断项有"检测"按钮;点击触发单条重检
  • diagItem 检测 800ms-1.5s 后返回 Mock 结果
  • riskItem 检测 800ms 模拟态后恢复
  • pending 条目检测按钮禁用
  • diagItemIcon 根据 label 推断 emoji8 类规则)

v2.3.5 内容高度自适应(决策 ⑩)

  • 删除 accordion-item--active max-height: 80%
  • 右栏 overflow-y: auto 整体可滚动
  • 自助诊断展开后内容高度由条目数自然撑开

v2.3.6 排队置底 + 未排队占位(决策 ⑪)

  • queue-section margin-top: auto 始终贴底
  • 排队时标题栏 #5 前面4人(蓝色背景徽章)
  • 未排队时标题栏 未排队(斜体灰色占位)
  • LV/分信息按 pointsInfo 状态显示

8.2 性能验收

  • Tab 切换响应 < 100ms
  • 异常徽章实时刷新 < 200ms
  • 移动端 toggle 切换 < 200ms

8.3 兼容性验收

  • Chrome / Edge / Safari 最新版
  • iOS Safari 14+
  • Android Chrome 8+
  • 屏宽 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

十一、关联文档

  • PRDdocs/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