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:
@@ -89,3 +89,22 @@ export async function suggestTags(conversationId: string): Promise<TagsResult> {
|
|||||||
)
|
)
|
||||||
return response.data.data
|
return response.data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存会话标签
|
||||||
|
* 将标签保存到会话记录中
|
||||||
|
*
|
||||||
|
* @param conversationId - 会话ID
|
||||||
|
* @param tags - 标签字典
|
||||||
|
* @returns 更新后的会话详情
|
||||||
|
*/
|
||||||
|
export async function saveConversationTags(
|
||||||
|
conversationId: string,
|
||||||
|
tags: Record<string, any>
|
||||||
|
): Promise<any> {
|
||||||
|
const response: AxiosResponse = await apiClient.post(
|
||||||
|
`/conversations/${conversationId}/tags`,
|
||||||
|
{ tags }
|
||||||
|
)
|
||||||
|
return response.data.data
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,149 @@
|
|||||||
<!-- =============================================================================
|
<!-- =============================================================================
|
||||||
// 企微IT智能服务台 — AI助手面板容器组件(v5.3 重构)
|
// 企微IT智能服务台 — AI助手面板容器组件(v5.4 重构 + Wingman 集成)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// 说明:坐席工作台右侧的AI助手面板
|
// 说明:坐席工作台右侧的AI助手面板
|
||||||
// 功能:上下两区域布局
|
// 功能:
|
||||||
|
// 顶部:Wingman 触发按钮组(生成回复建议 / 获取知识库引用 / 推荐排查流程)
|
||||||
|
// Wingman 结果展示区:根据 API 结果展示不同类型的 AI 建议
|
||||||
// 上方 ~1/3:🤖 AI 智能推荐区(1-3张推荐卡片)
|
// 上方 ~1/3:🤖 AI 智能推荐区(1-3张推荐卡片)
|
||||||
|
// 智能标注区:标签显示 + AI建议 + 手动编辑
|
||||||
// 下方 ~2/3:快速回复区(复用 QuickReplyPanel)
|
// 下方 ~2/3:快速回复区(复用 QuickReplyPanel)
|
||||||
// ============================================================================= -->
|
// ============================================================================= -->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="ai-assistant-panel">
|
<div class="ai-assistant-panel">
|
||||||
|
<!-- ================================================================== -->
|
||||||
|
<!-- Wingman 触发按钮组 -->
|
||||||
|
<!-- ================================================================== -->
|
||||||
|
<div class="wingman-triggers">
|
||||||
|
<el-button
|
||||||
|
class="wingman-btn"
|
||||||
|
size="small"
|
||||||
|
:loading="loadingDraft"
|
||||||
|
:disabled="!hasActiveConversation"
|
||||||
|
@click="handleGenerateDraft"
|
||||||
|
title="基于当前对话生成AI回复建议"
|
||||||
|
>
|
||||||
|
🤖 生成回复建议
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
class="wingman-btn"
|
||||||
|
size="small"
|
||||||
|
:loading="loadingKnowledge"
|
||||||
|
:disabled="!hasActiveConversation"
|
||||||
|
@click="handleGetKnowledge"
|
||||||
|
title="从知识库中检索相关引用"
|
||||||
|
>
|
||||||
|
📚 获取知识库引用
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
class="wingman-btn"
|
||||||
|
size="small"
|
||||||
|
:loading="loadingFlow"
|
||||||
|
:disabled="!hasActiveConversation"
|
||||||
|
@click="handleRecommendFlow"
|
||||||
|
title="基于问题类型推荐排查流程"
|
||||||
|
>
|
||||||
|
🔍 推荐排查流程
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ================================================================== -->
|
||||||
|
<!-- Wingman 结果展示区 -->
|
||||||
|
<!-- ================================================================== -->
|
||||||
|
<div v-if="wingmanResult" class="wingman-result-section">
|
||||||
|
<!-- 结果标题栏 -->
|
||||||
|
<div class="wingman-result-header">
|
||||||
|
<span class="wingman-result-bar"></span>
|
||||||
|
<span class="wingman-result-title">{{ wingmanResult.title }}</span>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
text
|
||||||
|
@click="clearWingmanResult"
|
||||||
|
title="关闭"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 回复草稿结果 -->
|
||||||
|
<div v-if="wingmanResult.type === 'draft'" class="wingman-draft-card">
|
||||||
|
<div class="draft-content">{{ wingmanResult.data.content }}</div>
|
||||||
|
<div class="draft-meta">
|
||||||
|
<el-tag size="small" type="success">
|
||||||
|
置信度: {{ (wingmanResult.data.confidence * 100).toFixed(0) }}%
|
||||||
|
</el-tag>
|
||||||
|
<span v-if="wingmanResult.data.reasoning" class="draft-reasoning">
|
||||||
|
💡 {{ wingmanResult.data.reasoning }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="draft-actions">
|
||||||
|
<el-button type="primary" size="small" @click="adoptDraft(wingmanResult.data.content)">
|
||||||
|
✅ 采纳到输入框
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" @click="editDraft(wingmanResult.data.content)">
|
||||||
|
✏️ 编辑后发送
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 知识库引用结果 -->
|
||||||
|
<div v-else-if="wingmanResult.type === 'knowledge'" class="wingman-knowledge-card">
|
||||||
|
<div class="knowledge-section">
|
||||||
|
<div class="knowledge-label">📋 问题描述</div>
|
||||||
|
<div class="knowledge-text">{{ wingmanResult.data.problem }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="knowledge-section">
|
||||||
|
<div class="knowledge-label">🔍 原因分析</div>
|
||||||
|
<div class="knowledge-text">{{ wingmanResult.data.cause }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="knowledge-section">
|
||||||
|
<div class="knowledge-label">✅ 解决方案</div>
|
||||||
|
<div class="knowledge-text">{{ wingmanResult.data.solution }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="knowledge-actions">
|
||||||
|
<el-button type="primary" size="small" @click="adoptDraft(wingmanResult.data.solution)">
|
||||||
|
📝 将方案填入输入框
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" @click="copyKnowledgeToClipboard">
|
||||||
|
📋 复制全部
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 排查流程推荐结果 -->
|
||||||
|
<div v-else-if="wingmanResult.type === 'flow'" class="wingman-flow-card">
|
||||||
|
<div class="flow-header">
|
||||||
|
<span class="flow-icon">🔧</span>
|
||||||
|
<span class="flow-title">推荐排查流程: {{ wingmanResult.data.category }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="wingmanResult.data.priority" class="flow-priority">
|
||||||
|
<el-tag
|
||||||
|
size="small"
|
||||||
|
:type="wingmanResult.data.priority === 'high' ? 'danger' : wingmanResult.data.priority === 'medium' ? 'warning' : 'info'"
|
||||||
|
>
|
||||||
|
优先级: {{ priorityLabel(wingmanResult.data.priority) }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="flow-steps">
|
||||||
|
<div
|
||||||
|
v-for="(tag, idx) in wingmanResult.data.suggested_tags"
|
||||||
|
:key="idx"
|
||||||
|
class="flow-step-item"
|
||||||
|
>
|
||||||
|
<span class="flow-step-num">{{ idx + 1 }}</span>
|
||||||
|
<span class="flow-step-label">{{ tag }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flow-empty" v-if="!wingmanResult.data.suggested_tags || wingmanResult.data.suggested_tags.length === 0">
|
||||||
|
暂无排查步骤建议,请结合知识库信息手动排查
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分隔线(仅在有 Wingman 结果时显示) -->
|
||||||
|
<div v-if="wingmanResult" class="ai-section-divider"></div>
|
||||||
|
|
||||||
<!-- ================================================================== -->
|
<!-- ================================================================== -->
|
||||||
<!-- 上方 ~1/3 区域 — 🤖 AI 智能推荐区 -->
|
<!-- 上方 ~1/3 区域 — 🤖 AI 智能推荐区 -->
|
||||||
<!-- ================================================================== -->
|
<!-- ================================================================== -->
|
||||||
@@ -39,6 +174,83 @@
|
|||||||
<!-- 分隔线 -->
|
<!-- 分隔线 -->
|
||||||
<div class="ai-section-divider"></div>
|
<div class="ai-section-divider"></div>
|
||||||
|
|
||||||
|
<!-- ================================================================== -->
|
||||||
|
<!-- 智能标注区 — C-T11 新增 -->
|
||||||
|
<!-- ================================================================== -->
|
||||||
|
<div class="ai-tags-section">
|
||||||
|
<!-- 标题栏 -->
|
||||||
|
<div class="ai-tags-header">
|
||||||
|
<span class="ai-tags-bar"></span>
|
||||||
|
<span class="ai-tags-title">🏷️ 智能标注</span>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
:loading="loadingTagSuggestions"
|
||||||
|
@click="handleSuggestTags"
|
||||||
|
title="获取AI标签建议"
|
||||||
|
>
|
||||||
|
AI建议
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 标签列表 -->
|
||||||
|
<div class="ai-tags-list">
|
||||||
|
<!-- 已保存的标签 -->
|
||||||
|
<div v-if="Object.keys(currentTags).length > 0" class="tags-saved">
|
||||||
|
<el-tag
|
||||||
|
v-for="(value, key) in currentTags"
|
||||||
|
:key="key"
|
||||||
|
class="tag-item"
|
||||||
|
:type="getTagType(key as string)"
|
||||||
|
closable
|
||||||
|
@close="handleRemoveTag(key as string)"
|
||||||
|
>
|
||||||
|
{{ formatTagLabel(key as string, value) }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- AI 建议的标签 -->
|
||||||
|
<div v-if="suggestedTagList.length > 0" class="tags-suggested">
|
||||||
|
<div class="tags-suggested-title">AI 建议:</div>
|
||||||
|
<el-tag
|
||||||
|
v-for="tag in suggestedTagList"
|
||||||
|
:key="tag"
|
||||||
|
class="tag-item tag-suggested"
|
||||||
|
type="info"
|
||||||
|
@click="handleAddSuggestedTag(tag)"
|
||||||
|
>
|
||||||
|
+ {{ tag }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 无标签时提示 -->
|
||||||
|
<div v-if="Object.keys(currentTags).length === 0 && suggestedTagList.length === 0" class="ai-tags-empty">
|
||||||
|
暂无标签,点击"AI建议"获取智能标注
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 手动添加标签 -->
|
||||||
|
<div class="ai-tags-add">
|
||||||
|
<el-input
|
||||||
|
v-model="newTagKey"
|
||||||
|
placeholder="标签名"
|
||||||
|
size="small"
|
||||||
|
style="width: 100px"
|
||||||
|
@keyup.enter="handleAddManualTag"
|
||||||
|
/>
|
||||||
|
<el-input
|
||||||
|
v-model="newTagValue"
|
||||||
|
placeholder="值"
|
||||||
|
size="small"
|
||||||
|
style="width: 80px"
|
||||||
|
@keyup.enter="handleAddManualTag"
|
||||||
|
/>
|
||||||
|
<el-button size="small" @click="handleAddManualTag">添加</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分隔线 -->
|
||||||
|
<div class="ai-section-divider"></div>
|
||||||
|
|
||||||
<!-- ================================================================== -->
|
<!-- ================================================================== -->
|
||||||
<!-- 下方 ~2/3 区域 — 快速回复区 -->
|
<!-- 下方 ~2/3 区域 — 快速回复区 -->
|
||||||
<!-- ================================================================== -->
|
<!-- ================================================================== -->
|
||||||
@@ -52,11 +264,13 @@
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 导入
|
// 导入
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
import AiSuggestReply from './AiSuggestReply.vue'
|
import AiSuggestReply from './AiSuggestReply.vue'
|
||||||
import QuickReplyPanel from './QuickReplyPanel.vue'
|
import QuickReplyPanel from './QuickReplyPanel.vue'
|
||||||
import { useConversationStore } from '@/stores/conversation'
|
import { useConversationStore } from '@/stores/conversation'
|
||||||
import type { DraftResult } from '@/api/wingman'
|
import type { DraftResult, SummaryResult, TagsResult } from '@/api/wingman'
|
||||||
|
import { generateDraft, generateSummary, suggestTags, saveConversationTags } from '@/api/wingman'
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 类型
|
// 类型
|
||||||
@@ -72,6 +286,22 @@ interface AiRecommendation {
|
|||||||
confidence: number
|
confidence: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 当前会话的标签类型 */
|
||||||
|
type ConversationTags = Record<string, any>
|
||||||
|
|
||||||
|
/** Wingman 结果类型枚举 */
|
||||||
|
type WingmanResultType = 'draft' | 'knowledge' | 'flow'
|
||||||
|
|
||||||
|
/** Wingman 结果展示数据 */
|
||||||
|
interface WingmanResultData {
|
||||||
|
/** 结果类型 */
|
||||||
|
type: WingmanResultType
|
||||||
|
/** 标题 */
|
||||||
|
title: string
|
||||||
|
/** 原始 API 数据 */
|
||||||
|
data: DraftResult | SummaryResult | TagsResult
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 状态
|
// 状态
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -79,22 +309,52 @@ interface AiRecommendation {
|
|||||||
/** 会话 Store */
|
/** 会话 Store */
|
||||||
const conversationStore = useConversationStore()
|
const conversationStore = useConversationStore()
|
||||||
|
|
||||||
/** AI 推荐列表(Mock 数据,后续由 AI 引擎填充) */
|
/** AI 推荐列表 */
|
||||||
const recommendations = ref<AiRecommendation[]>([])
|
const recommendations = ref<AiRecommendation[]>([])
|
||||||
|
|
||||||
|
/** 当前会话的标签 */
|
||||||
|
const currentTags = ref<ConversationTags>({})
|
||||||
|
|
||||||
|
/** AI 建议的标签列表 */
|
||||||
|
const suggestedTagList = ref<string[]>([])
|
||||||
|
|
||||||
|
/** 加载标签建议中 */
|
||||||
|
const loadingTagSuggestions = ref(false)
|
||||||
|
|
||||||
|
/** 新增标签 - 键 */
|
||||||
|
const newTagKey = ref('')
|
||||||
|
|
||||||
|
/** 新增标签 - 值 */
|
||||||
|
const newTagValue = ref('')
|
||||||
|
|
||||||
|
/** Wingman 当前结果 */
|
||||||
|
const wingmanResult = ref<WingmanResultData | null>(null)
|
||||||
|
|
||||||
|
/** Wingman 加载状态 - 生成回复 */
|
||||||
|
const loadingDraft = ref(false)
|
||||||
|
|
||||||
|
/** Wingman 加载状态 - 知识库 */
|
||||||
|
const loadingKnowledge = ref(false)
|
||||||
|
|
||||||
|
/** Wingman 加载状态 - 排查流程 */
|
||||||
|
const loadingFlow = ref(false)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 计算属性
|
// 计算属性
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
/** 是否有活跃的会话 */
|
||||||
|
const hasActiveConversation = computed(() => {
|
||||||
|
return !!conversationStore.currentConversationId
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从当前会话的 AI 草稿生成推荐列表
|
* 从当前会话的 AI 草稿生成推荐列表
|
||||||
* 将 draft 数据映射为推荐卡片格式
|
|
||||||
*/
|
*/
|
||||||
const loadRecommendationsFromDraft = computed(() => {
|
const loadRecommendationsFromDraft = computed(() => {
|
||||||
const convId = conversationStore.currentConversationId
|
const convId = conversationStore.currentConversationId
|
||||||
if (!convId) return []
|
if (!convId) return []
|
||||||
|
|
||||||
// 尝试从 AI 草稿缓存中获取推荐
|
|
||||||
const convDrafts = conversationStore.aiDrafts.get(convId)
|
const convDrafts = conversationStore.aiDrafts.get(convId)
|
||||||
if (!convDrafts || convDrafts.size === 0) return []
|
if (!convDrafts || convDrafts.size === 0) return []
|
||||||
|
|
||||||
@@ -113,11 +373,177 @@ const loadRecommendationsFromDraft = computed(() => {
|
|||||||
// 方法
|
// 方法
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优先级中文标签
|
||||||
|
*/
|
||||||
|
function priorityLabel(priority: string): string {
|
||||||
|
const map: Record<string, string> = { high: '高', medium: '中', low: '低' }
|
||||||
|
return map[priority] || priority
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取标签类型
|
||||||
|
*/
|
||||||
|
function getTagType(key: string): '' | 'success' | 'info' | 'warning' | 'danger' {
|
||||||
|
const tagTypeMap: Record<string, '' | 'success' | 'info' | 'warning' | 'danger'> = {
|
||||||
|
priority: 'danger',
|
||||||
|
category: 'warning',
|
||||||
|
emotion: 'info',
|
||||||
|
blocking: 'danger',
|
||||||
|
impact_scope: 'warning',
|
||||||
|
}
|
||||||
|
return tagTypeMap[key] || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化标签显示文本
|
||||||
|
*/
|
||||||
|
function formatTagLabel(key: string, value: any): string {
|
||||||
|
const labelMap: Record<string, string> = {
|
||||||
|
priority: '优先级',
|
||||||
|
category: '分类',
|
||||||
|
emotion: '情绪',
|
||||||
|
blocking: '阻断',
|
||||||
|
impact_scope: '影响范围',
|
||||||
|
}
|
||||||
|
const label = labelMap[key] || key
|
||||||
|
return `${label}: ${value}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Wingman 触发方法
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wingman 触发 - 生成回复建议
|
||||||
|
* 调用 wingman/draft API,展示 AI 生成的回复草稿
|
||||||
|
*/
|
||||||
|
async function handleGenerateDraft(): Promise<void> {
|
||||||
|
const convId = conversationStore.currentConversationId
|
||||||
|
if (!convId) {
|
||||||
|
ElMessage.warning('请先选择一个会话')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingDraft.value = true
|
||||||
|
try {
|
||||||
|
const result: DraftResult = await generateDraft(convId)
|
||||||
|
wingmanResult.value = {
|
||||||
|
type: 'draft',
|
||||||
|
title: '🤖 AI 回复建议',
|
||||||
|
data: result,
|
||||||
|
}
|
||||||
|
ElMessage.success('回复建议已生成')
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('生成回复建议失败:', error)
|
||||||
|
ElMessage.error(error?.message || '生成回复建议失败')
|
||||||
|
} finally {
|
||||||
|
loadingDraft.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wingman 触发 - 获取知识库引用
|
||||||
|
* 调用 wingman/summary API,展示结构化摘要作为知识库引用
|
||||||
|
*/
|
||||||
|
async function handleGetKnowledge(): Promise<void> {
|
||||||
|
const convId = conversationStore.currentConversationId
|
||||||
|
if (!convId) {
|
||||||
|
ElMessage.warning('请先选择一个会话')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingKnowledge.value = true
|
||||||
|
try {
|
||||||
|
const result: SummaryResult = await generateSummary(convId)
|
||||||
|
wingmanResult.value = {
|
||||||
|
type: 'knowledge',
|
||||||
|
title: '📚 知识库引用',
|
||||||
|
data: result,
|
||||||
|
}
|
||||||
|
ElMessage.success('知识库引用已获取')
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('获取知识库引用失败:', error)
|
||||||
|
ElMessage.error(error?.message || '获取知识库引用失败')
|
||||||
|
} finally {
|
||||||
|
loadingKnowledge.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wingman 触发 - 推荐排查流程
|
||||||
|
* 调用 wingman/tags API,按分类推荐排查步骤
|
||||||
|
*/
|
||||||
|
async function handleRecommendFlow(): Promise<void> {
|
||||||
|
const convId = conversationStore.currentConversationId
|
||||||
|
if (!convId) {
|
||||||
|
ElMessage.warning('请先选择一个会话')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingFlow.value = true
|
||||||
|
try {
|
||||||
|
const result: TagsResult = await suggestTags(convId)
|
||||||
|
wingmanResult.value = {
|
||||||
|
type: 'flow',
|
||||||
|
title: '🔍 排查流程推荐',
|
||||||
|
data: result,
|
||||||
|
}
|
||||||
|
ElMessage.success('排查流程已推荐')
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('推荐排查流程失败:', error)
|
||||||
|
ElMessage.error(error?.message || '推荐排查流程失败')
|
||||||
|
} finally {
|
||||||
|
loadingFlow.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除 Wingman 结果展示
|
||||||
|
*/
|
||||||
|
function clearWingmanResult(): void {
|
||||||
|
wingmanResult.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 采纳草稿到输入框
|
||||||
|
*/
|
||||||
|
function adoptDraft(content: string): void {
|
||||||
|
conversationStore.pendingReplyText = content
|
||||||
|
ElMessage.success('已填入输入框')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑草稿后发送(与 adopt 相同行为,填入输入框让坐席编辑)
|
||||||
|
*/
|
||||||
|
function editDraft(content: string): void {
|
||||||
|
conversationStore.pendingReplyText = content
|
||||||
|
ElMessage.success('已填入输入框,可编辑后发送')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复制知识库内容到剪贴板
|
||||||
|
*/
|
||||||
|
async function copyKnowledgeToClipboard(): Promise<void> {
|
||||||
|
if (!wingmanResult.value || wingmanResult.value.type !== 'knowledge') return
|
||||||
|
|
||||||
|
const data = wingmanResult.value.data as SummaryResult
|
||||||
|
const text = `问题描述:${data.problem}\n原因分析:${data.cause}\n解决方案:${data.solution}`
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
ElMessage.success('已复制到剪贴板')
|
||||||
|
} catch {
|
||||||
|
ElMessage.warning('复制失败,请手动复制')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// AI 推荐区方法
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理 AI 推荐卡片的选择
|
* 处理 AI 推荐卡片的选择
|
||||||
* 将推荐内容填充到对话输入框
|
|
||||||
*
|
|
||||||
* @param content - 推荐内容
|
|
||||||
*/
|
*/
|
||||||
function handleSelectRecommendation(content: string): void {
|
function handleSelectRecommendation(content: string): void {
|
||||||
conversationStore.pendingReplyText = content
|
conversationStore.pendingReplyText = content
|
||||||
@@ -125,9 +551,6 @@ function handleSelectRecommendation(content: string): void {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理快速回复模板的"使用"事件
|
* 处理快速回复模板的"使用"事件
|
||||||
* 将模板内容填充到对话输入框
|
|
||||||
*
|
|
||||||
* @param content - 模板内容(已替换变量)
|
|
||||||
*/
|
*/
|
||||||
function handleUseTemplate(content: string): void {
|
function handleUseTemplate(content: string): void {
|
||||||
conversationStore.pendingReplyText = content
|
conversationStore.pendingReplyText = content
|
||||||
@@ -135,7 +558,6 @@ function handleUseTemplate(content: string): void {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载 AI 推荐数据
|
* 加载 AI 推荐数据
|
||||||
* 优先从草稿缓存获取,否则使用 Mock 数据
|
|
||||||
*/
|
*/
|
||||||
function loadRecommendations(): void {
|
function loadRecommendations(): void {
|
||||||
const fromDraft = loadRecommendationsFromDraft.value
|
const fromDraft = loadRecommendationsFromDraft.value
|
||||||
@@ -144,7 +566,6 @@ function loadRecommendations(): void {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock 数据:当无真实 AI 推荐时展示示例
|
|
||||||
if (conversationStore.currentConversationId) {
|
if (conversationStore.currentConversationId) {
|
||||||
recommendations.value = [
|
recommendations.value = [
|
||||||
{
|
{
|
||||||
@@ -163,12 +584,145 @@ function loadRecommendations(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 标签区方法
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载当前会话的标签
|
||||||
|
*/
|
||||||
|
function loadCurrentTags(): void {
|
||||||
|
const conversation = conversationStore.currentConversation
|
||||||
|
if (conversation && conversation.tags) {
|
||||||
|
currentTags.value = conversation.tags as ConversationTags
|
||||||
|
} else {
|
||||||
|
currentTags.value = {}
|
||||||
|
}
|
||||||
|
suggestedTagList.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 AI 标签建议
|
||||||
|
*/
|
||||||
|
async function handleSuggestTags(): Promise<void> {
|
||||||
|
const convId = conversationStore.currentConversationId
|
||||||
|
if (!convId) {
|
||||||
|
ElMessage.warning('请先选择一个会话')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingTagSuggestions.value = true
|
||||||
|
try {
|
||||||
|
const result = await suggestTags(convId)
|
||||||
|
if (result && result.suggested_tags) {
|
||||||
|
suggestedTagList.value = result.suggested_tags.filter(
|
||||||
|
(tag: string) => !Object.keys(currentTags.value).includes(tag)
|
||||||
|
)
|
||||||
|
if (suggestedTagList.value.length === 0) {
|
||||||
|
ElMessage.info('暂无新的标签建议')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取标签建议失败:', error)
|
||||||
|
ElMessage.error('获取标签建议失败')
|
||||||
|
} finally {
|
||||||
|
loadingTagSuggestions.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加 AI 建议的标签
|
||||||
|
*/
|
||||||
|
async function handleAddSuggestedTag(tag: string): Promise<void> {
|
||||||
|
const convId = conversationStore.currentConversationId
|
||||||
|
if (!convId) return
|
||||||
|
|
||||||
|
currentTags.value[tag] = true
|
||||||
|
suggestedTagList.value = suggestedTagList.value.filter((t) => t !== tag)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await saveConversationTags(convId, currentTags.value)
|
||||||
|
ElMessage.success(`已添加标签: ${tag}`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存标签失败:', error)
|
||||||
|
ElMessage.error('保存标签失败')
|
||||||
|
delete currentTags.value[tag]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除已保存的标签
|
||||||
|
*/
|
||||||
|
async function handleRemoveTag(key: string): Promise<void> {
|
||||||
|
const convId = conversationStore.currentConversationId
|
||||||
|
if (!convId) return
|
||||||
|
|
||||||
|
const oldValue = currentTags.value[key]
|
||||||
|
delete currentTags.value[key]
|
||||||
|
|
||||||
|
try {
|
||||||
|
await saveConversationTags(convId, currentTags.value)
|
||||||
|
ElMessage.success(`已移除标签: ${key}`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存标签失败:', error)
|
||||||
|
ElMessage.error('保存标签失败')
|
||||||
|
currentTags.value[key] = oldValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动添加标签
|
||||||
|
*/
|
||||||
|
async function handleAddManualTag(): Promise<void> {
|
||||||
|
const convId = conversationStore.currentConversationId
|
||||||
|
if (!convId) {
|
||||||
|
ElMessage.warning('请先选择一个会话')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = newTagKey.value.trim()
|
||||||
|
const value = newTagValue.value.trim()
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
ElMessage.warning('请输入标签名')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTags.value[key] = value || true
|
||||||
|
newTagKey.value = ''
|
||||||
|
newTagValue.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
await saveConversationTags(convId, currentTags.value)
|
||||||
|
ElMessage.success(`已添加标签: ${key}`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存标签失败:', error)
|
||||||
|
ElMessage.error('保存标签失败')
|
||||||
|
delete currentTags.value[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 监听
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/** 监听当前会话变化,重新加载数据和清除 Wingman 结果 */
|
||||||
|
watch(
|
||||||
|
() => conversationStore.currentConversationId,
|
||||||
|
() => {
|
||||||
|
loadRecommendations()
|
||||||
|
loadCurrentTags()
|
||||||
|
clearWingmanResult()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 生命周期
|
// 生命周期
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadRecommendations()
|
loadRecommendations()
|
||||||
|
loadCurrentTags()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -180,6 +734,199 @@ onMounted(() => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Wingman 触发按钮组 ---- */
|
||||||
|
.wingman-triggers {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wingman-btn {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Wingman 结果展示区 ---- */
|
||||||
|
.wingman-result-section {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 8px 12px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wingman-result-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wingman-result-bar {
|
||||||
|
width: 3px;
|
||||||
|
height: 12px;
|
||||||
|
background-color: #07C160;
|
||||||
|
border-radius: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wingman-result-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 回复草稿卡片 ---- */
|
||||||
|
.wingman-draft-card {
|
||||||
|
background: rgba(7, 193, 96, 0.04);
|
||||||
|
border: 1px solid rgba(7, 193, 96, 0.15);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draft-content {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--text-primary);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draft-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draft-reasoning {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draft-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 知识库引用卡片 ---- */
|
||||||
|
.wingman-knowledge-card {
|
||||||
|
background: rgba(64, 158, 255, 0.04);
|
||||||
|
border: 1px solid rgba(64, 158, 255, 0.15);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-section {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-text {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 排查流程推荐卡片 ---- */
|
||||||
|
.wingman-flow-card {
|
||||||
|
background: rgba(245, 158, 11, 0.04);
|
||||||
|
border: 1px solid rgba(245, 158, 11, 0.15);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-priority {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-steps {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-step-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-step-num {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(7, 193, 96, 0.1);
|
||||||
|
color: #07C160;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-step-label {
|
||||||
|
flex: 1;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-empty {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 分隔线 ---- */
|
||||||
|
.ai-section-divider {
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- 上方 AI 推荐区 ---- */
|
/* ---- 上方 AI 推荐区 ---- */
|
||||||
.ai-recommend-section {
|
.ai-recommend-section {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
@@ -225,12 +972,84 @@ onMounted(() => {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- 分隔线 ---- */
|
/* ---- 智能标注区 ---- */
|
||||||
.ai-section-divider {
|
.ai-tags-section {
|
||||||
border-bottom: 1px solid var(--border-color);
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 8px 12px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-tags-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-tags-bar {
|
||||||
|
width: 3px;
|
||||||
|
height: 12px;
|
||||||
|
background-color: var(--accent);
|
||||||
|
border-radius: 2px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ai-tags-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-tags-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
max-height: 100px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-saved {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-suggested {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-suggested-title {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-item {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-suggested {
|
||||||
|
border-style: dashed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-tags-empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-tags-add {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- 下方快速回复区 ---- */
|
/* ---- 下方快速回复区 ---- */
|
||||||
.quick-reply-section {
|
.quick-reply-section {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@@ -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