feat(C组): C-T1~T13 AI与数据模块完成

- C-T1 Wingman 辅助面板
- C-T2 Wingman 后端接口
- C-T3 排查流程图编辑器
- C-T4 流程图后端接口
- C-T5 流程图H5端展示
- C-T6 运营数据看板
- C-T7 看板数据接口
- C-T8 全局一致性审查(周2)
- C-T9 坐席绩效报表
- C-T10 绩效数据接口+Excel导出
- C-T11 会话智能标注
- C-T12 全局一致性审查(周3)
- C-T13 代码评审+PR创建
This commit is contained in:
Simon
2026-07-02 19:23:03 +08:00
parent fc22de7f4d
commit 6277db3951
5 changed files with 2207 additions and 18 deletions
@@ -0,0 +1,120 @@
// =============================================================================
// 企微IT智能服务台 — H5用户端排查模板 API 调用模块(v5.4 增强)
// =============================================================================
// 说明:封装与排查模板相关的所有 HTTP 请求
// 对应后端 API/api/troubleshooting-templates
// 包括:获取模板列表、获取模板详情
// =============================================================================
import apiClient from './index'
import type { AxiosResponse } from 'axios'
// --------------------------------------------------------------------------
// TypeScript 类型定义 — 与后端 Schema 保持一致
// --------------------------------------------------------------------------
/** 排查步骤路径节点 */
export interface PathStep {
/** 节点唯一标识(用于进度追踪) */
nodeId?: string
/** 步骤标题 */
label: string
/** 步骤状态: done / current / pending */
status: 'done' | 'current' | 'pending'
}
/** 决策树递归节点 */
export interface FlowchartNode {
/** 节点唯一标识 */
id: string
/** 节点类型: step / decision */
type: 'step' | 'decision'
/** 节点标签文字 */
label: string
/** 节点状态: done / current / pending */
status?: 'done' | 'current' | 'pending'
/** 子步骤列表(step 类型的操作明细) */
children?: FlowchartNode[]
/** "是" 分支(decision 类型)/ 下一步(step 类型) */
yes_branch?: FlowchartNode
/** "否" 分支(decision 类型) */
no_branch?: FlowchartNode
}
/** 排查模板对象(对应后端 TroubleshootingTemplateResponse */
export interface TroubleshootingTemplate {
/** 模板唯一标识 */
id: string
/** 模板名称 */
name: string
/** 分类: vpn/email/system/account */
category: string
/** 排障步骤路径 */
path_steps: PathStep[]
/** 流程图定义 */
flowchart: FlowchartNode
/** 是否启用 */
is_active: boolean
/** 创建时间 */
created_at: string
/** 更新时间 */
updated_at: string
}
/** 排查模板列表响应 */
export interface TroubleshootingTemplateListData {
/** 模板列表 */
items: TroubleshootingTemplate[]
/** 总数 */
total: number
}
/** 查询参数 */
export interface TroubleshootingTemplateQuery {
/** 按分类过滤(可选) */
category?: string
/** 页码(从1开始) */
page?: number
/** 每页数量 */
page_size?: number
}
// --------------------------------------------------------------------------
// API 函数
// --------------------------------------------------------------------------
/**
* 获取排查模板列表
* 支持按分类过滤和分页
*
* @param params - 查询参数
* @param params.category - 按分类过滤(可选)
* @param params.page - 页码(可选,默认 1
* @param params.page_size - 每页数量(可选,默认 20)
* @returns 排查模板列表数据
*/
export async function getTroubleshootingTemplates(
params?: TroubleshootingTemplateQuery,
): Promise<TroubleshootingTemplateListData> {
const response: AxiosResponse = await apiClient.get('/troubleshooting-templates', {
params: {
category: params?.category || undefined,
page: params?.page || 1,
page_size: params?.page_size || 20,
},
})
return response.data.data
}
/**
* 获取排查模板详情
*
* @param id - 模板ID
* @returns 排查模板详情
*/
export async function getTroubleshootingTemplate(
id: string,
): Promise<TroubleshootingTemplate> {
const response: AxiosResponse = await apiClient.get(`/troubleshooting-templates/${id}`)
return response.data.data
}
@@ -0,0 +1,825 @@
<!-- =============================================================================
// 企微IT智能服务台 — H5用户端排查模板详情页面(v5.4 增强)
// =============================================================================
// 说明:C-T5 流程图 H5 端展示
// 功能:
// 1. 展示流程图详情(树形结构)
// 2. 用户可以按照流程图自助排查
// 3. 点击决策节点的选择项来推进流程
// 4. 路径进度条正确反映排查进度(nodeId 追踪)
// 5. "联系IT客服" 按钮始终可见,便于随时切换人工支持
// ============================================================================= -->
<template>
<div class="troubleshooting-detail">
<!-- 顶部导航 -->
<div class="troubleshooting-detail__header">
<van-nav-bar
:title="templateName"
left-arrow
@click-left="goBack"
>
<template #right>
<van-icon
name="service-o"
size="20"
color="#07C160"
@click="goToChat"
class="header-contact-btn"
/>
</template>
</van-nav-bar>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="troubleshooting-detail__loading">
<van-loading type="spinner" color="#07C160" size="32" />
<span>加载中...</span>
</div>
<!-- 错误状态 -->
<div v-else-if="errorMsg" class="troubleshooting-detail__error">
<van-empty :description="errorMsg">
<van-button type="primary" size="small" @click="loadDetail">
重试
</van-button>
</van-empty>
</div>
<!-- 流程图内容 -->
<div v-else-if="template" class="troubleshooting-detail__content">
<!-- 路径进度条 -->
<div class="flow-progress" v-if="pathSteps.length > 0">
<div class="flow-progress__bar">
<template v-for="(step, index) in pathSteps" :key="step.nodeId || index">
<div
class="flow-progress__step"
:class="`flow-progress__step--${step.status}`"
>
<span
class="flow-progress__dot"
:class="`flow-progress__dot--${step.status}`"
>
<template v-if="step.status === 'done'"></template>
<template v-else>{{ index + 1 }}</template>
</span>
<span class="flow-progress__label">{{ step.label }}</span>
</div>
<span
v-if="index < pathSteps.length - 1"
class="flow-progress__arrow"
:class="{ 'flow-progress__arrow--active': step.status === 'done' }"
></span>
</template>
</div>
</div>
<!-- 当前节点展示 -->
<div class="flow-content">
<!-- 决策节点 -->
<template v-if="currentNode && currentNode.type === 'decision'">
<div class="flow-question">
<div class="flow-question__header">
<span class="flow-question__icon"></span>
<span class="flow-question__text">{{ currentNode.label }}</span>
</div>
<div class="flow-question__options">
<button
class="flow-option flow-option--yes"
@click="handleSelect('yes')"
:disabled="submitting"
>
<span class="flow-option__icon"></span>
<span>{{ currentNode.yes_branch?.label || '是' }}</span>
</button>
<button
class="flow-option flow-option--no"
@click="handleSelect('no')"
:disabled="submitting"
>
<span class="flow-option__icon"></span>
<span>{{ currentNode.no_branch?.label || '否' }}</span>
</button>
</div>
</div>
</template>
<!-- 步骤节点 -->
<template v-else-if="currentNode && currentNode.type === 'step'">
<div class="flow-instruction">
<div class="flow-instruction__header">
<span class="flow-instruction__icon">📋</span>
<span class="flow-instruction__title">{{ currentNode.label }}</span>
</div>
<!-- 子步骤明细 -->
<div v-if="currentNode.children && currentNode.children.length > 0" class="flow-sub-steps">
<div
v-for="(child, idx) in currentNode.children"
:key="child.id"
class="flow-sub-step"
:class="`flow-sub-step--${child.status || 'pending'}`"
>
<span class="flow-sub-step__dot">{{ idx + 1 }}</span>
<span class="flow-sub-step__label">{{ child.label }}</span>
</div>
</div>
<!-- 操作提示 -->
<div class="flow-instruction__hint">
<span>💡 按步骤操作完成后点击下方按钮继续</span>
</div>
<!-- 继续按钮 -->
<van-button
type="primary"
block
size="large"
@click="handleContinue"
:loading="submitting"
>
完成此步骤继续下一步
</van-button>
</div>
</template>
<!-- 流程结束 -->
<template v-else-if="isCompleted">
<div class="flow-complete">
<div class="flow-complete__icon">🎉</div>
<h3 class="flow-complete__title">排查完成</h3>
<p class="flow-complete__desc">
如果问题仍未解决建议您联系IT支持获取人工帮助
</p>
<van-button type="primary" block size="large" @click="goToChat">
📞 联系IT客服
</van-button>
<van-button
plain
block
size="small"
style="margin-top: 10px"
@click="resetFlow"
>
🔄 重新排查
</van-button>
</div>
</template>
<!-- 初始状态 -->
<template v-else>
<div class="flow-start">
<div class="flow-start__icon">🚀</div>
<h3 class="flow-start__title">开始排查</h3>
<p class="flow-start__desc">
{{ template.name }} - 按照流程指引逐步排查问题
</p>
<van-button type="primary" block size="large" @click="startFlow">
开始排查
</van-button>
<van-button
plain
block
size="small"
style="margin-top: 10px"
@click="goToChat"
>
💬 直接联系IT客服
</van-button>
</div>
</template>
</div>
<!-- 排查中底部联系客服浮动入口 -->
<div v-if="started && !isCompleted" class="flow-contact-fab">
<van-button
type="default"
size="small"
icon="service-o"
round
@click="goToChat"
>
联系IT客服
</van-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { showToast } from 'vant'
import {
getTroubleshootingTemplate,
type TroubleshootingTemplate,
type FlowchartNode,
type PathStep,
} from '@/api/troubleshooting-templates'
const router = useRouter()
const route = useRoute()
// --------------------------------------------------------------------------
// 状态
// --------------------------------------------------------------------------
/** 模板详情 */
const template = ref<TroubleshootingTemplate | null>(null)
/** 模板名称 */
const templateName = computed(() => (route.query.name as string) || '排查详情')
/** 加载状态 */
const loading = ref(true)
/** 错误信息 */
const errorMsg = ref('')
/** 提交状态(防重复点击) */
const submitting = ref(false)
/** 路径步骤 */
const pathSteps = ref<PathStep[]>([])
/** 当前节点 */
const currentNode = ref<FlowchartNode | null>(null)
/** 是否已开始 */
const started = ref(false)
/** 已访问节点 ID 列表(用于进度追踪) */
const visitedNodeIds = ref<Set<string>>(new Set())
// --------------------------------------------------------------------------
// 计算属性
// --------------------------------------------------------------------------
/** 流程是否完成 */
const isCompleted = computed(() => {
return started.value && !currentNode.value
})
// --------------------------------------------------------------------------
// 方法
// --------------------------------------------------------------------------
/**
* 返回上一页
*/
function goBack(): void {
if (started.value && !isCompleted.value) {
// 如果排查进行中,返回前提示
router.back()
} else {
router.back()
}
}
/**
* 加载模板详情
*/
async function loadDetail(): Promise<void> {
loading.value = true
errorMsg.value = ''
try {
const id = route.params.id as string
const data = await getTroubleshootingTemplate(id)
template.value = data
// 从模板的流程图预计算路径步骤
if (data.flowchart) {
pathSteps.value = buildPathSteps(data.flowchart)
}
} catch (error: any) {
console.error('[TroubleshootingDetail] 加载失败:', error)
errorMsg.value = error?.message || '加载失败,请稍后重试'
} finally {
loading.value = false
}
}
/**
* 从流程图根节点递归构建路径步骤列表
* 遍历所有决策节点和步骤节点(深度优先),排除子步骤
*/
function buildPathSteps(root: FlowchartNode): PathStep[] {
const steps: PathStep[] = []
function walk(node: FlowchartNode): void {
if (!node) return
// 只收集 decision 和 step 类型的主节点,子步骤通过 children 展示
if (node.type === 'decision' || node.type === 'step') {
steps.push({
nodeId: node.id,
label: node.label,
status: 'pending' as const,
})
}
// 对 decision 节点,递归处理两个分支
if (node.type === 'decision') {
if (node.yes_branch) walk(node.yes_branch)
if (node.no_branch) walk(node.no_branch)
}
// 对 step 节点,按 yes_branch 作为下一个节点
if (node.type === 'step' && node.yes_branch) {
walk(node.yes_branch)
}
}
walk(root)
// 标记第一个步骤为 current
if (steps.length > 0) {
steps[0].status = 'current'
}
return steps
}
/**
* 开始排查流程
*/
function startFlow(): void {
if (!template.value?.flowchart) return
started.value = true
visitedNodeIds.value = new Set<string>()
currentNode.value = template.value.flowchart
currentNode.value.status = 'current'
visitedNodeIds.value.add(currentNode.value.id)
// 更新路径步骤:第一个标记为 current
updatePathStepsForNode(currentNode.value.id)
}
/**
* 处理决策节点选择
*/
function handleSelect(option: 'yes' | 'no'): void {
if (!currentNode.value || submitting.value) return
submitting.value = true
try {
// 标记当前节点为已完成
currentNode.value.status = 'done'
const currentId = currentNode.value.id
// 获取下一个节点
const nextNode: FlowchartNode | undefined =
option === 'yes'
? currentNode.value.yes_branch
: currentNode.value.no_branch
if (nextNode) {
nextNode.status = 'current'
visitedNodeIds.value.add(nextNode.id)
currentNode.value = nextNode
updatePathStepsForNode(nextNode.id)
} else {
// 分支无下一个节点,流程结束
currentNode.value = null
// 所有步骤标记完成
pathSteps.value = pathSteps.value.map(step => ({ ...step, status: 'done' as const }))
}
showToast('已记录您的选择')
} finally {
submitting.value = false
}
}
/**
* 继续下一步(步骤节点完成)
* 步骤节点完成后,通过 yes_branch 推进到下一个节点
*/
function handleContinue(): void {
if (!currentNode.value || submitting.value) return
submitting.value = true
try {
// 标记当前节点为已完成
currentNode.value.status = 'done'
// 也标记所有子步骤为完成(如果有)
if (currentNode.value.children) {
for (const child of currentNode.value.children) {
child.status = 'done'
}
}
// 步骤节点的 yes_branch 指向下一个主节点
const nextNode: FlowchartNode | undefined = currentNode.value.yes_branch
if (nextNode) {
nextNode.status = 'current'
visitedNodeIds.value.add(nextNode.id)
currentNode.value = nextNode
updatePathStepsForNode(nextNode.id)
showToast('已进入下一步')
} else {
// 没有下一个节点,流程结束
currentNode.value = null
pathSteps.value = pathSteps.value.map(step => ({ ...step, status: 'done' as const }))
showToast('排查流程已完成')
}
} finally {
submitting.value = false
}
}
/**
* 根据当前节点 ID 更新路径步骤状态
* 使用 nodeId 精确匹配,避免 label 冲突
*/
function updatePathStepsForNode(currentNodeId: string): void {
const matchedIndex = pathSteps.value.findIndex(s => s.nodeId === currentNodeId)
if (matchedIndex < 0) {
// 当前节点不在预计算的路径步骤中(可能是嵌套子步骤等)
// 不做更新,保持现有步骤状态
return
}
pathSteps.value = pathSteps.value.map((step, i) => ({
...step,
status:
i < matchedIndex
? ('done' as const)
: i === matchedIndex
? ('current' as const)
: ('pending' as const),
}))
}
/**
* 重置排查流程
*/
function resetFlow(): void {
started.value = false
currentNode.value = null
visitedNodeIds.value = new Set()
// 重新构建路径步骤
if (template.value?.flowchart) {
pathSteps.value = buildPathSteps(template.value.flowchart)
}
}
/**
* 跳转到聊天页面联系客服
*/
function goToChat(): void {
router.push({ name: 'ChatView' })
}
// --------------------------------------------------------------------------
// 生命周期
// --------------------------------------------------------------------------
onMounted(() => {
loadDetail()
})
</script>
<style scoped>
/* ===== 主容器 ===== */
.troubleshooting-detail {
min-height: 100vh;
background: var(--bg-secondary);
padding-bottom: 80px;
}
/* ===== 顶部导航 ===== */
.troubleshooting-detail__header {
background: var(--bg-primary);
position: sticky;
top: 0;
z-index: 10;
}
.header-contact-btn {
cursor: pointer;
padding: 4px;
}
/* ===== 加载状态 ===== */
.troubleshooting-detail__loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 0;
gap: 12px;
color: var(--text-tertiary);
font-size: 14px;
}
/* ===== 错误状态 ===== */
.troubleshooting-detail__error {
padding: 60px 0;
}
/* ===== 内容区域 ===== */
.troubleshooting-detail__content {
padding: 12px;
}
/* ===== 路径进度条 ===== */
.flow-progress {
background: var(--bg-primary);
border-radius: 12px;
padding: 14px;
margin-bottom: 12px;
}
.flow-progress__bar {
display: flex;
align-items: center;
gap: 2px;
overflow-x: auto;
padding-bottom: 4px;
}
.flow-progress__step {
display: flex;
align-items: center;
gap: 4px;
white-space: nowrap;
flex-shrink: 0;
}
.flow-progress__dot {
width: 22px;
height: 22px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 700;
flex-shrink: 0;
}
.flow-progress__dot--done {
background: #22c55e;
color: #fff;
}
.flow-progress__dot--current {
background: #07C160;
color: #fff;
box-shadow: 0 0 0 3px rgba(7, 193, 96, 0.25);
}
.flow-progress__dot--pending {
background: var(--bg-tertiary);
color: var(--text-tertiary);
border: 1px solid var(--border-color);
}
.flow-progress__label {
font-size: 11px;
color: var(--text-secondary);
}
.flow-progress__step--done .flow-progress__label {
color: var(--text-tertiary);
text-decoration: line-through;
}
.flow-progress__step--current .flow-progress__label {
color: #07C160;
font-weight: 600;
}
.flow-progress__arrow {
color: var(--text-tertiary);
font-size: 10px;
flex-shrink: 0;
padding: 0 4px;
}
.flow-progress__arrow--active {
color: #22c55e;
}
/* ===== 流程内容卡片 ===== */
.flow-content {
background: var(--bg-primary);
border-radius: 12px;
padding: 16px;
}
/* ===== 决策节点 ===== */
.flow-question {
padding: 8px 0;
}
.flow-question__header {
display: flex;
align-items: flex-start;
gap: 10px;
margin-bottom: 16px;
}
.flow-question__icon {
font-size: 20px;
flex-shrink: 0;
}
.flow-question__text {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
line-height: 1.5;
}
.flow-question__options {
display: flex;
gap: 12px;
margin-top: 4px;
}
.flow-option {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 14px 16px;
border-radius: 12px;
border: 2px solid var(--border-color);
background: var(--bg-secondary);
color: var(--text-primary);
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
font-family: inherit;
}
.flow-option:active:not(:disabled) {
transform: scale(0.97);
}
.flow-option:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.flow-option--yes {
border-color: rgba(34, 197, 94, 0.4);
background: rgba(34, 197, 94, 0.08);
}
.flow-option--yes:active:not(:disabled) {
background: rgba(34, 197, 94, 0.2);
}
.flow-option--no {
border-color: rgba(239, 68, 68, 0.4);
background: rgba(239, 68, 68, 0.08);
}
.flow-option--no:active:not(:disabled) {
background: rgba(239, 68, 68, 0.2);
}
.flow-option__icon {
font-size: 18px;
}
/* ===== 步骤节点 ===== */
.flow-instruction {
padding: 8px 0;
}
.flow-instruction__header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 14px;
}
.flow-instruction__icon {
font-size: 18px;
flex-shrink: 0;
}
.flow-instruction__title {
font-size: 15px;
font-weight: 600;
color: #07C160;
}
.flow-sub-steps {
display: flex;
flex-direction: column;
gap: 8px;
margin: 14px 0;
}
.flow-sub-step {
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
}
.flow-sub-step__dot {
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 700;
flex-shrink: 0;
background: var(--bg-tertiary);
color: var(--text-tertiary);
}
.flow-sub-step--done .flow-sub-step__dot {
background: #22c55e;
color: #fff;
}
.flow-sub-step--done .flow-sub-step__label {
color: var(--text-tertiary);
text-decoration: line-through;
}
.flow-sub-step--current .flow-sub-step__dot {
background: #07C160;
color: #fff;
}
.flow-sub-step--current .flow-sub-step__label {
color: #07C160;
font-weight: 600;
}
.flow-sub-step__label {
color: var(--text-secondary);
}
.flow-instruction__hint {
margin: 14px 0;
padding: 10px 12px;
background: rgba(7, 193, 96, 0.08);
border-radius: 8px;
font-size: 13px;
color: #07C160;
}
/* ===== 开始 / 完成状态 ===== */
.flow-start,
.flow-complete {
text-align: center;
padding: 30px 20px;
}
.flow-start__icon,
.flow-complete__icon {
font-size: 48px;
margin-bottom: 16px;
}
.flow-start__title,
.flow-complete__title {
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
margin: 0 0 10px 0;
}
.flow-start__desc,
.flow-complete__desc {
font-size: 14px;
color: var(--text-tertiary);
margin: 0 0 20px 0;
line-height: 1.5;
}
/* ===== 底部联系客服浮动入口 ===== */
.flow-contact-fab {
position: fixed;
bottom: 24px;
right: 16px;
z-index: 100;
}
.flow-contact-fab :deep(.van-button) {
box-shadow: 0 2px 12px rgba(7, 193, 96, 0.3);
border-color: #07C160;
color: #07C160;
}
</style>
@@ -0,0 +1,406 @@
<!-- =============================================================================
// 企微IT智能服务台 — H5用户端排查模板列表页面(v5.4 增强)
// =============================================================================
// 说明:C-T5 流程图 H5 端展示
// 功能:
// 1. 展示可用的排查模板列表
// 2. 支持按分类筛选
// 3. 支持下拉刷新
// 4. 点击进入流程图详情页
// 5. "联系IT客服" 底部浮动入口
// ============================================================================= -->
<template>
<div class="troubleshooting-list">
<!-- 顶部标题 -->
<div class="troubleshooting-list__header">
<van-nav-bar
title="故障排查"
left-arrow
@click-left="goBack"
>
<template #right>
<van-icon
name="service-o"
size="20"
color="#07C160"
@click="goToChat"
class="header-contact-btn"
/>
</template>
</van-nav-bar>
</div>
<!-- 分类筛选 -->
<div class="troubleshooting-list__filter">
<van-tabs
v-model:active="activeCategory"
@change="onCategoryChange"
:color="'#07C160'"
:title-active-color="'#07C160'"
>
<van-tab title="全部" name="" />
<van-tab title="VPN" name="vpn" />
<van-tab title="邮箱" name="email" />
<van-tab title="系统" name="system" />
<van-tab title="账号" name="account" />
</van-tabs>
</div>
<!-- 下拉刷新容器 -->
<van-pull-refresh
v-model="refreshing"
@refresh="onRefresh"
:head-height="80"
pulling-text="下拉刷新"
loosing-text="释放刷新"
loading-text="加载中..."
>
<!-- 加载状态 -->
<div v-if="loading" class="troubleshooting-list__loading">
<van-loading type="spinner" color="#07C160" size="32" />
<span>加载中...</span>
</div>
<!-- 错误状态 -->
<div v-else-if="errorMsg" class="troubleshooting-list__error">
<van-empty :description="errorMsg">
<van-button type="primary" size="small" @click="loadTemplates">
重试
</van-button>
</van-empty>
</div>
<!-- 模板列表 -->
<div v-else class="troubleshooting-list__content">
<div class="template-grid">
<div
v-for="template in templates"
:key="template.id"
class="template-card"
@click="goToDetail(template)"
>
<div class="template-card__icon">
{{ getCategoryIcon(template.category) }}
</div>
<div class="template-card__info">
<h3 class="template-card__name">{{ template.name }}</h3>
<p class="template-card__desc">{{ getCategoryDesc(template.category) }}</p>
<div class="template-card__meta">
<span class="template-card__steps">
📋 {{ template.path_steps?.length || 0 }} 个步骤
</span>
</div>
</div>
<div class="template-card__arrow">
<van-icon name="arrow" />
</div>
</div>
</div>
<!-- 空状态 -->
<van-empty
v-if="templates.length === 0 && !loading"
description="暂无排查模板"
image="search"
/>
</div>
</van-pull-refresh>
<!-- 底部联系客服浮动入口 -->
<div class="troubleshooting-contact-fab">
<van-button
type="default"
size="small"
icon="service-o"
round
@click="goToChat"
>
联系IT客服
</van-button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import {
getTroubleshootingTemplates,
type TroubleshootingTemplate,
} from '@/api/troubleshooting-templates'
const router = useRouter()
// --------------------------------------------------------------------------
// 状态
// --------------------------------------------------------------------------
/** 当前选中的分类 */
const activeCategory = ref('')
/** 模板列表 */
const templates = ref<TroubleshootingTemplate[]>([])
/** 首次加载状态 */
const loading = ref(true)
/** 下拉刷新状态 */
const refreshing = ref(false)
/** 错误信息 */
const errorMsg = ref('')
// --------------------------------------------------------------------------
// 方法
// --------------------------------------------------------------------------
/**
* 返回上一页
*/
function goBack(): void {
router.back()
}
/**
* 分类变化时重新加载
*/
function onCategoryChange(): void {
templates.value = []
errorMsg.value = ''
loading.value = true
loadTemplates()
}
/**
* 下拉刷新
*/
async function onRefresh(): Promise<void> {
refreshing.value = true
try {
await loadTemplates()
} finally {
refreshing.value = false
}
}
/**
* 加载模板列表
*/
async function loadTemplates(): Promise<void> {
// 首次加载显示全屏 loading
if (templates.value.length === 0 && !refreshing.value) {
loading.value = true
errorMsg.value = ''
}
try {
const data = await getTroubleshootingTemplates({
category: activeCategory.value || undefined,
})
templates.value = data.items || []
} catch (error: any) {
console.error('[TroubleshootingList] 加载失败:', error)
// 只在无数据时显示错误页
if (templates.value.length === 0) {
errorMsg.value = error?.message || '加载失败,请稍后重试'
}
} finally {
loading.value = false
}
}
/**
* 获取分类图标
*/
function getCategoryIcon(category: string): string {
const iconMap: Record<string, string> = {
vpn: '🔐',
email: '📧',
system: '💻',
account: '👤',
}
return iconMap[category] || '🔧'
}
/**
* 获取分类描述
*/
function getCategoryDesc(category: string): string {
const descMap: Record<string, string> = {
vpn: 'VPN连接异常、无法访问内网',
email: '邮箱登录失败、收发信问题',
system: '系统登录异常、软件故障',
account: '账号锁定、权限申请',
}
return descMap[category] || '常见IT问题排查'
}
/**
* 跳转到模板详情页
*/
function goToDetail(template: TroubleshootingTemplate): void {
router.push({
name: 'TroubleshootingDetail',
params: { id: template.id },
query: { name: template.name },
})
}
/**
* 跳转到聊天页面联系客服
*/
function goToChat(): void {
router.push({ name: 'ChatView' })
}
// --------------------------------------------------------------------------
// 生命周期
// --------------------------------------------------------------------------
onMounted(() => {
loadTemplates()
})
</script>
<style scoped>
/* ===== 主容器 ===== */
.troubleshooting-list {
min-height: 100vh;
background: var(--bg-secondary);
padding-bottom: 80px;
}
/* ===== 顶部标题 ===== */
.troubleshooting-list__header {
background: var(--bg-primary);
}
.header-contact-btn {
cursor: pointer;
padding: 4px;
}
/* ===== 分类筛选 ===== */
.troubleshooting-list__filter {
background: var(--bg-primary);
position: sticky;
top: 0;
z-index: 10;
}
/* ===== 加载状态 ===== */
.troubleshooting-list__loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 0;
gap: 12px;
color: var(--text-tertiary);
font-size: 14px;
}
/* ===== 错误状态 ===== */
.troubleshooting-list__error {
padding: 60px 0;
}
/* ===== 内容区域 ===== */
.troubleshooting-list__content {
padding: 12px;
}
/* ===== 模板卡片网格 ===== */
.template-grid {
display: flex;
flex-direction: column;
gap: 10px;
}
/* ===== 模板卡片 ===== */
.template-card {
display: flex;
align-items: center;
gap: 12px;
padding: 14px;
background: var(--bg-primary);
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
cursor: pointer;
transition: all 0.2s;
}
.template-card:active {
background: var(--bg-secondary);
transform: scale(0.98);
}
.template-card__icon {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
background: var(--bg-secondary);
border-radius: 10px;
flex-shrink: 0;
}
.template-card__info {
flex: 1;
min-width: 0;
}
.template-card__name {
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
margin: 0 0 4px 0;
line-height: 1.4;
}
.template-card__desc {
font-size: 12px;
color: var(--text-tertiary);
margin: 0 0 4px 0;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.template-card__meta {
display: flex;
align-items: center;
gap: 8px;
}
.template-card__steps {
font-size: 11px;
color: var(--text-quaternary);
}
.template-card__arrow {
color: var(--text-tertiary);
font-size: 16px;
flex-shrink: 0;
}
/* ===== 底部联系客服浮动入口 ===== */
.troubleshooting-contact-fab {
position: fixed;
bottom: 24px;
right: 16px;
z-index: 100;
}
.troubleshooting-contact-fab :deep(.van-button) {
box-shadow: 0 2px 12px rgba(7, 193, 96, 0.3);
border-color: #07C160;
color: #07C160;
}
</style>