2026-06-14 16:49:18 +08:00
|
|
|
|
<!--
|
|
|
|
|
|
=============================================================================
|
|
|
|
|
|
企微IT智能服务台 — 排查流程图管理页
|
|
|
|
|
|
=============================================================================
|
2026-06-16 14:30:09 +08:00
|
|
|
|
说明:JSON 导入导出 + 在线编辑 + 树形预览
|
|
|
|
|
|
阶段三实现 - 用户已选 B 方案(双栏 CodeMirror + vue-json-pretty)
|
|
|
|
|
|
功能:
|
|
|
|
|
|
- 列表(从 /api/troubleshooting-templates 拉取)
|
|
|
|
|
|
- 预览/编辑/删除(单条)
|
|
|
|
|
|
- 导入 JSON(文件)
|
|
|
|
|
|
- 导出全部(批量下载)
|
|
|
|
|
|
- 新建(空模板)
|
|
|
|
|
|
=============================================================================
|
2026-06-14 16:49:18 +08:00
|
|
|
|
-->
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="flowcharts-page">
|
|
|
|
|
|
<!-- 页面标题 -->
|
2026-06-16 14:30:09 +08:00
|
|
|
|
<div class="page-header">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="page-title">排查流程图管理</div>
|
|
|
|
|
|
<div class="page-desc">JSON 导入导出 + 在线编辑 + 树形预览。共 {{ flowcharts.length }} 套模板</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-06-14 16:49:18 +08:00
|
|
|
|
|
2026-06-16 14:30:09 +08:00
|
|
|
|
<!-- 操作按钮 -->
|
2026-06-14 16:49:18 +08:00
|
|
|
|
<div class="flowchart-actions">
|
2026-06-16 14:30:09 +08:00
|
|
|
|
<el-button type="primary" @click="handleImport">
|
2026-06-14 16:49:18 +08:00
|
|
|
|
<el-icon><Upload /></el-icon>
|
|
|
|
|
|
导入 JSON
|
|
|
|
|
|
</el-button>
|
2026-06-16 14:30:09 +08:00
|
|
|
|
<el-button @click="handleExportAll" :disabled="flowcharts.length === 0">
|
2026-06-14 16:49:18 +08:00
|
|
|
|
<el-icon><Download /></el-icon>
|
|
|
|
|
|
导出全部
|
|
|
|
|
|
</el-button>
|
2026-06-16 14:30:09 +08:00
|
|
|
|
<el-button @click="handleNew">
|
2026-06-14 16:49:18 +08:00
|
|
|
|
<el-icon><Plus /></el-icon>
|
|
|
|
|
|
新建流程图
|
|
|
|
|
|
</el-button>
|
2026-06-16 14:30:09 +08:00
|
|
|
|
<el-button @click="loadList" :loading="loading">
|
|
|
|
|
|
<el-icon><Refresh /></el-icon>
|
|
|
|
|
|
刷新
|
|
|
|
|
|
</el-button>
|
2026-06-14 16:49:18 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 流程图模板表格 -->
|
|
|
|
|
|
<div class="table-wrapper">
|
|
|
|
|
|
<el-table
|
2026-06-16 14:30:09 +08:00
|
|
|
|
v-loading="loading"
|
2026-06-14 16:49:18 +08:00
|
|
|
|
: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"
|
2026-06-16 14:30:09 +08:00
|
|
|
|
empty-text="暂无流程图,点击「新建流程图」开始"
|
2026-06-14 16:49:18 +08:00
|
|
|
|
>
|
2026-06-16 14:30:09 +08:00
|
|
|
|
<el-table-column label="流程图名称" min-width="220">
|
2026-06-14 16:49:18 +08:00
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<div class="flowchart-name">
|
|
|
|
|
|
<el-icon :size="16" style="color: var(--accent); margin-right: 6px">
|
|
|
|
|
|
<Share />
|
|
|
|
|
|
</el-icon>
|
2026-06-16 14:30:09 +08:00
|
|
|
|
<span>{{ row.name }}</span>
|
2026-06-14 16:49:18 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-06-16 14:30:09 +08:00
|
|
|
|
<el-table-column label="分类" width="100">
|
2026-06-14 16:49:18 +08:00
|
|
|
|
<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" />
|
2026-06-16 14:30:09 +08:00
|
|
|
|
<el-table-column label="预估时间" width="90" align="center">
|
|
|
|
|
|
<template #default="{ row }">{{ row.estimated_time ?? '-' }} 分钟</template>
|
2026-06-14 16:49:18 +08:00
|
|
|
|
</el-table-column>
|
2026-06-16 14:30:09 +08:00
|
|
|
|
<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>
|
2026-06-14 16:49:18 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-06-16 14:30:09 +08:00
|
|
|
|
<!-- 在线编辑器对话框 -->
|
|
|
|
|
|
<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"
|
|
|
|
|
|
/>
|
2026-06-14 16:49:18 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-06-16 14:30:09 +08:00
|
|
|
|
// =============================================================================
|
|
|
|
|
|
// 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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-14 16:49:18 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-06-16 14:30:09 +08:00
|
|
|
|
.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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-14 16:49:18 +08:00
|
|
|
|
.flowchart-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.flowchart-name {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-16 14:30:09 +08:00
|
|
|
|
.table-wrapper {
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
border-radius: var(--radius-lg);
|
|
|
|
|
|
padding: 4px;
|
2026-06-14 16:49:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
|
.flowchart-table-row:hover td {
|
|
|
|
|
|
background-color: var(--bg-tertiary) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|