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
This commit is contained in:
@@ -0,0 +1,109 @@
|
|||||||
|
{
|
||||||
|
"name": "账号密码 / SSO 登录故障排查",
|
||||||
|
"category": "account",
|
||||||
|
"description": "员工忘记密码、账号被锁、SSO 单点登录失败、AD 域账号同步异常",
|
||||||
|
"estimated_time": 6,
|
||||||
|
"difficulty": 1,
|
||||||
|
"tags": ["账号", "密码", "SSO", "AD域", "登录"],
|
||||||
|
"root_node": {
|
||||||
|
"id": "fc-acct-1",
|
||||||
|
"type": "step",
|
||||||
|
"label": "确认员工使用哪种登录方式(域账号/企微SSO/邮箱SSO)",
|
||||||
|
"status": "pending",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-acct-2",
|
||||||
|
"type": "decision",
|
||||||
|
"label": "是否提示账号已锁定?",
|
||||||
|
"yes_branch": {
|
||||||
|
"id": "fc-acct-3",
|
||||||
|
"type": "step",
|
||||||
|
"label": "AD 管理控制台解锁账号 + 重置临时密码",
|
||||||
|
"status": "pending",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-acct-4",
|
||||||
|
"type": "step",
|
||||||
|
"label": "通知员工首次登录需修改密码",
|
||||||
|
"status": "pending"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fc-acct-5",
|
||||||
|
"type": "decision",
|
||||||
|
"label": "员工能正常登录?",
|
||||||
|
"yes_branch": {
|
||||||
|
"id": "fc-acct-6",
|
||||||
|
"type": "step",
|
||||||
|
"label": "回访确认 + 提醒密码保管"
|
||||||
|
},
|
||||||
|
"no_branch": {
|
||||||
|
"id": "fc-acct-7",
|
||||||
|
"type": "step",
|
||||||
|
"label": "升级二线:信息安全团队"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"no_branch": {
|
||||||
|
"id": "fc-acct-8",
|
||||||
|
"type": "step",
|
||||||
|
"label": "确认密码是否过期(>90天)",
|
||||||
|
"status": "pending",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-acct-9",
|
||||||
|
"type": "decision",
|
||||||
|
"label": "SSO 登录页能打开?",
|
||||||
|
"yes_branch": {
|
||||||
|
"id": "fc-acct-10",
|
||||||
|
"type": "step",
|
||||||
|
"label": "引导员工走自助密码重置流程",
|
||||||
|
"status": "pending",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-acct-11",
|
||||||
|
"type": "decision",
|
||||||
|
"label": "重置邮件是否收到?",
|
||||||
|
"yes_branch": {
|
||||||
|
"id": "fc-acct-12",
|
||||||
|
"type": "step",
|
||||||
|
"label": "按邮件链接重置 + 回访"
|
||||||
|
},
|
||||||
|
"no_branch": {
|
||||||
|
"id": "fc-acct-13",
|
||||||
|
"type": "step",
|
||||||
|
"label": "检查邮箱/反垃圾/电话二次验证"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"no_branch": {
|
||||||
|
"id": "fc-acct-14",
|
||||||
|
"type": "step",
|
||||||
|
"label": "检查浏览器代理 + 缓存 + 尝试无痕模式",
|
||||||
|
"status": "pending",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-acct-15",
|
||||||
|
"type": "decision",
|
||||||
|
"label": "换浏览器/无痕能打开?",
|
||||||
|
"yes_branch": {
|
||||||
|
"id": "fc-acct-16",
|
||||||
|
"type": "step",
|
||||||
|
"label": "指导员工清除原浏览器缓存"
|
||||||
|
},
|
||||||
|
"no_branch": {
|
||||||
|
"id": "fc-acct-17",
|
||||||
|
"type": "step",
|
||||||
|
"label": "升级二线:检查 SSO 网关状态"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
{
|
||||||
|
"name": "电脑 / Windows 系统故障排查",
|
||||||
|
"category": "system",
|
||||||
|
"description": "员工电脑蓝屏、死机、卡顿、开机黑屏、Windows 更新失败",
|
||||||
|
"estimated_time": 15,
|
||||||
|
"difficulty": 3,
|
||||||
|
"tags": ["电脑", "Windows", "蓝屏", "系统更新", "卡顿"],
|
||||||
|
"root_node": {
|
||||||
|
"id": "fc-sys-1",
|
||||||
|
"type": "step",
|
||||||
|
"label": "确认故障现象(蓝屏代码/卡顿/黑屏/无法开机)",
|
||||||
|
"status": "pending",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-sys-2",
|
||||||
|
"type": "decision",
|
||||||
|
"label": "电脑能正常开机进入桌面?",
|
||||||
|
"yes_branch": {
|
||||||
|
"id": "fc-sys-3",
|
||||||
|
"type": "step",
|
||||||
|
"label": "引导员工打开任务管理器查看资源占用",
|
||||||
|
"status": "pending",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-sys-4",
|
||||||
|
"type": "decision",
|
||||||
|
"label": "CPU/内存/磁盘哪项占用高?",
|
||||||
|
"yes_branch": {
|
||||||
|
"id": "fc-sys-5",
|
||||||
|
"type": "step",
|
||||||
|
"label": "按占用类型分别处理:",
|
||||||
|
"status": "current",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-sys-6",
|
||||||
|
"type": "step",
|
||||||
|
"label": "CPU高:结束异常进程,查启动项",
|
||||||
|
"status": "pending"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fc-sys-7",
|
||||||
|
"type": "step",
|
||||||
|
"label": "内存高:检查泄漏进程,加内存条",
|
||||||
|
"status": "pending"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fc-sys-8",
|
||||||
|
"type": "step",
|
||||||
|
"label": "磁盘100%:查大文件/重做系统考虑",
|
||||||
|
"status": "pending"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"no_branch": {
|
||||||
|
"id": "fc-sys-9",
|
||||||
|
"type": "step",
|
||||||
|
"label": "检查最近安装的软件/驱动/更新",
|
||||||
|
"status": "pending",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-sys-10",
|
||||||
|
"type": "decision",
|
||||||
|
"label": "回滚后是否恢复?",
|
||||||
|
"yes_branch": {
|
||||||
|
"id": "fc-sys-11",
|
||||||
|
"type": "step",
|
||||||
|
"label": "标记该软件/更新为不兼容,记录案例"
|
||||||
|
},
|
||||||
|
"no_branch": {
|
||||||
|
"id": "fc-sys-12",
|
||||||
|
"type": "step",
|
||||||
|
"label": "进入安全模式进一步排查"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"no_branch": {
|
||||||
|
"id": "fc-sys-13",
|
||||||
|
"type": "step",
|
||||||
|
"label": "判断开机阶段(BIOS/启动管理器/登录界面)",
|
||||||
|
"status": "pending",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-sys-14",
|
||||||
|
"type": "decision",
|
||||||
|
"label": "能进安全模式?",
|
||||||
|
"yes_branch": {
|
||||||
|
"id": "fc-sys-15",
|
||||||
|
"type": "step",
|
||||||
|
"label": "在安全模式卸载最近驱动/更新",
|
||||||
|
"status": "pending",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-sys-16",
|
||||||
|
"type": "decision",
|
||||||
|
"label": "重启后正常?",
|
||||||
|
"yes_branch": {
|
||||||
|
"id": "fc-sys-17",
|
||||||
|
"type": "step",
|
||||||
|
"label": "回访确认 + 记录故障点"
|
||||||
|
},
|
||||||
|
"no_branch": {
|
||||||
|
"id": "fc-sys-18",
|
||||||
|
"type": "step",
|
||||||
|
"label": "备份数据后考虑重装系统"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"no_branch": {
|
||||||
|
"id": "fc-sys-19",
|
||||||
|
"type": "step",
|
||||||
|
"label": "硬件层故障:硬盘/内存条/主板",
|
||||||
|
"status": "pending",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-sys-20",
|
||||||
|
"type": "decision",
|
||||||
|
"label": "外接显示器/拔内存条有变化?",
|
||||||
|
"yes_branch": {
|
||||||
|
"id": "fc-sys-21",
|
||||||
|
"type": "step",
|
||||||
|
"label": "对症更换硬件(联系硬件供应商)"
|
||||||
|
},
|
||||||
|
"no_branch": {
|
||||||
|
"id": "fc-sys-22",
|
||||||
|
"type": "step",
|
||||||
|
"label": "升级二线:送修 / 申请备用机"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
{
|
||||||
|
"name": "企业微信 / 协作工具故障排查",
|
||||||
|
"category": "wecom",
|
||||||
|
"description": "企微登录失败、消息发不出、群文件无法下载、视频会议卡顿、审批打不开",
|
||||||
|
"estimated_time": 8,
|
||||||
|
"difficulty": 2,
|
||||||
|
"tags": ["企微", "WeCom", "消息", "视频会议", "审批", "协作"],
|
||||||
|
"root_node": {
|
||||||
|
"id": "fc-wc-1",
|
||||||
|
"type": "step",
|
||||||
|
"label": "确认故障模块(消息/会议/审批/通讯录/文件)",
|
||||||
|
"status": "pending",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-wc-2",
|
||||||
|
"type": "decision",
|
||||||
|
"label": "能否登录企微(手机/电脑端)?",
|
||||||
|
"no_branch": {
|
||||||
|
"id": "fc-wc-3",
|
||||||
|
"type": "step",
|
||||||
|
"label": "引导员工:重新扫码登录/更新企微版本",
|
||||||
|
"status": "pending",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-wc-4",
|
||||||
|
"type": "decision",
|
||||||
|
"label": "重新登录成功?",
|
||||||
|
"yes_branch": {
|
||||||
|
"id": "fc-wc-5",
|
||||||
|
"type": "step",
|
||||||
|
"label": "回访确认其他功能也正常"
|
||||||
|
},
|
||||||
|
"no_branch": {
|
||||||
|
"id": "fc-wc-6",
|
||||||
|
"type": "step",
|
||||||
|
"label": "检查公司是否全员断网/账号是否离职"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"yes_branch": {
|
||||||
|
"id": "fc-wc-7",
|
||||||
|
"type": "step",
|
||||||
|
"label": "按故障模块分别处理:",
|
||||||
|
"status": "current",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-wc-8",
|
||||||
|
"type": "step",
|
||||||
|
"label": "【消息】发不出/收不到:检查网络 + 退出重登 + 清缓存",
|
||||||
|
"status": "pending"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fc-wc-9",
|
||||||
|
"type": "step",
|
||||||
|
"label": "【视频会议】卡顿/掉线:检查带宽(>2Mbps) + 关闭其他视频",
|
||||||
|
"status": "pending"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fc-wc-10",
|
||||||
|
"type": "step",
|
||||||
|
"label": "【审批】打不开:确认审批权限 + 联系审批管理员",
|
||||||
|
"status": "pending"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fc-wc-11",
|
||||||
|
"type": "step",
|
||||||
|
"label": "【文件】下载失败:检查存储空间 + 重新下载",
|
||||||
|
"status": "pending"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fc-wc-12",
|
||||||
|
"type": "step",
|
||||||
|
"label": "【通讯录】看不到新同事:引导同步通讯录",
|
||||||
|
"status": "pending"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fc-wc-13",
|
||||||
|
"type": "decision",
|
||||||
|
"label": "处理后是否解决?",
|
||||||
|
"yes_branch": {
|
||||||
|
"id": "fc-wc-14",
|
||||||
|
"type": "step",
|
||||||
|
"label": "回访 + 记录案例到知识库"
|
||||||
|
},
|
||||||
|
"no_branch": {
|
||||||
|
"id": "fc-wc-15",
|
||||||
|
"type": "step",
|
||||||
|
"label": "升级二线:企微企业管理员 / 厂商支持",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-wc-16",
|
||||||
|
"type": "step",
|
||||||
|
"label": "提供工单截图 + 故障时间 + 员工 userid"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
{
|
||||||
|
"name": "VPN / 远程办公故障排查",
|
||||||
|
"category": "vpn",
|
||||||
|
"description": "员工无法连接公司 VPN,或连接后访问内网失败,或频繁掉线",
|
||||||
|
"estimated_time": 8,
|
||||||
|
"difficulty": 2,
|
||||||
|
"tags": ["VPN", "远程办公", "aTrust", "网络"],
|
||||||
|
"root_node": {
|
||||||
|
"id": "fc-vpn-1",
|
||||||
|
"type": "step",
|
||||||
|
"label": "确认员工当前网络环境(在家/出差/咖啡厅)",
|
||||||
|
"status": "pending",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-vpn-2",
|
||||||
|
"type": "decision",
|
||||||
|
"label": "VPN 客户端能否打开登录页?",
|
||||||
|
"yes_branch": {
|
||||||
|
"id": "fc-vpn-3",
|
||||||
|
"type": "step",
|
||||||
|
"label": "检查账号密码 + 二次认证",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-vpn-4",
|
||||||
|
"type": "decision",
|
||||||
|
"label": "是否连接成功?",
|
||||||
|
"yes_branch": {
|
||||||
|
"id": "fc-vpn-5",
|
||||||
|
"type": "step",
|
||||||
|
"label": "回访确认可访问内网系统"
|
||||||
|
},
|
||||||
|
"no_branch": {
|
||||||
|
"id": "fc-vpn-6",
|
||||||
|
"type": "step",
|
||||||
|
"label": "清除 DNS 缓存 + 重连 aTrust"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"no_branch": {
|
||||||
|
"id": "fc-vpn-7",
|
||||||
|
"type": "step",
|
||||||
|
"label": "升级 VPN 客户端到最新版",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-vpn-8",
|
||||||
|
"type": "decision",
|
||||||
|
"label": "重试能否登录?",
|
||||||
|
"yes_branch": {
|
||||||
|
"id": "fc-vpn-9",
|
||||||
|
"type": "step",
|
||||||
|
"label": "回访确认"
|
||||||
|
},
|
||||||
|
"no_branch": {
|
||||||
|
"id": "fc-vpn-10",
|
||||||
|
"type": "step",
|
||||||
|
"label": "升级二线:信息安全团队(提供 userid + 时间)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
{
|
||||||
|
"name": "企业邮箱故障排查",
|
||||||
|
"category": "email",
|
||||||
|
"description": "员工邮箱登录失败、收发异常、附件打不开、签名问题",
|
||||||
|
"estimated_time": 7,
|
||||||
|
"difficulty": 2,
|
||||||
|
"tags": ["邮箱", "Outlook", "Foxmail", "登录", "附件"],
|
||||||
|
"root_node": {
|
||||||
|
"id": "fc-email-1",
|
||||||
|
"type": "step",
|
||||||
|
"label": "确认邮箱客户端(Outlook/Foxmail/网页/手机)",
|
||||||
|
"status": "pending",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-email-2",
|
||||||
|
"type": "decision",
|
||||||
|
"label": "能否登录网页邮箱?",
|
||||||
|
"yes_branch": {
|
||||||
|
"id": "fc-email-3",
|
||||||
|
"type": "step",
|
||||||
|
"label": "说明账号本身可用,问题在客户端",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-email-4",
|
||||||
|
"type": "decision",
|
||||||
|
"label": "是否收不到新邮件?",
|
||||||
|
"yes_branch": {
|
||||||
|
"id": "fc-email-5",
|
||||||
|
"type": "step",
|
||||||
|
"label": "检查反垃圾设置 + 邮件规则 + 邮箱配额"
|
||||||
|
},
|
||||||
|
"no_branch": {
|
||||||
|
"id": "fc-email-6",
|
||||||
|
"type": "step",
|
||||||
|
"label": "检查 Outlook 缓存 + 重建索引 + 检查 PST 文件大小"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"no_branch": {
|
||||||
|
"id": "fc-email-7",
|
||||||
|
"type": "step",
|
||||||
|
"label": "检查账号是否锁定 + 密码是否过期",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-email-8",
|
||||||
|
"type": "decision",
|
||||||
|
"label": "重置密码后能否登录?",
|
||||||
|
"yes_branch": {
|
||||||
|
"id": "fc-email-9",
|
||||||
|
"type": "step",
|
||||||
|
"label": "回访 + 通知修改其他系统密码"
|
||||||
|
},
|
||||||
|
"no_branch": {
|
||||||
|
"id": "fc-email-10",
|
||||||
|
"type": "step",
|
||||||
|
"label": "升级二线:邮件管理员(提供 userid + 错误截图)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
{
|
||||||
|
"name": "网络 / WiFi 故障排查",
|
||||||
|
"category": "network",
|
||||||
|
"description": "员工连不上公司 WiFi、有线网慢、IP 冲突、WiFi 认证失败、丢包",
|
||||||
|
"estimated_time": 10,
|
||||||
|
"difficulty": 2,
|
||||||
|
"tags": ["网络", "WiFi", "有线", "IP冲突", "丢包"],
|
||||||
|
"root_node": {
|
||||||
|
"id": "fc-net-1",
|
||||||
|
"type": "step",
|
||||||
|
"label": "确认故障范围(单个员工/同楼层/全公司)",
|
||||||
|
"status": "pending",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-net-2",
|
||||||
|
"type": "decision",
|
||||||
|
"label": "影响范围多大?",
|
||||||
|
"yes_branch": {
|
||||||
|
"id": "fc-net-3",
|
||||||
|
"type": "step",
|
||||||
|
"label": "【全公司/楼层】立即升级二线:网络团队",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-net-4",
|
||||||
|
"type": "step",
|
||||||
|
"label": "同时记录:故障时间 + 影响人数 + 现场照片"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"no_branch": {
|
||||||
|
"id": "fc-net-5",
|
||||||
|
"type": "step",
|
||||||
|
"label": "【单个员工】继续单点排查",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-net-6",
|
||||||
|
"type": "decision",
|
||||||
|
"label": "有线网 or WiFi?",
|
||||||
|
"yes_branch": {
|
||||||
|
"id": "fc-net-7",
|
||||||
|
"type": "step",
|
||||||
|
"label": "检查网线 + 换端口 + 重新拨号",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-net-8",
|
||||||
|
"type": "decision",
|
||||||
|
"label": "换端口能用?",
|
||||||
|
"yes_branch": {
|
||||||
|
"id": "fc-net-9",
|
||||||
|
"type": "step",
|
||||||
|
"label": "原端口硬件故障,工单报修"
|
||||||
|
},
|
||||||
|
"no_branch": {
|
||||||
|
"id": "fc-net-10",
|
||||||
|
"type": "step",
|
||||||
|
"label": "检查 IP 冲突:ipconfig /all + 释放续租"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"no_branch": {
|
||||||
|
"id": "fc-net-11",
|
||||||
|
"type": "step",
|
||||||
|
"label": "WiFi 排查:重连 + 忘记网络 + 检查 SSID",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-net-12",
|
||||||
|
"type": "decision",
|
||||||
|
"label": "其他员工同位置能用 WiFi?",
|
||||||
|
"yes_branch": {
|
||||||
|
"id": "fc-net-13",
|
||||||
|
"type": "step",
|
||||||
|
"label": "员工设备问题:重装网卡驱动 + 升级系统"
|
||||||
|
},
|
||||||
|
"no_branch": {
|
||||||
|
"id": "fc-net-14",
|
||||||
|
"type": "step",
|
||||||
|
"label": "AP 信号弱:升级二线查 AP 部署"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
{
|
||||||
|
"name": "打印机 / 外设故障排查",
|
||||||
|
"category": "printer",
|
||||||
|
"description": "员工打印失败、卡纸、驱动问题、扫描仪、U盘识别",
|
||||||
|
"estimated_time": 6,
|
||||||
|
"difficulty": 1,
|
||||||
|
"tags": ["打印", "扫描", "U盘", "外设", "驱动"],
|
||||||
|
"root_node": {
|
||||||
|
"id": "fc-print-1",
|
||||||
|
"type": "step",
|
||||||
|
"label": "确认外设类型(打印/扫描/U盘/其他)",
|
||||||
|
"status": "pending",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-print-2",
|
||||||
|
"type": "decision",
|
||||||
|
"label": "打印机型号?",
|
||||||
|
"yes_branch": {
|
||||||
|
"id": "fc-print-3",
|
||||||
|
"type": "step",
|
||||||
|
"label": "【打印】按故障现象分流:",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-print-4",
|
||||||
|
"type": "step",
|
||||||
|
"label": "卡纸:打开盖板 + 按箭头方向抽纸 + 检查纸槽"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fc-print-5",
|
||||||
|
"type": "step",
|
||||||
|
"label": "脱机:重新添加打印机 + 检查网络(IP 直连 or 服务器共享)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fc-print-6",
|
||||||
|
"type": "step",
|
||||||
|
"label": "驱动异常:卸载重装 + 选对型号 + 重启打印服务"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fc-print-7",
|
||||||
|
"type": "decision",
|
||||||
|
"label": "其他员工同打印机能用?",
|
||||||
|
"yes_branch": {
|
||||||
|
"id": "fc-print-8",
|
||||||
|
"type": "step",
|
||||||
|
"label": "员工电脑问题:换电脑测试确认"
|
||||||
|
},
|
||||||
|
"no_branch": {
|
||||||
|
"id": "fc-print-9",
|
||||||
|
"type": "step",
|
||||||
|
"label": "升级二线:硬件供应商(联系信息见公告)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"no_branch": {
|
||||||
|
"id": "fc-print-10",
|
||||||
|
"type": "step",
|
||||||
|
"label": "【扫描仪/其他外设】:",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-print-11",
|
||||||
|
"type": "step",
|
||||||
|
"label": "扫描仪:检查 USB 连接 + 重新装驱动 + 测试扫描"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fc-print-12",
|
||||||
|
"type": "step",
|
||||||
|
"label": "U盘:插入其他电脑测试 + 检查文件系统(ExFAT 兼容性)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fc-print-13",
|
||||||
|
"type": "step",
|
||||||
|
"label": "其他外设:走通用流程(查线/换口/换电脑/重装驱动)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"name": "软件 / 应用故障排查",
|
||||||
|
"category": "software",
|
||||||
|
"description": "员工软件装不上、闪退、license 过期、版本不兼容、Office/PS/财务软件等",
|
||||||
|
"estimated_time": 8,
|
||||||
|
"difficulty": 2,
|
||||||
|
"tags": ["软件", "Office", "安装", "闪退", "license", "财务"],
|
||||||
|
"root_node": {
|
||||||
|
"id": "fc-soft-1",
|
||||||
|
"type": "step",
|
||||||
|
"label": "确认软件名 + 版本(让员工截图)",
|
||||||
|
"status": "pending",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-soft-2",
|
||||||
|
"type": "decision",
|
||||||
|
"label": "员工是否有管理员权限安装?",
|
||||||
|
"yes_branch": {
|
||||||
|
"id": "fc-soft-3",
|
||||||
|
"type": "step",
|
||||||
|
"label": "【管理员】继续自助排查:",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-soft-4",
|
||||||
|
"type": "step",
|
||||||
|
"label": "装不上:检查系统版本兼容性 + 关杀毒软件 + 管理员运行"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fc-soft-5",
|
||||||
|
"type": "step",
|
||||||
|
"label": "闪退:看 Windows 事件日志 + 找 crash dump"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fc-soft-6",
|
||||||
|
"type": "step",
|
||||||
|
"label": "license 过期:走 IT 资产流程申请续期(申请单见知识库)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"no_branch": {
|
||||||
|
"id": "fc-soft-7",
|
||||||
|
"type": "step",
|
||||||
|
"label": "【普通员工】坐席远程协助安装:",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-soft-8",
|
||||||
|
"type": "step",
|
||||||
|
"label": "常用软件清单(从软件中心/SCCM):Office、Adobe、Foxmail、企微"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fc-soft-9",
|
||||||
|
"type": "step",
|
||||||
|
"label": "非常用软件:需走软件申请流程(部门主管审批 → IT 评估)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fc-soft-10",
|
||||||
|
"type": "decision",
|
||||||
|
"label": "远程能否解决?",
|
||||||
|
"yes_branch": {
|
||||||
|
"id": "fc-soft-11",
|
||||||
|
"type": "step",
|
||||||
|
"label": "回访确认"
|
||||||
|
},
|
||||||
|
"no_branch": {
|
||||||
|
"id": "fc-soft-12",
|
||||||
|
"type": "step",
|
||||||
|
"label": "升级二线:对应软件负责人"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
{
|
||||||
|
"name": "硬件 / 桌面设备故障排查",
|
||||||
|
"category": "hardware",
|
||||||
|
"description": "员工显示器、键盘鼠标、耳机、视频会议摄像头、笔记本电池等",
|
||||||
|
"estimated_time": 10,
|
||||||
|
"difficulty": 2,
|
||||||
|
"tags": ["硬件", "显示器", "键盘", "鼠标", "耳机", "摄像头"],
|
||||||
|
"root_node": {
|
||||||
|
"id": "fc-hw-1",
|
||||||
|
"type": "step",
|
||||||
|
"label": "确认设备类型(显示器/键鼠/耳机/摄像头/其他)",
|
||||||
|
"status": "pending",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-hw-2",
|
||||||
|
"type": "decision",
|
||||||
|
"label": "故障设备能换一台测试吗?",
|
||||||
|
"yes_branch": {
|
||||||
|
"id": "fc-hw-3",
|
||||||
|
"type": "step",
|
||||||
|
"label": "换设备测试,确认是设备本身问题:",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-hw-4",
|
||||||
|
"type": "step",
|
||||||
|
"label": "【显示器】:换视频线(HDMI/DP/VGA) + 检查分辨率"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fc-hw-5",
|
||||||
|
"type": "step",
|
||||||
|
"label": "【键鼠】:换 USB 口 + 换电池 + 蓝牙重新配对"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fc-hw-6",
|
||||||
|
"type": "step",
|
||||||
|
"label": "【耳机/摄像头】:检查 USB/3.5mm + 隐私盖 + 系统权限"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fc-hw-7",
|
||||||
|
"type": "decision",
|
||||||
|
"label": "换设备后正常?",
|
||||||
|
"yes_branch": {
|
||||||
|
"id": "fc-hw-8",
|
||||||
|
"type": "step",
|
||||||
|
"label": "原设备故障:走 IT 资产报废/更换流程"
|
||||||
|
},
|
||||||
|
"no_branch": {
|
||||||
|
"id": "fc-hw-9",
|
||||||
|
"type": "step",
|
||||||
|
"label": "电脑端问题:检查驱动 + 系统设置"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"no_branch": {
|
||||||
|
"id": "fc-hw-10",
|
||||||
|
"type": "step",
|
||||||
|
"label": "【笔记本内嵌设备】:屏幕/键盘/电池/CPU 风扇",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fc-hw-11",
|
||||||
|
"type": "step",
|
||||||
|
"label": "走送修流程(备份数据 → IT 出具送修单 → 厂商维修)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fc-hw-12",
|
||||||
|
"type": "step",
|
||||||
|
"label": "需要备用机:走 IT 资产借用流程(最长 2 周)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fc-hw-13",
|
||||||
|
"type": "step",
|
||||||
|
"label": "升级二线:硬件供应商(联系信息见公告)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
把 9 套排查流程图 JSON 合并到一个数组,输出 00-all.json(便于一次性 import)。
|
||||||
|
用法:python build_all.py
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
HERE = Path(__file__).parent
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# 1. 找 9 个单文件(排除 00-all.json 和 build_all.py)
|
||||||
|
files = sorted(HERE.glob("[0-9][0-9]-*.json"))
|
||||||
|
if not files:
|
||||||
|
print("❌ 没找到任何 0X-*.json 文件")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"📦 找到 {len(files)} 个模板文件:")
|
||||||
|
for f in files:
|
||||||
|
print(f" - {f.name}")
|
||||||
|
|
||||||
|
# 2. 逐个读 + 校验
|
||||||
|
templates = []
|
||||||
|
for f in files:
|
||||||
|
try:
|
||||||
|
with open(f, "r", encoding="utf-8") as fp:
|
||||||
|
tpl = json.load(fp)
|
||||||
|
# 简单校验
|
||||||
|
for required in ("name", "category", "root_node"):
|
||||||
|
if required not in tpl:
|
||||||
|
raise ValueError(f"缺少必要字段: {required}")
|
||||||
|
templates.append(tpl)
|
||||||
|
print(f" ✅ {f.name}: {tpl['name']} ({len(json.dumps(tpl, ensure_ascii=False))} 字符)")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ {f.name}: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 3. 输出汇总文件
|
||||||
|
out = HERE / "00-all.json"
|
||||||
|
with open(out, "w", encoding="utf-8") as fp:
|
||||||
|
json.dump(templates, fp, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
print(f"\n✅ 已生成 {out.name} (共 {len(templates)} 套)")
|
||||||
|
print(f"\n💡 接下来你可以:")
|
||||||
|
print(f" 1. 打开 {out.name} 预览 9 套完整内容")
|
||||||
|
print(f" 2. 在 Admin 后台的「排查流程图」页 → 「导入 JSON」选择此文件")
|
||||||
|
print(f" 3. 或调用后端 API:")
|
||||||
|
print(f" for tpl in templates: POST /api/troubleshooting-templates")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -13,11 +13,18 @@
|
|||||||
"type-check": "vue-tsc --noEmit"
|
"type-check": "vue-tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
|
"@codemirror/state": "^6.4.1",
|
||||||
|
"@codemirror/theme-one-dark": "^6.1.2",
|
||||||
|
"@codemirror/view": "^6.26.3",
|
||||||
"@element-plus/icons-vue": "^2.3.0",
|
"@element-plus/icons-vue": "^2.3.0",
|
||||||
"axios": "^1.7.0",
|
"axios": "^1.7.0",
|
||||||
|
"codemirror": "^6.0.1",
|
||||||
"element-plus": "^2.7.0",
|
"element-plus": "^2.7.0",
|
||||||
"pinia": "^2.1.0",
|
"pinia": "^2.1.0",
|
||||||
"vue": "^3.4.0",
|
"vue": "^3.4.0",
|
||||||
|
"vue-codemirror": "^6.0.1",
|
||||||
|
"vue-json-pretty": "^2.2.4",
|
||||||
"vue-router": "^4.3.0"
|
"vue-router": "^4.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
// =============================================================================
|
||||||
|
// 排查模板 API 客户端
|
||||||
|
// =============================================================================
|
||||||
|
// 对接后端 /api/troubleshooting-templates 5 个 REST 端点
|
||||||
|
// 5 个端点:GET 列表 / GET 详情 / POST 新建 / PUT 更新 / DELETE 删除
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// 类型定义
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** 步骤节点(顺序执行) */
|
||||||
|
export interface PathStepNode {
|
||||||
|
id: string
|
||||||
|
type: 'step'
|
||||||
|
label: string
|
||||||
|
status?: 'done' | 'current' | 'pending'
|
||||||
|
children?: FlowchartNode[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 决策节点(yes/no 分支) */
|
||||||
|
export interface DecisionNode {
|
||||||
|
id: string
|
||||||
|
type: 'decision'
|
||||||
|
label: string
|
||||||
|
status?: 'done' | 'current' | 'pending'
|
||||||
|
yes_branch?: FlowchartNode
|
||||||
|
no_branch?: FlowchartNode
|
||||||
|
children?: FlowchartNode[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 流程图节点(递归) */
|
||||||
|
export type FlowchartNode = PathStepNode | DecisionNode
|
||||||
|
|
||||||
|
/** 排查模板 */
|
||||||
|
export interface TroubleshootingTemplate {
|
||||||
|
id?: string
|
||||||
|
name: string
|
||||||
|
category: string
|
||||||
|
description?: string
|
||||||
|
estimated_time?: number
|
||||||
|
difficulty?: number
|
||||||
|
tags?: string[]
|
||||||
|
root_node: FlowchartNode
|
||||||
|
version?: string
|
||||||
|
status?: 'draft' | 'published'
|
||||||
|
created_at?: string
|
||||||
|
updated_at?: string
|
||||||
|
// 后端可能附加的统计字段
|
||||||
|
nodeCount?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** API 响应通用结构 */
|
||||||
|
interface ApiResponse<T> {
|
||||||
|
code: number
|
||||||
|
message: string
|
||||||
|
data: T
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Axios 实例(继承全局 baseURL)
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
const http = axios.create({
|
||||||
|
baseURL: '/api',
|
||||||
|
timeout: 30000,
|
||||||
|
})
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// 5 个端点
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** GET /api/troubleshooting-templates — 获取模板列表 */
|
||||||
|
export async function listTemplates(): Promise<TroubleshootingTemplate[]> {
|
||||||
|
const res = await http.get<ApiResponse<TroubleshootingTemplate[]>>(
|
||||||
|
'/troubleshooting-templates'
|
||||||
|
)
|
||||||
|
return res.data.data || []
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET /api/troubleshooting-templates/{id} — 获取模板详情 */
|
||||||
|
export async function getTemplate(id: string): Promise<TroubleshootingTemplate> {
|
||||||
|
const res = await http.get<ApiResponse<TroubleshootingTemplate>>(
|
||||||
|
`/troubleshooting-templates/${id}`
|
||||||
|
)
|
||||||
|
return res.data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /api/troubleshooting-templates — 新建模板 */
|
||||||
|
export async function createTemplate(
|
||||||
|
data: TroubleshootingTemplate
|
||||||
|
): Promise<TroubleshootingTemplate> {
|
||||||
|
const res = await http.post<ApiResponse<TroubleshootingTemplate>>(
|
||||||
|
'/troubleshooting-templates',
|
||||||
|
data
|
||||||
|
)
|
||||||
|
return res.data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/** PUT /api/troubleshooting-templates/{id} — 更新模板 */
|
||||||
|
export async function updateTemplate(
|
||||||
|
id: string,
|
||||||
|
data: TroubleshootingTemplate
|
||||||
|
): Promise<TroubleshootingTemplate> {
|
||||||
|
const res = await http.put<ApiResponse<TroubleshootingTemplate>>(
|
||||||
|
`/troubleshooting-templates/${id}`,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
return res.data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/** DELETE /api/troubleshooting-templates/{id} — 删除模板 */
|
||||||
|
export async function deleteTemplate(id: string): Promise<void> {
|
||||||
|
await http.delete(`/troubleshooting-templates/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 工具:把对象格式化成 JSON 字符串(带缩进) */
|
||||||
|
export function formatJson(obj: unknown): string {
|
||||||
|
return JSON.stringify(obj, null, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 工具:校验 JSON 字符串是否合法,返回 {ok, data, error} */
|
||||||
|
export function validateJson(
|
||||||
|
text: string
|
||||||
|
): { ok: true; data: TroubleshootingTemplate } | { ok: false; error: string } {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(text) as TroubleshootingTemplate
|
||||||
|
return { ok: true, data }
|
||||||
|
} catch (e) {
|
||||||
|
const err = e as Error
|
||||||
|
return { ok: false, error: err.message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 工具:统计节点数(递归) */
|
||||||
|
export function countNodes(node: FlowchartNode | undefined): number {
|
||||||
|
if (!node) return 0
|
||||||
|
let count = 1
|
||||||
|
if (node.children) {
|
||||||
|
for (const child of node.children) {
|
||||||
|
count += countNodes(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 决策节点的 yes/no 分支
|
||||||
|
if ('yes_branch' in node && node.yes_branch) {
|
||||||
|
count += countNodes(node.yes_branch)
|
||||||
|
}
|
||||||
|
if ('no_branch' in node && node.no_branch) {
|
||||||
|
count += countNodes(node.no_branch)
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 工具:统计决策节点数 */
|
||||||
|
export function countDecisions(node: FlowchartNode | undefined): number {
|
||||||
|
if (!node) return 0
|
||||||
|
let count = node.type === 'decision' ? 1 : 0
|
||||||
|
if (node.children) {
|
||||||
|
for (const child of node.children) {
|
||||||
|
count += countDecisions(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ('yes_branch' in node && node.yes_branch) {
|
||||||
|
count += countDecisions(node.yes_branch)
|
||||||
|
}
|
||||||
|
if ('no_branch' in node && node.no_branch) {
|
||||||
|
count += countDecisions(node.no_branch)
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
@@ -0,0 +1,475 @@
|
|||||||
|
<!-- =============================================================================
|
||||||
|
// 排查流程图 — 在线编辑器对话框
|
||||||
|
// =============================================================================
|
||||||
|
// 双栏布局(用户已确认 A 方案):
|
||||||
|
// - 左 50%:CodeMirror JSON 源码(语法高亮 + 行号 + oneDark 主题)
|
||||||
|
// - 右 50%:vue-json-pretty 树形预览(只读)
|
||||||
|
// - 顶部:基本信息(名称/分类/标签/时间/难度)
|
||||||
|
// - 底栏:格式化/复制/导出/取消/保存
|
||||||
|
// ============================================================================= -->
|
||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:model-value="modelValue"
|
||||||
|
@update:model-value="(v) => $emit('update:modelValue', v)"
|
||||||
|
:title="dialogTitle"
|
||||||
|
width="80%"
|
||||||
|
top="5vh"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
:destroy-on-close="true"
|
||||||
|
>
|
||||||
|
<!-- ===== 顶部基本信息 ===== -->
|
||||||
|
<div class="basic-info">
|
||||||
|
<el-form :inline="true" size="small" label-width="70px">
|
||||||
|
<el-form-item label="名称">
|
||||||
|
<el-input v-model="form.name" placeholder="VPN 远程办公故障排查" style="width: 240px" :disabled="readonly" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="分类">
|
||||||
|
<el-select v-model="form.category" placeholder="选择分类" style="width: 130px" :disabled="readonly">
|
||||||
|
<el-option v-for="c in CATEGORY_OPTIONS" :key="c" :label="c" :value="c" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="预估时间">
|
||||||
|
<el-input-number v-model="form.estimated_time" :min="1" :max="120" size="small" :disabled="readonly" />
|
||||||
|
<span class="suffix">分钟</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="难度">
|
||||||
|
<el-rate v-model="form.difficulty" :max="5" :disabled="readonly" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="标签">
|
||||||
|
<el-select
|
||||||
|
v-model="form.tags"
|
||||||
|
multiple
|
||||||
|
filterable
|
||||||
|
allow-create
|
||||||
|
default-first-option
|
||||||
|
placeholder="按 Enter 添加"
|
||||||
|
style="width: 240px"
|
||||||
|
:disabled="readonly"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== 双栏编辑区 ===== -->
|
||||||
|
<div class="dual-pane">
|
||||||
|
<!-- 左:JSON 源码编辑器 -->
|
||||||
|
<div class="pane left-pane">
|
||||||
|
<div class="pane-header">
|
||||||
|
<span class="pane-title">📝 JSON 源码</span>
|
||||||
|
<span class="pane-stats">
|
||||||
|
节点 {{ stats.nodes }} · 决策 {{ stats.decisions }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="pane-body">
|
||||||
|
<codemirror
|
||||||
|
v-model="jsonText"
|
||||||
|
:options="cmOptions"
|
||||||
|
:height="`${editorHeight}px`"
|
||||||
|
:style="{ height: `${editorHeight}px` }"
|
||||||
|
@change="onCodeChange"
|
||||||
|
:readonly="readonly"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右:树形预览 -->
|
||||||
|
<div class="pane right-pane">
|
||||||
|
<div class="pane-header">
|
||||||
|
<span class="pane-title">🌳 树形预览</span>
|
||||||
|
<span class="pane-stats">
|
||||||
|
<el-tag v-if="parseOk" type="success" size="small">✅ JSON 有效</el-tag>
|
||||||
|
<el-tag v-else type="danger" size="small">❌ {{ parseError }}</el-tag>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="pane-body">
|
||||||
|
<div v-if="parseOk" class="tree-wrapper">
|
||||||
|
<vue-json-pretty
|
||||||
|
:data="parsedData"
|
||||||
|
:show-length="true"
|
||||||
|
:show-line="true"
|
||||||
|
:path="rootPath"
|
||||||
|
:deep="6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else class="tree-error">
|
||||||
|
<p>❌ JSON 解析失败</p>
|
||||||
|
<pre>{{ parseError }}</pre>
|
||||||
|
<p class="hint">请检查左栏 JSON 语法,例如:</p>
|
||||||
|
<ul>
|
||||||
|
<li>末尾的逗号</li>
|
||||||
|
<li>未闭合的引号或大括号</li>
|
||||||
|
<li>未转义的字符</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== 底栏按钮 ===== -->
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<div class="footer-left">
|
||||||
|
<el-button size="small" @click="handleFormat" :disabled="readonly">
|
||||||
|
<el-icon><MagicStick /></el-icon>
|
||||||
|
格式化
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" @click="handleCopy" :disabled="!parseOk">
|
||||||
|
<el-icon><CopyDocument /></el-icon>
|
||||||
|
复制
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" @click="handleExport" :disabled="!parseOk">
|
||||||
|
<el-icon><Download /></el-icon>
|
||||||
|
导出此条
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="footer-right">
|
||||||
|
<el-button size="small" @click="handleCancel">取消</el-button>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
:disabled="!parseOk || readonly"
|
||||||
|
:loading="saving"
|
||||||
|
@click="handleSave"
|
||||||
|
>
|
||||||
|
<el-icon><Check /></el-icon>
|
||||||
|
保存
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// =============================================================================
|
||||||
|
// imports
|
||||||
|
// =============================================================================
|
||||||
|
import { ref, reactive, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { MagicStick, CopyDocument, Download, Check } from '@element-plus/icons-vue'
|
||||||
|
import { Codemirror } from 'vue-codemirror'
|
||||||
|
import { json } from '@codemirror/lang-json'
|
||||||
|
import { oneDark } from '@codemirror/theme-one-dark'
|
||||||
|
import VueJsonPretty from 'vue-json-pretty'
|
||||||
|
import 'vue-json-pretty/lib/styles.css'
|
||||||
|
import {
|
||||||
|
formatJson,
|
||||||
|
validateJson,
|
||||||
|
countNodes,
|
||||||
|
countDecisions,
|
||||||
|
type TroubleshootingTemplate,
|
||||||
|
type FlowchartNode,
|
||||||
|
} from '@/api/troubleshooting'
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// props & emits
|
||||||
|
// =============================================================================
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
template: TroubleshootingTemplate | null
|
||||||
|
readonly?: boolean
|
||||||
|
saving?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: boolean]
|
||||||
|
save: [template: TroubleshootingTemplate]
|
||||||
|
cancel: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 常量
|
||||||
|
// =============================================================================
|
||||||
|
const CATEGORY_OPTIONS = [
|
||||||
|
'vpn', 'email', 'account', 'system', 'network',
|
||||||
|
'printer', 'software', 'hardware', 'wecom', 'other',
|
||||||
|
]
|
||||||
|
|
||||||
|
const EMPTY_TEMPLATE: TroubleshootingTemplate = {
|
||||||
|
name: '',
|
||||||
|
category: 'system',
|
||||||
|
description: '',
|
||||||
|
estimated_time: 5,
|
||||||
|
difficulty: 2,
|
||||||
|
tags: [],
|
||||||
|
root_node: {
|
||||||
|
id: 'fc-new-1',
|
||||||
|
type: 'step',
|
||||||
|
label: '请修改此步骤',
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 响应式状态
|
||||||
|
// =============================================================================
|
||||||
|
const form = reactive<TroubleshootingTemplate>({ ...EMPTY_TEMPLATE })
|
||||||
|
const jsonText = ref<string>('')
|
||||||
|
const parseOk = ref<boolean>(true)
|
||||||
|
const parseError = ref<string>('')
|
||||||
|
const parsedData = ref<TroubleshootingTemplate | null>(null)
|
||||||
|
const editorHeight = ref<number>(500)
|
||||||
|
|
||||||
|
// 编辑器配置
|
||||||
|
const cmOptions = {
|
||||||
|
mode: 'application/json',
|
||||||
|
theme: 'oneDark',
|
||||||
|
lineNumbers: true,
|
||||||
|
lineWrapping: true,
|
||||||
|
tabSize: 2,
|
||||||
|
indentUnit: 2,
|
||||||
|
smartIndent: true,
|
||||||
|
matchBrackets: true,
|
||||||
|
autoCloseBrackets: true,
|
||||||
|
foldGutter: true,
|
||||||
|
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||||
|
extensions: [json(), oneDark],
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 计算属性
|
||||||
|
// =============================================================================
|
||||||
|
const dialogTitle = computed(() => {
|
||||||
|
if (props.readonly) return `👁 预览: ${form.name || '未命名'}`
|
||||||
|
return props.template?.id ? `✎ 编辑: ${form.name || '未命名'}` : '+ 新建流程图'
|
||||||
|
})
|
||||||
|
|
||||||
|
const stats = computed(() => ({
|
||||||
|
nodes: parseOk.value && parsedData.value ? countNodes(parsedData.value.root_node) : 0,
|
||||||
|
decisions: parseOk.value && parsedData.value ? countDecisions(parsedData.value.root_node) : 0,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const rootPath = computed(() => 'root')
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// watch
|
||||||
|
// =============================================================================
|
||||||
|
watch(
|
||||||
|
() => props.template,
|
||||||
|
(newTpl) => {
|
||||||
|
if (newTpl) {
|
||||||
|
Object.assign(form, newTpl)
|
||||||
|
jsonText.value = formatJson(newTpl)
|
||||||
|
onCodeChange()
|
||||||
|
} else {
|
||||||
|
Object.assign(form, EMPTY_TEMPLATE)
|
||||||
|
jsonText.value = formatJson(EMPTY_TEMPLATE)
|
||||||
|
onCodeChange()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(jsonText, () => {
|
||||||
|
// 实时同步到 form(让顶部表单跟着 JSON 变)
|
||||||
|
if (parseOk.value && parsedData.value) {
|
||||||
|
form.name = parsedData.value.name || form.name
|
||||||
|
form.category = parsedData.value.category || form.category
|
||||||
|
form.estimated_time = parsedData.value.estimated_time ?? form.estimated_time
|
||||||
|
form.difficulty = parsedData.value.difficulty ?? form.difficulty
|
||||||
|
form.tags = parsedData.value.tags || form.tags
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 生命周期
|
||||||
|
// =============================================================================
|
||||||
|
function updateEditorHeight() {
|
||||||
|
editorHeight.value = Math.max(window.innerHeight - 380, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateEditorHeight()
|
||||||
|
window.addEventListener('resize', updateEditorHeight)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', updateEditorHeight)
|
||||||
|
})
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 方法
|
||||||
|
// =============================================================================
|
||||||
|
function onCodeChange() {
|
||||||
|
const result = validateJson(jsonText.value)
|
||||||
|
if (result.ok) {
|
||||||
|
parseOk.value = true
|
||||||
|
parseError.value = ''
|
||||||
|
parsedData.value = result.data
|
||||||
|
} else {
|
||||||
|
parseOk.value = false
|
||||||
|
parseError.value = result.error
|
||||||
|
parsedData.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFormat() {
|
||||||
|
if (parseOk.value && parsedData.value) {
|
||||||
|
jsonText.value = formatJson(parsedData.value)
|
||||||
|
ElMessage.success('JSON 已格式化')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCopy() {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(jsonText.value)
|
||||||
|
ElMessage.success('已复制到剪贴板')
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('复制失败,请手动 Ctrl+C')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleExport() {
|
||||||
|
if (!parseOk.value || !parsedData.value) return
|
||||||
|
const blob = new Blob([jsonText.value], { type: 'application/json' })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = `${(form.name || 'flowchart').replace(/\s+/g, '-')}.json`
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
document.body.removeChild(a)
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
ElMessage.success('已导出')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancel() {
|
||||||
|
emit('update:modelValue', false)
|
||||||
|
emit('cancel')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSave() {
|
||||||
|
if (!parseOk.value || !parsedData.value) {
|
||||||
|
ElMessage.error('JSON 无效,无法保存')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 合并:form 基本信息 + parsedData JSON 内容
|
||||||
|
const finalTpl: TroubleshootingTemplate = {
|
||||||
|
...parsedData.value,
|
||||||
|
name: form.name,
|
||||||
|
category: form.category,
|
||||||
|
description: form.description ?? parsedData.value.description,
|
||||||
|
estimated_time: form.estimated_time,
|
||||||
|
difficulty: form.difficulty,
|
||||||
|
tags: form.tags,
|
||||||
|
}
|
||||||
|
emit('save', finalTpl)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.basic-info {
|
||||||
|
padding: 12px 0;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.basic-info :deep(.el-form-item) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.basic-info .suffix {
|
||||||
|
margin-left: 4px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dual-pane {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
height: 540px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane-title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane-stats {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-pane .pane-body {
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-wrapper {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-error {
|
||||||
|
color: #f56c6c;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-error pre {
|
||||||
|
background: #fef0f0;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-error .hint {
|
||||||
|
margin-top: 12px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-error ul {
|
||||||
|
margin: 4px 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-left,
|
||||||
|
.footer-right {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog__body) {
|
||||||
|
padding: 16px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.CodeMirror) {
|
||||||
|
height: 100%;
|
||||||
|
font-family: 'Fira Code', 'Source Code Pro', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,230 +2,382 @@
|
|||||||
=============================================================================
|
=============================================================================
|
||||||
企微IT智能服务台 — 排查流程图管理页
|
企微IT智能服务台 — 排查流程图管理页
|
||||||
=============================================================================
|
=============================================================================
|
||||||
说明:JSON 导入导出 + 预览 + 版本管理
|
说明:JSON 导入导出 + 在线编辑 + 树形预览
|
||||||
阶段三开始实现,当前为占位功能
|
阶段三实现 - 用户已选 B 方案(双栏 CodeMirror + vue-json-pretty)
|
||||||
显示模板列表 + 灰化的导入/导出/新建按钮
|
功能:
|
||||||
底部展示实现路径
|
- 列表(从 /api/troubleshooting-templates 拉取)
|
||||||
|
- 预览/编辑/删除(单条)
|
||||||
|
- 导入 JSON(文件)
|
||||||
|
- 导出全部(批量下载)
|
||||||
|
- 新建(空模板)
|
||||||
|
=============================================================================
|
||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="flowcharts-page">
|
<div class="flowcharts-page">
|
||||||
<!-- 页面标题 -->
|
<!-- 页面标题 -->
|
||||||
<div class="page-title">排查流程图管理</div>
|
<div class="page-header">
|
||||||
<div class="page-desc">JSON 导入导出 + 预览 + 版本管理。阶段三开始实现,后续升级为可视化拖拽编辑。</div>
|
<div>
|
||||||
|
<div class="page-title">排查流程图管理</div>
|
||||||
|
<div class="page-desc">JSON 导入导出 + 在线编辑 + 树形预览。共 {{ flowcharts.length }} 套模板</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 操作按钮(灰化占位) -->
|
<!-- 操作按钮 -->
|
||||||
<div class="flowchart-actions">
|
<div class="flowchart-actions">
|
||||||
<el-button type="primary" disabled>
|
<el-button type="primary" @click="handleImport">
|
||||||
<el-icon><Upload /></el-icon>
|
<el-icon><Upload /></el-icon>
|
||||||
导入 JSON
|
导入 JSON
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button disabled>
|
<el-button @click="handleExportAll" :disabled="flowcharts.length === 0">
|
||||||
<el-icon><Download /></el-icon>
|
<el-icon><Download /></el-icon>
|
||||||
导出全部
|
导出全部
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button disabled>
|
<el-button @click="handleNew">
|
||||||
<el-icon><Plus /></el-icon>
|
<el-icon><Plus /></el-icon>
|
||||||
新建流程图
|
新建流程图
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button @click="loadList" :loading="loading">
|
||||||
|
<el-icon><Refresh /></el-icon>
|
||||||
|
刷新
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 流程图模板表格 -->
|
<!-- 流程图模板表格 -->
|
||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
<el-table
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
:data="flowcharts"
|
:data="flowcharts"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
:header-cell-style="{ background: 'var(--bg-tertiary)', color: 'var(--text-secondary)', fontSize: '12px' }"
|
:header-cell-style="{ background: 'var(--bg-tertiary)', color: 'var(--text-secondary)', fontSize: '12px' }"
|
||||||
:cell-style="{ color: 'var(--text-primary)', fontSize: '13px' }"
|
:cell-style="{ color: 'var(--text-primary)', fontSize: '13px' }"
|
||||||
row-class-name="flowchart-table-row"
|
row-class-name="flowchart-table-row"
|
||||||
|
empty-text="暂无流程图,点击「新建流程图」开始"
|
||||||
>
|
>
|
||||||
<el-table-column label="流程图名称" min-width="200">
|
<el-table-column label="流程图名称" min-width="220">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="flowchart-name">
|
<div class="flowchart-name">
|
||||||
<el-icon :size="16" style="color: var(--accent); margin-right: 6px">
|
<el-icon :size="16" style="color: var(--accent); margin-right: 6px">
|
||||||
<Share />
|
<Share />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
{{ row.name }}
|
<span>{{ row.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="分类" width="80">
|
<el-table-column label="分类" width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag size="small" effect="plain">{{ row.category }}</el-tag>
|
<el-tag size="small" effect="plain">{{ row.category }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="节点数" width="80" align="center" prop="nodeCount" />
|
<el-table-column label="节点数" width="80" align="center" prop="nodeCount" />
|
||||||
<el-table-column label="版本" width="70" align="center" prop="version" />
|
<el-table-column label="预估时间" width="90" align="center">
|
||||||
<el-table-column label="最后更新" width="110" prop="updatedAt" />
|
<template #default="{ row }">{{ row.estimated_time ?? '-' }} 分钟</template>
|
||||||
<el-table-column label="状态" width="90">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag :type="row.statusType" size="small">
|
|
||||||
{{ row.statusText }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="140" align="center">
|
<el-table-column label="版本" width="70" align="center">
|
||||||
<template #default>
|
<template #default="{ row }">{{ row.version || 'v1.0' }}</template>
|
||||||
<el-button size="small" text type="primary" disabled>预览</el-button>
|
</el-table-column>
|
||||||
<el-button size="small" text disabled>编辑</el-button>
|
<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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 实现路径 -->
|
<!-- 在线编辑器对话框 -->
|
||||||
<div class="roadmap-section">
|
<FlowchartEditorDialog
|
||||||
<div class="roadmap-title">
|
v-model="dialogVisible"
|
||||||
<el-icon :size="16" style="color: var(--accent); margin-right: 6px"><Flag /></el-icon>
|
:template="currentTemplate"
|
||||||
实现路径
|
:readonly="dialogMode === 'preview'"
|
||||||
</div>
|
:saving="saving"
|
||||||
<div class="roadmap-steps">
|
@save="handleSave"
|
||||||
<div class="roadmap-step active">
|
@cancel="handleDialogCancel"
|
||||||
<div class="step-number">Step 1</div>
|
/>
|
||||||
<div class="step-title">JSON 导入导出 + 预览</div>
|
|
||||||
<div class="step-phase">阶段三 3B</div>
|
<!-- 隐藏的文件选择器(导入 JSON) -->
|
||||||
</div>
|
<input
|
||||||
<el-icon :size="16" class="roadmap-arrow"><ArrowRight /></el-icon>
|
ref="fileInputRef"
|
||||||
<div class="roadmap-step">
|
type="file"
|
||||||
<div class="step-number">Step 2</div>
|
accept=".json,application/json"
|
||||||
<div class="step-title">导出为 Dify 变量</div>
|
style="display: none"
|
||||||
<div class="step-phase">阶段四 4A</div>
|
@change="handleFileSelected"
|
||||||
</div>
|
/>
|
||||||
<el-icon :size="16" class="roadmap-arrow"><ArrowRight /></el-icon>
|
|
||||||
<div class="roadmap-step">
|
|
||||||
<div class="step-number">Step 3</div>
|
|
||||||
<div class="step-title">Dify HTTP 回调</div>
|
|
||||||
<div class="step-phase">阶段四</div>
|
|
||||||
</div>
|
|
||||||
<el-icon :size="16" class="roadmap-arrow"><ArrowRight /></el-icon>
|
|
||||||
<div class="roadmap-step">
|
|
||||||
<div class="step-number">Step 4</div>
|
|
||||||
<div class="step-title">可视化拖拽编辑</div>
|
|
||||||
<div class="step-phase">远景</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// ==========================================================================
|
// =============================================================================
|
||||||
// Demo 数据
|
// imports
|
||||||
// ==========================================================================
|
// =============================================================================
|
||||||
const flowcharts = [
|
import { ref, onMounted } from 'vue'
|
||||||
{
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
name: 'VPN连接故障排查',
|
import {
|
||||||
category: '网络',
|
Upload, Download, Plus, Refresh,
|
||||||
nodeCount: 12,
|
Share, View, Edit, Delete,
|
||||||
version: 'v2.1',
|
} from '@element-plus/icons-vue'
|
||||||
updatedAt: '2026-06-10',
|
import FlowchartEditorDialog from '@/components/flowchart/FlowchartEditorDialog.vue'
|
||||||
statusType: 'success',
|
import {
|
||||||
statusText: '已发布',
|
listTemplates,
|
||||||
},
|
getTemplate,
|
||||||
{
|
createTemplate,
|
||||||
name: '打印机脱机排查',
|
updateTemplate,
|
||||||
category: '外设',
|
deleteTemplate,
|
||||||
nodeCount: 8,
|
formatJson,
|
||||||
version: 'v1.3',
|
countNodes,
|
||||||
updatedAt: '2026-06-08',
|
type TroubleshootingTemplate,
|
||||||
statusType: 'success',
|
} from '@/api/troubleshooting'
|
||||||
statusText: '已发布',
|
|
||||||
},
|
// =============================================================================
|
||||||
{
|
// 响应式状态
|
||||||
name: '邮箱登录失败排查',
|
// =============================================================================
|
||||||
category: '软件',
|
const flowcharts = ref<TroubleshootingTemplate[]>([])
|
||||||
nodeCount: 10,
|
const loading = ref<boolean>(false)
|
||||||
version: 'v1.0',
|
const saving = ref<boolean>(false)
|
||||||
updatedAt: '2026-06-06',
|
|
||||||
statusType: 'warning',
|
const dialogVisible = ref<boolean>(false)
|
||||||
statusText: '草稿',
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<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 {
|
.flowchart-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 流程图名称 */
|
|
||||||
.flowchart-name {
|
.flowchart-name {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
|
||||||
|
|
||||||
/* 实现路径区域 */
|
|
||||||
.roadmap-section {
|
|
||||||
margin-top: 24px;
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.roadmap-title {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.roadmap-steps {
|
|
||||||
display: flex;
|
|
||||||
gap: 0;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.roadmap-step {
|
|
||||||
border-radius: var(--radius);
|
|
||||||
padding: 12px 16px;
|
|
||||||
flex: 1;
|
|
||||||
text-align: center;
|
|
||||||
background: var(--bg-primary);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
.roadmap-step.active {
|
|
||||||
background: var(--accent-light);
|
|
||||||
border-color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-number {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
.roadmap-step.active .step-number {
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-title {
|
.table-wrapper {
|
||||||
font-size: 13px;
|
background: white;
|
||||||
margin-top: 4px;
|
border-radius: var(--radius-lg);
|
||||||
color: var(--text-secondary);
|
padding: 4px;
|
||||||
}
|
|
||||||
.roadmap-step.active .step-title {
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-phase {
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.roadmap-arrow {
|
|
||||||
color: var(--text-muted);
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin: 0 4px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* 流程图表格行悬停 */
|
|
||||||
.flowchart-table-row:hover td {
|
.flowchart-table-row:hover td {
|
||||||
background-color: var(--bg-tertiary) !important;
|
background-color: var(--bg-tertiary) !important;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user