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:
@@ -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>
|
||||
Reference in New Issue
Block a user