Files
wecom_it_smart_desk/frontend-admin/src/views/Flowcharts.vue
T
Simon cec5607c45 feat(admin): Flowcharts.vue JSON 在线编辑 + 9 套排查模板种子数据
为管理后台'排查流程图'模块加 JSON 在线编辑能力 + 提供 9 套
办公 IT 常见故障排查模板种子数据(账号/系统/企微/VPN/邮箱/网络/
打印机/软件/硬件),管理员可基于此学习、筛选、修改、新增。

## 选型(按'优选开源'原则)
- @codemirror/lang-json / state / theme-one-dark / view
- codemirror(核心)
- vue-codemirror(Vue 3 集成)
- vue-json-pretty(JSON 树形预览)
全部为社区成熟开源组件,非自行开发

## 改动
- frontend-admin/package.json: 加 6 个 npm 依赖
- frontend-admin/src/api/troubleshooting.ts(新): TS 类型 +
  5 个 API client(listTemplates / getTemplate / createTemplate /
  updateTemplate / deleteTemplate) + formatJson/validateJson/
  countNodes/countDecisions 工具函数
- frontend-admin/src/components/flowchart/FlowchartEditorDialog.vue(新):
  双面板编辑器(左 CodeMirror + 右 vue-json-pretty),
  实时 JSON 校验 + 节点/决策统计 + 格式/复制/导出按钮
- frontend-admin/src/views/Flowcharts.vue(改): 列表 + 导入/导出/
  新建按钮 + EditorDialog 集成 + 文件上传 + 删除确认

## 9 套种子数据
- 01-account-password.json 账号密码
- 02-pc-system.json        电脑系统
- 03-wecom.json            企微问题
- 04-vpn.json              VPN 接入
- 05-email.json            邮箱
- 06-network.json          网络
- 07-printer.json          打印机
- 08-software.json         软件
- 09-hardware.json         硬件
每套 ~150-200 行,结构:name / category / description /
estimated_time / difficulty / tags / root_node(决策树)

## 工具脚本
- data/seed-templates/build_all.py: 合并 9 个 JSON 成 00-all.json
2026-06-16 14:30:09 +08:00

385 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
=============================================================================
企微IT智能服务台 排查流程图管理页
=============================================================================
说明JSON 导入导出 + 在线编辑 + 树形预览
阶段三实现 - 用户已选 B 方案(双栏 CodeMirror + vue-json-pretty)
功能:
- 列表( /api/troubleshooting-templates 拉取)
- 预览/编辑/删除(单条)
- 导入 JSON(文件)
- 导出全部(批量下载)
- 新建(空模板)
=============================================================================
-->
<template>
<div class="flowcharts-page">
<!-- 页面标题 -->
<div class="page-header">
<div>
<div class="page-title">排查流程图管理</div>
<div class="page-desc">JSON 导入导出 + 在线编辑 + 树形预览 {{ flowcharts.length }} 套模板</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="flowchart-actions">
<el-button type="primary" @click="handleImport">
<el-icon><Upload /></el-icon>
导入 JSON
</el-button>
<el-button @click="handleExportAll" :disabled="flowcharts.length === 0">
<el-icon><Download /></el-icon>
导出全部
</el-button>
<el-button @click="handleNew">
<el-icon><Plus /></el-icon>
新建流程图
</el-button>
<el-button @click="loadList" :loading="loading">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
<!-- 流程图模板表格 -->
<div class="table-wrapper">
<el-table
v-loading="loading"
:data="flowcharts"
style="width: 100%"
:header-cell-style="{ background: 'var(--bg-tertiary)', color: 'var(--text-secondary)', fontSize: '12px' }"
:cell-style="{ color: 'var(--text-primary)', fontSize: '13px' }"
row-class-name="flowchart-table-row"
empty-text="暂无流程图,点击新建流程图开始"
>
<el-table-column label="流程图名称" min-width="220">
<template #default="{ row }">
<div class="flowchart-name">
<el-icon :size="16" style="color: var(--accent); margin-right: 6px">
<Share />
</el-icon>
<span>{{ row.name }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="分类" width="100">
<template #default="{ row }">
<el-tag size="small" effect="plain">{{ row.category }}</el-tag>
</template>
</el-table-column>
<el-table-column label="节点数" width="80" align="center" prop="nodeCount" />
<el-table-column label="预估时间" width="90" align="center">
<template #default="{ row }">{{ row.estimated_time ?? '-' }} 分钟</template>
</el-table-column>
<el-table-column label="版本" width="70" align="center">
<template #default="{ row }">{{ row.version || 'v1.0' }}</template>
</el-table-column>
<el-table-column label="最后更新" width="120" align="center">
<template #default="{ row }">{{ formatDate(row.updated_at) }}</template>
</el-table-column>
<el-table-column label="操作" width="180" align="center" fixed="right">
<template #default="{ row }">
<el-button size="small" text type="primary" @click="handlePreview(row)">
<el-icon><View /></el-icon>
预览
</el-button>
<el-button size="small" text type="primary" @click="handleEdit(row)">
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-button size="small" text type="danger" @click="handleDelete(row)">
<el-icon><Delete /></el-icon>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 在线编辑器对话框 -->
<FlowchartEditorDialog
v-model="dialogVisible"
:template="currentTemplate"
:readonly="dialogMode === 'preview'"
:saving="saving"
@save="handleSave"
@cancel="handleDialogCancel"
/>
<!-- 隐藏的文件选择器(导入 JSON) -->
<input
ref="fileInputRef"
type="file"
accept=".json,application/json"
style="display: none"
@change="handleFileSelected"
/>
</div>
</template>
<script setup lang="ts">
// =============================================================================
// imports
// =============================================================================
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Upload, Download, Plus, Refresh,
Share, View, Edit, Delete,
} from '@element-plus/icons-vue'
import FlowchartEditorDialog from '@/components/flowchart/FlowchartEditorDialog.vue'
import {
listTemplates,
getTemplate,
createTemplate,
updateTemplate,
deleteTemplate,
formatJson,
countNodes,
type TroubleshootingTemplate,
} from '@/api/troubleshooting'
// =============================================================================
// 响应式状态
// =============================================================================
const flowcharts = ref<TroubleshootingTemplate[]>([])
const loading = ref<boolean>(false)
const saving = ref<boolean>(false)
const dialogVisible = ref<boolean>(false)
const dialogMode = ref<'preview' | 'edit' | 'create'>('preview')
const currentTemplate = ref<TroubleshootingTemplate | null>(null)
const fileInputRef = ref<HTMLInputElement | null>(null)
// =============================================================================
// 加载列表
// =============================================================================
async function loadList() {
loading.value = true
try {
const list = await listTemplates()
// 附加节点数(便于表格展示)
flowcharts.value = list.map((t) => ({
...t,
nodeCount: countNodes(t.root_node),
}))
} catch (e) {
ElMessage.error('加载流程图列表失败')
console.error(e)
} finally {
loading.value = false
}
}
onMounted(loadList)
// =============================================================================
// 操作
// =============================================================================
// 预览
async function handlePreview(row: TroubleshootingTemplate) {
try {
// 重新拉详情(确保数据最新)
const tpl = await getTemplate(row.id!)
currentTemplate.value = tpl
dialogMode.value = 'preview'
dialogVisible.value = true
} catch {
// 拉失败就用列表里那条
currentTemplate.value = row
dialogMode.value = 'preview'
dialogVisible.value = true
}
}
// 编辑
async function handleEdit(row: TroubleshootingTemplate) {
try {
const tpl = await getTemplate(row.id!)
currentTemplate.value = tpl
dialogMode.value = 'edit'
dialogVisible.value = true
} catch {
currentTemplate.value = row
dialogMode.value = 'edit'
dialogVisible.value = true
}
}
// 新建
function handleNew() {
currentTemplate.value = null
dialogMode.value = 'create'
dialogVisible.value = true
}
// 删除
async function handleDelete(row: TroubleshootingTemplate) {
try {
await ElMessageBox.confirm(
`确定要删除流程图「${row.name}」吗?此操作不可恢复。`,
'删除确认',
{
type: 'warning',
confirmButtonText: '删除',
cancelButtonText: '取消',
confirmButtonClass: 'el-button--danger',
}
)
} catch {
return // 用户取消
}
try {
await deleteTemplate(row.id!)
ElMessage.success('已删除')
await loadList()
} catch (e) {
ElMessage.error('删除失败')
console.error(e)
}
}
// 保存
async function handleSave(tpl: TroubleshootingTemplate) {
saving.value = true
try {
if (dialogMode.value === 'create' || !tpl.id) {
await createTemplate(tpl)
ElMessage.success('创建成功')
} else {
await updateTemplate(tpl.id, tpl)
ElMessage.success('更新成功')
}
dialogVisible.value = false
await loadList()
} catch (e) {
ElMessage.error('保存失败,请检查 JSON 格式')
console.error(e)
} finally {
saving.value = false
}
}
// 取消
function handleDialogCancel() {
dialogVisible.value = false
}
// =============================================================================
// 导入 / 导出
// =============================================================================
function handleImport() {
fileInputRef.value?.click()
}
async function handleFileSelected(event: Event) {
const target = event.target as HTMLInputElement
const file = target.files?.[0]
if (!file) return
try {
const text = await file.text()
const result = JSON.parse(text) as TroubleshootingTemplate
// 简单校验
if (!result.name || !result.category || !result.root_node) {
throw new Error('JSON 缺少必要字段(name/category/root_node)')
}
// 把导入的 JSON 当作"新建"打开,让用户确认/编辑
currentTemplate.value = result
dialogMode.value = 'create'
dialogVisible.value = true
ElMessage.success('JSON 解析成功,请确认后保存')
} catch (e) {
const err = e as Error
ElMessage.error(`JSON 解析失败: ${err.message}`)
} finally {
// 清 input 以便下次能选同一文件
target.value = ''
}
}
function handleExportAll() {
const data = flowcharts.value.map((t) => ({
...t,
// 去掉统计字段,只导出核心数据
nodeCount: undefined,
}))
const json = formatJson(data)
const blob = new Blob([json], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `troubleshooting-templates-all-${Date.now()}.json`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
ElMessage.success(`已导出 ${data.length} 条流程图`)
}
// =============================================================================
// 工具
// =============================================================================
function formatDate(iso?: string): string {
if (!iso) return '-'
try {
return new Date(iso).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).replace(/\//g, '-')
} catch {
return iso
}
}
</script>
<style scoped>
.page-header {
margin-bottom: 20px;
}
.page-title {
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
}
.page-desc {
font-size: 13px;
color: var(--text-secondary);
margin-top: 4px;
}
.flowchart-actions {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.flowchart-name {
display: flex;
align-items: center;
font-weight: 500;
}
.table-wrapper {
background: white;
border-radius: var(--radius-lg);
padding: 4px;
}
</style>
<style>
.flowchart-table-row:hover td {
background-color: var(--bg-tertiary) !important;
}
</style>