1823 lines
115 KiB
Vue
1823 lines
115 KiB
Vue
<script setup lang="ts">
|
||
import { computed, ref } from 'vue';
|
||
import { Icon } from '@iconify/vue'
|
||
import { useThemeStore } from '@/store/modules/theme';
|
||
import {
|
||
CHANGES, type ChangeLevel, type ChangeOrder, type ChangeStatus, type ChangeType,
|
||
} from "@/typings/model";
|
||
import { getColorWithOpacity } from "@/utils/common";
|
||
import {
|
||
AiError,
|
||
aiApplication,
|
||
aiLevelScore,
|
||
aiRecognize,
|
||
aiRiskOrganize,
|
||
aiRiskPreAnalysis,
|
||
aiTypeJudge,
|
||
type CompareRow,
|
||
} from "@/service/api/ai";
|
||
import HighlightText from "./modules/HighlightText.vue";
|
||
import Ed from "./modules/Ed.vue";
|
||
import Hazop from "./modules/hazop.vue";
|
||
import Jsa from "./modules/jsa.vue";
|
||
import Scl from "./modules/scl.vue";
|
||
import RiskCheck from "./modules/riskCheck.vue";
|
||
|
||
const themeStore = useThemeStore();
|
||
|
||
interface ApplyForm {
|
||
name: string;
|
||
dept: string;
|
||
mainChanges: string[];
|
||
related: string;
|
||
purposes: string[];
|
||
effect: string;
|
||
type: string;
|
||
level: string;
|
||
duration: string;
|
||
urgent: boolean;
|
||
date: any;
|
||
restoreDate: any;
|
||
}
|
||
const form = ref<ApplyForm>({
|
||
name: "", dept: "", mainChanges: [""], related: "", purposes: [], effect: "",
|
||
type: "", level: "", duration: "", urgent: false, date: null, restoreDate: null,
|
||
});
|
||
const tab = ref<string>("pre");
|
||
const TABS = [
|
||
{ id: "pre", name: "变更预识别", icon: 'lucide:sparkles' },
|
||
{ id: "form", name: "变更申请表", icon: 'akar-icons:file' },
|
||
{ id: "risk", name: "风险分析记录表", icon: 'proicons:alert-triangle' },
|
||
{ id: "train", name: "变更培训内容", icon: 'lucide:graduation-cap' },
|
||
{ id: "pssr", name: "PSSR 检查内容", icon: 'tabler:clipboard-check' },
|
||
{ id: "accept", name: "验收评价", icon: 'material-symbols:check-circle-outline' },
|
||
{ id: "close", name: "变更关闭确认表", icon: 'quill:folder-open' },
|
||
];
|
||
const confirmed = ref<Record<string, boolean>>({});
|
||
const blockedTabs = ref<string[]>([]);
|
||
|
||
const aiEnabled = ref<boolean>(true);
|
||
const analyzing = ref<boolean>(false);
|
||
// ---------- 变更预识别 ----------
|
||
const desc = ref<string>(
|
||
"某缩合反应现在控在80℃要反应8小时,车间反馈产能跟不上。我们小试做了几组,90℃下4小时转化率就能到98%,但有个未知杂质会从0.05%涨到0.12%。这个杂质结构还没定,不过不是基因毒性警示结构。想申请把温度提到90℃,同时把反应时间缩到4小时。",
|
||
);
|
||
const rows = ref<any>(null);
|
||
const AI_ROWS = [
|
||
{
|
||
id: 1, item: "工艺流程",
|
||
before: "流程简述(含物料、设备):某缩合反应,原料A与原料B在缩合反应釜中混合,加热至80℃进行保温反应8小时,反应完成后转入后处理工序。\n设计意图:通过缩合反应制得目标产物,保证转化率和杂质水平。",
|
||
after: "流程简述(含物料、设备):保温温度提升至90℃,反应时间缩短为4小时,其余流程不变。\n设计意图:在保持转化率98%的前提下缩短反应时间、提升产能。",
|
||
hl: ["保温温度提升至90℃,反应时间缩短为4小时", "在保持转化率98%的前提下缩短反应时间、提升产能"],
|
||
},
|
||
{
|
||
id: 2, item: "设备信息",
|
||
before: "基本信息(名称、设计参数、材质、用途):缩合反应釜,设计温度100℃,设计压力0.2MPa,材质SS304,用于缩合反应。\n使用信息(安装位置、连接方式、维护策略):安装于反应工段,夹套加热,按预防性维护计划定期检查。",
|
||
after: "无变化",
|
||
},
|
||
{
|
||
id: 3, item: "运行条件",
|
||
before: "运行参数:反应温度80±2℃,保温时间8小时,压力常压。\n管控措施:DCS温度高报警设定85℃,高高报警90℃并联锁切断加热介质。",
|
||
after: "运行参数:反应温度改为90±2℃,保温时间改为4小时,压力不变。\n管控措施:DCS温度高报警需上调至95℃,高高报警及联锁值需上调至100℃。",
|
||
hl: ["90±2℃", "4小时", "DCS温度高报警需上调至95℃,高高报警及联锁值需上调至100℃"],
|
||
},
|
||
{ id: 4, item: "操作方式", before: "DCS自动化控制,远程操作,单人监控。", after: "无变化" },
|
||
];
|
||
// AI 调用失败统一提示:不阻塞页面、不清空已有数据,保留手动编辑能力(P1)
|
||
const aiFail = (e: any) => {
|
||
if (e instanceof AiError) window.$message?.warning(`AI 分析失败:${e.message},可手动填写`);
|
||
else window.$message?.warning("AI 分析失败,可手动填写");
|
||
};
|
||
// 页面当前对比行 → AI 契约行
|
||
const compareRows = (): CompareRow[] => (rows.value ?? []).map((x: any) => ({ item: x.item, before: x.before, after: x.after, hl: x.hl }));
|
||
// ---------- AI 变更识别 ----------
|
||
const runIdentify = async () => {
|
||
if (!desc.value.trim()) { window.$message?.warning("请先输入变更内容描述"); return; }
|
||
analyzing.value = true;
|
||
rows.value = null;
|
||
try {
|
||
const res = await aiRecognize(desc.value);
|
||
rows.value = (res?.rows ?? []).map((x, i) => ({ id: i + 1, item: x.item, before: x.before, after: x.after, hl: x.hl }));
|
||
window.$message?.success(`AI 变更识别完成,共 ${rows.value.length} 项,请逐项人工确认(可直接修改)`);
|
||
} catch (e: any) {
|
||
rows.value = AI_ROWS.map((x) => ({ ...x }));
|
||
aiFail(e);
|
||
} finally {
|
||
analyzing.value = false;
|
||
}
|
||
};
|
||
|
||
const editing = ref<any>(null);
|
||
|
||
// ---------- 风险预分析 ----------
|
||
const preRiskEdit = ref<any>(null);
|
||
const preRiskShown = ref<boolean>(false);
|
||
const preRiskText = ref({
|
||
severity: "本次变更为缩合反应温度由80℃提升至90℃、保温时间由8h缩短至4h。变更前后反应体系与物料不变,不新增化学品,不存在新增燃爆、毒害、腐蚀等危害严重性变化;温度提升后仍在溶剂沸点与物料分解温度的安全裕度之内。综上,本次变更未显著增加潜在事件的危害严重程度,事故后果(人员伤亡、财产损失、环境污染)的严重等级与变更前基本一致;但杂质水平由0.05%升至0.12%,需关注长期运行对产品质量与设备结垢的累积影响。",
|
||
probability: "变更引入了新的偏差场景。首先,反应温度更接近溶剂回流温度,温控失效或冷却水中断时,超温溜温概率上升,可能引发冲料;其次,保温时间缩短使反应终点判断窗口变窄,人为误判导致转化率不足或副反应增加的概率上升;再者,未知杂质含量升高(0.05%→0.12%),其热稳定性尚未定性,长期累积可能加速釜壁结垢与搅拌负荷异常;此外,DCS报警/联锁值上调后原有报警裕度改变,误报、漏报概率需重新评估。",
|
||
protection: "变更主要影响与温度控制相关的保护层。原DCS温度高报警85℃、高高报警及联锁90℃已不适用于新工艺窗口,需同步上调至95℃/100℃并重新整定验证;安全阀起跳压力与泄放能力不受本次温度调整直接影响,仍然有效;操作规程与应急预案须修订后,方可继续作为有效的人为保护层。建议投用前完成联锁测试与报警验证,并将杂质含量纳入日常分析计划。",
|
||
});
|
||
const runRiskAnalysis = async () => {
|
||
analyzing.value = true;
|
||
try {
|
||
const res = await aiRiskPreAnalysis(desc.value, compareRows());
|
||
preRiskText.value = {
|
||
severity: res?.severity ?? preRiskText.value.severity,
|
||
probability: res?.probability ?? preRiskText.value.probability,
|
||
protection: res?.protection ?? preRiskText.value.protection,
|
||
};
|
||
preRiskShown.value = true;
|
||
window.$message?.success("AI 风险预分析报告已生成(危害严重性 / 事件概率 / 保护措施影响),内容支持手动修改");
|
||
} catch (e: any) {
|
||
preRiskShown.value = true;
|
||
aiFail(e);
|
||
} finally {
|
||
analyzing.value = false;
|
||
}
|
||
};
|
||
const saveDraft = () => window.$message?.success("已保存为草稿,可在左侧「我的草稿」中继续编辑");
|
||
|
||
// ---------- 生成变更申请单 ----------
|
||
const formGen = ref<string>("idle");
|
||
const materialsText = ref("");
|
||
const updateDocs = ref<string[]>([]);
|
||
const disciplines = ref<string[]>([]);
|
||
const tools = ref<string[]>(["HAZOP","JSA", "SCL", "RISK_CHECK"]);
|
||
const UPDATE_DOC_OPTIONS = ['PID图纸', '操作规程', '总图', '设备台账', '工艺卡片', '联锁台账', '应急预案'];
|
||
const DISCIPLINE_OPTIONS = ['技术', '仪表', '电气', '环保', '质量'];
|
||
const genForm = async () => {
|
||
if (formGen.value === "generating") return;
|
||
formGen.value = "generating";
|
||
window.$message?.info("AI 正在生成变更申请单…(「变更申请表」标签已显示生成中标识)");
|
||
try {
|
||
const res = await aiApplication(desc.value, compareRows());
|
||
const pick = (v: string | null | undefined, opts: string[], cur: string) => (v && opts.includes(v) ? v : cur);
|
||
const main = (res?.main_changes ?? []).filter((x) => x && x.trim());
|
||
const purposes = (res?.purposes ?? []).filter((x) => PURPOSE_OPTIONS.includes(x));
|
||
const docs = (res?.update_docs ?? []).filter((x) => UPDATE_DOC_OPTIONS.includes(x));
|
||
const discs = (res?.disciplines ?? []).filter((x) => DISCIPLINE_OPTIONS.includes(x));
|
||
const aiTools = (res?.risk_tools ?? [])
|
||
.map((x) => RISK_TOOLS_FORM.find((t) => t.key === x || t.label === x)?.key)
|
||
.filter((x): x is string => !!x);
|
||
form.value = {
|
||
...form.value,
|
||
name: res?.name || form.value.name,
|
||
mainChanges: main.length ? main : form.value.mainChanges,
|
||
related: (res?.related_changes ?? []).filter(Boolean).join("、") || form.value.related,
|
||
purposes: purposes.length ? purposes : form.value.purposes,
|
||
effect: res?.effect || form.value.effect,
|
||
type: pick(res?.change_type, ['工艺', '设备', '管理'], form.value.type),
|
||
duration: pick(res?.duration_suggestion, ['永久', '临时'], form.value.duration),
|
||
};
|
||
if (res?.materials_text) materialsText.value = res.materials_text;
|
||
if (docs.length) updateDocs.value = docs;
|
||
if (discs.length) {
|
||
disciplines.value = discs;
|
||
toggleDiscipline(discs);
|
||
}
|
||
if (res?.signers && Object.keys(res.signers).length) signers.value = { ...res.signers };
|
||
if (aiTools.length) tools.value = aiTools;
|
||
currentPlugin.value = "";
|
||
formGen.value = "done";
|
||
tab.value = "form";
|
||
window.$message?.success("变更申请单已生成:AI 自动总结名称、预填内容,均可手动修改");
|
||
} catch (e: any) {
|
||
formGen.value = "idle";
|
||
aiFail(e);
|
||
}
|
||
};
|
||
// ---------- 审批流程设置 ----------
|
||
const approvers = ref<Record<string, string>>({
|
||
部门审核: "李主任(一车间主任)",
|
||
最终批准: "李主任(部门负责人 · 固定)",
|
||
});
|
||
const signers = ref<Record<string, string>>({});
|
||
const sheet = ref<"type" | "level" | "approver" | null>(null);
|
||
let demoSeq = 43;
|
||
function nextChangeId() {
|
||
return `MOC-2026-R01-${String(demoSeq++).padStart(4, "0")}`;
|
||
}
|
||
const today = () => new Date().toISOString().slice(0, 10);
|
||
interface LedgerRow {
|
||
id: string;
|
||
changeId?: string; // 关联 CHANGES,可打开完整详情
|
||
title: string;
|
||
type: ChangeType;
|
||
level: ChangeLevel;
|
||
duration: "永久" | "临时";
|
||
urgent?: boolean;
|
||
applyTime: string; // 申请时间
|
||
dept: string; // 申请部门
|
||
applicant: string; // 申请人
|
||
planUse: string; // 计划投用时间
|
||
status: ChangeStatus;
|
||
statusDetail: string; // 当前环节说明
|
||
statusTime: string; // 状态更新时间
|
||
overdueDays?: number;
|
||
approveTime?: string; // 审批通过时间
|
||
useTime?: string; // 实际投用时间
|
||
handlers: { name: string; done: boolean }[]; // 接班人(当前待办/已办)
|
||
}
|
||
const LEDGER_ROWS: LedgerRow[] = [
|
||
{ id: "MOC-2026-R01-0035", changeId: "MOC-2026-R01-0035", title: "R-201 反应釜搅拌器电机更换(55kW→75kW 防爆电机)", type: "设备", level: "重要", duration: "永久", applyTime: "2026-07-28 09:12", dept: "一车间", applicant: "张工艺", planUse: "2026-08-10", status: "审批中", statusDetail: "专业会签中(周设备、王仪表待签)", statusTime: "2026-07-30 14:20", handlers: [{ name: "王仪表", done: true }, { name: "周设备", done: false }, { name: "王仪表", done: false }] },
|
||
{ id: "MOC-2026-R01-0036", changeId: "MOC-2026-R01-0036", title: "聚合反应温度控制上限调整(78℃→82℃)", type: "工艺", level: "重要", duration: "永久", applyTime: "2026-07-25 10:05", dept: "一车间", applicant: "孙丽", planUse: "2026-08-05", status: "审批中", statusDetail: "安全审查(王海燕)", statusTime: "2026-07-29 16:40", handlers: [{ name: "王海燕", done: false }] },
|
||
{ id: "MOC-2026-R02-0038", changeId: "MOC-2026-R02-0038", title: "DCS 罐区液位报警值 LIA-301 调整", type: "工艺", level: "一般", duration: "永久", applyTime: "2026-07-20 08:30", dept: "一车间", applicant: "吴强", planUse: "2026-07-31", status: "待PSSR", statusDetail: "培训 / PSSR 资料已传 · 待确认投用", statusTime: "2026-07-27 11:15", approveTime: "2026-07-26 16:40", handlers: [{ name: "吴强", done: false }] },
|
||
{ id: "MOC-2026-R01-0039", changeId: "MOC-2026-R01-0039", title: "V-102 安全阀起跳压力临时调整(紧急补办)", type: "设备", level: "一般", duration: "临时", urgent: true, applyTime: "2026-07-15 19:40", dept: "一车间", applicant: "张工艺", planUse: "2026-07-16(已投用)", status: "待关闭", statusDetail: "待关闭确认(资料更新核查中)", statusTime: "2026-07-27 09:02", approveTime: "2026-07-15 21:05", useTime: "2026-07-16 06:30", overdueDays: 4, handlers: [{ name: "李主任", done: false }] },
|
||
{ id: "MOC-2026-R03-0040", changeId: "MOC-2026-R03-0040", title: "原料环己烷供应商变更(新增 B 供应商)", type: "管理", level: "重要", duration: "永久", applyTime: "2026-07-18 14:22", dept: "供应部", applicant: "刘芳", planUse: "2026-08-01", status: "实施中", statusDetail: "实施准备中 · 培训 / PSSR 资料待上传", statusTime: "2026-07-26 10:30", handlers: [{ name: "刘芳", done: false }] },
|
||
{ id: "MOC-2026-R02-0031", title: "冷冻盐水泵 P-103 叶轮材质变更", type: "设备", level: "一般", duration: "永久", applyTime: "2026-06-28 09:50", dept: "二车间", applicant: "周涛", planUse: "2026-07-15", status: "待验收", statusDetail: "变更验收(72h 运行验证)", statusTime: "2026-07-24 15:44", overdueDays: 7, handlers: [{ name: "李主任", done: false }] },
|
||
{ id: "MOC-2026-R01-0029", title: "操作规程修订(夏季工况)", type: "管理", level: "一般", duration: "临时", applyTime: "2026-06-20 11:26", dept: "一车间", applicant: "张工艺", planUse: "2026-07-01", status: "审批中", statusDetail: "部门审核(李主任)", statusTime: "2026-06-21 08:40", handlers: [{ name: "李主任", done: false }] },
|
||
{ id: "MOC-2026-R02-0033", title: "包装线贴标机控制程序升级", type: "设备", level: "一般", duration: "永久", applyTime: "2026-07-08 13:35", dept: "二车间", applicant: "吴强", planUse: "2026-08-02", status: "已批准", statusDetail: "待实施(计划 8 月 2 日投用)", statusTime: "2026-07-22 09:12", handlers: [{ name: "周设备", done: false }] },
|
||
{ id: "MOC-2026-R03-0027", title: "化验室通风橱整体更换", type: "设备", level: "一般", duration: "永久", applyTime: "2026-06-12 15:10", dept: "质量部", applicant: "陈静", planUse: "2026-06-25", status: "已关闭", statusDetail: "关闭确认完成,已归档", statusTime: "2026-07-02 10:18", handlers: [{ name: "钱峰", done: true }] },
|
||
{ id: "MOC-2026-R01-0024", title: "氮气管网压力分级调整", type: "工艺", level: "重要", duration: "永久", applyTime: "2026-05-30 09:00", dept: "一车间", applicant: "孙丽", planUse: "2026-06-20", status: "已关闭", statusDetail: "关闭确认完成,已归档", statusTime: "2026-06-28 16:30", handlers: [{ name: "李主任", done: true }, { name: "刘副总", done: true }] },
|
||
];
|
||
const ROLES = [
|
||
{ id: "applicant", name: "申请人", person: "张工艺", org: "一车间 · 工艺组", desc: "发起变更申请、AI 辅助判定、跟踪进度" },
|
||
{ id: "process", name: "专业会签(仪表)", person: "王仪表", org: "技术部 · 仪表科", desc: "专业会签、HAZOP 分析、确认连带变更" },
|
||
{ id: "process2", name: "专业会签(设备)", person: "周设备", org: "设备管理部 · 设备科", desc: "专业会签、设备完整性检查、行动项落实" },
|
||
{ id: "safety", name: "安全工程师", person: "王海燕", org: "安全环保部", desc: "安全审查、PSSR 检查表确认、风险分析组织" },
|
||
{ id: "dept", name: "车间主任", person: "李主任", org: "一车间", desc: "部门审核、现场验收、变更关闭确认" },
|
||
{ id: "vp", name: "分管领导", person: "刘副总", org: "公司领导", desc: "重要变更最终批准、移动端审批" },
|
||
{ id: "admin", name: "系统管理员", person: "刘敏", org: "信息化部", desc: "流程配置、字典维护、权限分配" },
|
||
{ id: "quality", name: "质量工程师", person: "陈质量", org: "质量部", desc: "质量会签、产品合格判定、检验方案确认" },
|
||
{ id: "viewer", name: "非审批人员", person: "吴新", org: "相关部门", desc: "以信息查看为主,不参与变更流程" },
|
||
];
|
||
function nowStr() {
|
||
const d = new Date();
|
||
const p = (n: number) => String(n).padStart(2, "0");
|
||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
||
}
|
||
type DemoListener = () => void;
|
||
const demoListeners = new Set<DemoListener>();
|
||
function emitDemo() { demoListeners.forEach((f) => f()); }
|
||
function submitChange(c: ChangeOrder) {
|
||
CHANGES.unshift(c);
|
||
const cur = c.flow.find((n) => n.status === "current");
|
||
LEDGER_ROWS.unshift({
|
||
id: c.id,
|
||
changeId: c.id,
|
||
title: c.title,
|
||
type: c.type,
|
||
level: c.level,
|
||
duration: c.duration,
|
||
urgent: c.urgent || undefined,
|
||
applyTime: nowStr(),
|
||
dept: c.org.split("·")[0].trim(),
|
||
applicant: c.applicant,
|
||
planUse: c.date || "待定",
|
||
status: c.status,
|
||
statusDetail: cur ? `${cur.name}(待处理)` : "审批中",
|
||
statusTime: nowStr(),
|
||
handlers: (cur?.roles ?? []).map((rid) => ({ name: ROLES.find((r) => r.id === rid)?.person ?? rid, done: false })),
|
||
});
|
||
emitDemo();
|
||
}
|
||
// ---------- 提交 ----------
|
||
const submitApply = () => {
|
||
if (!desc.value.trim()) { window.$message?.warning("请先录入变更描述并完成 AI 识别"); return; }
|
||
if (!form.value) { window.$message?.warning("请先在「变更预识别」页点击「生成变更申请单」,生成申请表后再提交"); return; }
|
||
const unconfirmed = ["form", "risk", "train", "pssr", "accept", "close"].filter((t) => !confirmed.value[t]);
|
||
if (unconfirmed.length) {
|
||
blockedTabs.value = unconfirmed;
|
||
window.$message?.warning(`全部标签页确认后方可提交,请先完成以下标签页的「内容确认完毕」(标签栏已用红点标出):${unconfirmed.map((t) => TAB_NAME[t]).join("、")}`);
|
||
return;
|
||
}
|
||
blockedTabs.value = [];
|
||
const id = nextChangeId();
|
||
const level: ChangeLevel = form.value.level === "重要" ? "重要" : "一般";
|
||
const flow: any[] = level === "重要"
|
||
? [
|
||
{ name: "部门审核", kind: "审批", roles: ["dept"], status: "current" },
|
||
{ name: "专业会签", kind: "会签", roles: ["process", "process2"], status: "todo", locked: true, members: [{ name: "王仪表(仪表)", done: false }, { name: "周设备(设备)", done: false }] },
|
||
{ name: "安全审查", kind: "审批", roles: ["safety"], status: "todo", locked: true },
|
||
{ name: "分管领导最终批准", kind: "最终批准", roles: ["vp"], status: "todo", locked: true },
|
||
]
|
||
: [
|
||
{ name: "部门审核", kind: "审批", roles: ["dept"], status: "current" },
|
||
{ name: "专业审批", kind: "审批", roles: ["process"], status: "todo", locked: true },
|
||
{ name: "部门负责人批准", kind: "最终批准", roles: ["dept"], status: "todo", locked: true },
|
||
];
|
||
const order: ChangeOrder = {
|
||
id,
|
||
title: form.value.name || desc.value.slice(0, 24) || "未命名变更",
|
||
type: (form.value.type || "工艺") as ChangeType,
|
||
level,
|
||
duration: form.value.duration === "临时" ? "临时" : "永久",
|
||
urgent: !!form.value.urgent,
|
||
status: "审批中",
|
||
applicant: ROLES[0].person,
|
||
org: form.value.dept || ROLES[0].org,
|
||
device: "—",
|
||
date: today(),
|
||
purpose: form.value.purposes.join("、") || "提高生产效能",
|
||
effect: form.value.effect || "—",
|
||
before: rows.value?.map((x) => `${x.item}:${x.before}`).join(";") || desc.value.slice(0, 60),
|
||
after: rows.value?.map((x) => `${x.item}:${x.after}`).join(";") || "—",
|
||
related: form.value.related ? form.value.related.split("、") : updateDocs.value,
|
||
materials: materialsText.value ? materialsText.value.split("\n").filter(Boolean) : [],
|
||
riskTools: tools.value,
|
||
flow,
|
||
};
|
||
submitChange(order);
|
||
window.$message?.success(`已提交 ${id},进入「部门审核」节点(台账 / 审批中心 / 角标已联动)`);
|
||
// emit("openChange", order);
|
||
};
|
||
|
||
const PURPOSE_OPTIONS = [
|
||
"满足市场需求", "提高生产效能", "改善操作条件", "增加安全性能",
|
||
"达到环保要求", "法律合规整改", "减少能耗物损", "稳定系统运行",
|
||
"完善管理制度", "达到质量改进", "设备可靠性升级",
|
||
];
|
||
const DEPT_APPROVER_OPTS = ["李主任(一车间主任)", "赵主任(二车间主任)", "陈主任(三车间主任)"].map((x) => ({ label: x, value: x }));
|
||
const SIGNER_POOL = ["王仪表(技术部)", "王仪表(技术部 · 仪表)", "周设备(设备科 · 电气)", "王海燕(安全环保部)", "王海燕(质量部)"];
|
||
const signerOpts = (cur: string) => [...new Set([cur, ...SIGNER_POOL])].map((x) => ({ label: x, value: x }));
|
||
const levelScores = ref<number[]>([2, 2, 3, 1, 2, 3]);
|
||
const totalScore = computed(() => levelScores.value.reduce((a, b) => a + b, 0));
|
||
const levelResult = computed(() => (totalScore.value >= 20 ? "重要变更" : "一般变更"));
|
||
const RISK_TOOLS_FORM = [
|
||
{ key: "HAZOP", label: "HAZOP" },
|
||
{ key: "JSA", label: "JSA" },
|
||
{ key: "SCL", label: "设备SCL" },
|
||
{ key: "RISK_CHECK", label: "风险检查表" },
|
||
];
|
||
const recommendTools = () => {
|
||
const lv = form.value?.level === "重要" ? "重要" : "一般";
|
||
tools.value = lv === "重要" ? ["HAZOP", "JSA", "检查表法", "通用检查表"] : ["JSA", "检查表法", "通用检查表"];
|
||
window.$message?.success(`已按「${lv}变更」规则匹配风险分析工具,可继续手动重选`);
|
||
};
|
||
|
||
const files = ref<string[]>(["小试实验报告.pdf", "杂质初步分析.docx"]);
|
||
const previewFile = ref<string | null>(null);
|
||
const DISCIPLINE_SIGNER: Record<string, string> = {
|
||
技术: "王仪表(技术部)", 仪表: "王仪表(技术部 · 仪表)", 电气: "周设备(设备科 · 电气)", 环保: "王海燕(安全环保部)", 质量: "王海燕(质量部)",
|
||
};
|
||
const toggleDiscipline = (d:any) => {
|
||
const result = disciplines.value.reduce((acc: Record<string, string>, key: string) => {
|
||
acc[key] = DISCIPLINE_SIGNER[key]; // 如果键不存在,值为 undefined
|
||
return acc;
|
||
}, {});
|
||
signers.value = {...result};
|
||
};
|
||
|
||
const TAB_NAME: Record<string, string> = {
|
||
pre: "变更预识别", form: "变更申请表", risk: "风险分析记录表",
|
||
train: "变更培训内容", pssr: "PSSR检查内容", accept: "验收评价", close: "变更关闭确认表",
|
||
};
|
||
const confirmTab = (t: string) => {
|
||
const v = !confirmed.value[t];
|
||
confirmed.value = { ...confirmed.value, [t]: v };
|
||
if (v) window.$message?.success(`已确认${TAB_NAME[t]}内容完毕(标签页栏已标记 ✓)`);
|
||
else window.$message?.info(`已取消${TAB_NAME[t]}的确认标记`);
|
||
};
|
||
const regenerate = () => window.$message?.success(`演示:已根据变更描述重新生成${TAB_NAME[tab.value]}(本页内容已刷新)`);
|
||
const download = () => window.$message?.success(`演示:${TAB_NAME[tab.value]}已下载(Word/PDF)`);
|
||
|
||
const downloadPdf = () => window.$message?.success(`演示:${previewFile.value} 已下载`);
|
||
const LEVEL_DIMS = [
|
||
{
|
||
name: "1. 设备影响", note: "不合适的材质将会导致",
|
||
options: [
|
||
{ label: "对设备带来一定的损害及腐蚀", score: 1 },
|
||
{ label: "对设备带来中等的损害及腐蚀", score: 2 },
|
||
{ label: "对设备带来明显的损害及腐蚀", score: 3 },
|
||
{ label: "对设备带来重大的损害及腐蚀", score: 4 },
|
||
],
|
||
},
|
||
{
|
||
name: "2. 财务影响", note: "工艺设计错误将会导致",
|
||
options: [
|
||
{ label: "一定损失,a<1万人民币", score: 1 },
|
||
{ label: "中等损失,直接损失1万≤a<10万人民币", score: 2 },
|
||
{ label: "大量损失,直接损失10万≤a<100万人民币", score: 3 },
|
||
{ label: "巨额损失,直接损失100万人民币≤a", score: 4 },
|
||
],
|
||
},
|
||
{
|
||
name: "3. 质量影响", note: "",
|
||
options: [
|
||
{ label: "对质量带来一定的风险(非关键工席、非关键设备设施的变更)", score: 1 },
|
||
{ label: "对质量带来中等风险(B类原料替代、生产辅助设备变更)", score: 2 },
|
||
{ label: "对质量带来明显的风险(主要工艺参数、中间体生产工艺变更)", score: 3 },
|
||
{ label: "对质量有很高风险(主要工艺路线、关键工艺参数变更)", score: 4 },
|
||
],
|
||
},
|
||
{
|
||
name: "4. 设计的复杂程度", note: "工艺设计包含了",
|
||
options: [
|
||
{ label: "少量的简单系统(单个设备或单个部件)", score: 1 },
|
||
{ label: "大量的简单系统(多个单类设备或部件)", score: 2 },
|
||
{ label: "至少一个复杂系统(涉及多设备/工艺相应调整)", score: 3 },
|
||
{ label: "大量的复杂系统(需要装置配套改造调整)", score: 4 },
|
||
],
|
||
},
|
||
{
|
||
name: "5. 工艺设计的成熟度", note: "",
|
||
options: [
|
||
{ label: "被频繁使用,且被证明为专利方设计", score: 1 },
|
||
{ label: "被少量使用,或被证明为专利方设计", score: 2 },
|
||
{ label: "被相关工业设计方法或参考文献认可(非专利方)", score: 3 },
|
||
{ label: "新的,未发表的", score: 4 },
|
||
],
|
||
},
|
||
{
|
||
name: "6. 工艺危害", note: "物料性质和工艺条件",
|
||
options: [
|
||
{ label: "无危害(常压、-10℃≤温度<40℃、无毒、无腐蚀不燃)", score: 1 },
|
||
{ label: "可燃/负压/低正压(0~0.3MPa)、40℃≤温度<80℃", score: 2 },
|
||
{ label: "易燃/中压(0.3~1.0MPa)、80℃≤温度<150℃", score: 3 },
|
||
{ label: "极度危害/高压(>1.0MPa)、温度≥150℃", score: 4 },
|
||
],
|
||
},
|
||
];
|
||
const finishApprover = () => {
|
||
sheet.value = null;
|
||
window.$message?.success('审批流程已更新(底部固定行摘要同步刷新)');
|
||
}
|
||
// 风险识别分析记录表
|
||
const TOOL_CARDS = [
|
||
{ key: "HAZOP" as const, name: "HAZOP 分析", desc: "节点-偏离结构化分析:原因 / 后果 / LS 风险矩阵 / 安全措施" },
|
||
{ key: "JSA" as const, name: "JSA 分析", desc: "作业步骤分解:危害因素识别与 LS 风险分级管控" },
|
||
{ key: "SCL" as const, name: "设备 SCL", desc: "设备 / 管理检查项目对照标准逐项核查" },
|
||
{ key: "RISK_CHECK" as const, name: "风险检查表", desc: "变更通用检查项:是 / 否 / 不涉及逐项判定,输出风险描述与管控措施" },
|
||
];
|
||
const pluginDone = ref<Record<string, number>>({});
|
||
const riskRecords = ref<any>([]);
|
||
const updRec = (i: number, patch: Partial<any>) => {
|
||
riskRecords.value = riskRecords.value.map((x, j) => (j === i ? { ...x, ...patch } : x));
|
||
};
|
||
const lvlNext = (v: string) => (v === "高" ? "中" : v === "中" ? "低" : "高");
|
||
const pssrGroups = ref<{ g: string; items: any[] }[]>([
|
||
{ g: "一、风险分析行动项落实", items: [
|
||
{ c: "联锁跳车电流设定值校核并留存记录", p: "投用前" },
|
||
{ c: "电缆与开关容量校核升级完成", p: "投用前" },
|
||
{ c: "操作人员培训完成并考核合格", p: "投用前" },
|
||
] },
|
||
{ g: "二、现场设备安装检查确认", items: [
|
||
{ c: "电机安装就位,联轴器对中合格", p: "投用前" },
|
||
{ c: "地脚螺栓紧固,减振垫安装到位", p: "投用前" },
|
||
{ c: "电缆接线、防爆格兰头密封良好", p: "投用前" },
|
||
] },
|
||
{ g: "三、保护措施确认", items: [
|
||
{ c: "联锁跳车设定值与新电机额定电流匹配", p: "投用前" },
|
||
{ c: "电机外壳接地、防护罩完好", p: "投用前" },
|
||
{ c: "紧急停机按钮功能试验正常", p: "投用前" },
|
||
] },
|
||
{ g: "四、资料归档确认", items: [
|
||
{ c: "P&ID(图号 PID-R201-03)已更新并受控", p: "关闭前" },
|
||
{ c: "设备台账、备件清单已更新", p: "关闭前" },
|
||
{ c: "培训签到表、考核记录已归档", p: "关闭前" },
|
||
] },
|
||
]);
|
||
const toPssr = (i: number) => {
|
||
const rec = riskRecords.value[i];
|
||
if (!rec || rec.pssr) return;
|
||
pssrGroups.value = pssrGroups.value.map((g:any, gi:any) => gi === 0
|
||
? { ...g, items: [...g.items, { c: `${rec.suggest}(来源:风险分析记录表 · ${rec.item})`, p: "投用前" }] }
|
||
: g);
|
||
updRec(i, { pssr: true });
|
||
window.$message?.success("建议措施已转为 PSSR 行动项(见「PSSR 检查内容」· 一、风险分析行动项落实)");
|
||
};
|
||
const toPssrAll = () => {
|
||
const rest = riskRecords.value.map((x:any, i:any) => (x.pssr ? -1 : i)).filter((i:any) => i >= 0);
|
||
if (!rest.length) { window.$message?.info("所有建议措施均已转为 PSSR 行动项"); return; }
|
||
const items = rest.map((i:any) => ({ c: `${riskRecords.value[i].suggest}(来源:风险分析记录表 · ${riskRecords.value[i].item})`, p: "投用前" }));
|
||
pssrGroups.value = pssrGroups.value.map((g:any, gi:any) => gi === 0 ? { ...g, items: [...g.items, ...items] } : g);
|
||
riskRecords.value = riskRecords.value.map((x:any) => ({ ...x, pssr: true }));
|
||
window.$message?.success(`已将 ${rest.length} 条建议措施全部转为 PSSR 行动项(见「PSSR 检查内容」)`);
|
||
};
|
||
// L/S → 页面风险等级(高/中/低)换算,对齐官方 5×5 矩阵阈值
|
||
const lsLevel = (l: number | null, s: number | null) => {
|
||
const rr = (l ?? 0) * (s ?? 0);
|
||
return rr >= 10 ? "高" : rr >= 5 ? "中" : "低";
|
||
};
|
||
const aiOrganize = async () => {
|
||
analyzing.value = true;
|
||
try {
|
||
const res = await aiRiskOrganize(desc.value, compareRows());
|
||
const gen = (res?.records ?? []).map((r) => ({
|
||
item: r.item,
|
||
scene: r.scene,
|
||
inherent: lsLevel(r.l, r.s),
|
||
existing: r.existing,
|
||
suggest: r.suggest,
|
||
residual: lsLevel(r.residual_l, r.residual_s),
|
||
source: "AI 整理",
|
||
}));
|
||
riskRecords.value = [...riskRecords.value, ...gen];
|
||
window.$message?.success(`AI 已根据变更内容自动整理形成风险分析记录表(${gen.length} 项),支持手动修改`);
|
||
} catch (e: any) {
|
||
aiFail(e);
|
||
} finally {
|
||
analyzing.value = false;
|
||
}
|
||
};
|
||
// ---------- AI 类型判定 / 等级评分(抽屉打开时调用,静态示例作 fallback) ----------
|
||
const typeAi = ref<{ type: string; confidence: number; conclusion: string; hit_rules: string[]; exclude_rules: string[]; knowledge_refs: string[] } | null>(null);
|
||
const typeAiLoading = ref<boolean>(false);
|
||
const openTypeSheet = async () => {
|
||
sheet.value = "type";
|
||
if (typeAiLoading.value) return;
|
||
typeAiLoading.value = true;
|
||
try {
|
||
const res = await aiTypeJudge(desc.value, compareRows());
|
||
typeAi.value = {
|
||
type: res?.type ?? "",
|
||
confidence: res?.confidence ?? 0,
|
||
conclusion: res?.conclusion ?? "",
|
||
hit_rules: res?.hit_rules ?? [],
|
||
exclude_rules: res?.exclude_rules ?? [],
|
||
knowledge_refs: res?.knowledge_refs ?? [],
|
||
};
|
||
} catch (e: any) {
|
||
aiFail(e);
|
||
} finally {
|
||
typeAiLoading.value = false;
|
||
}
|
||
};
|
||
const levelAiText = ref<string>("");
|
||
const levelAiBasis = ref<string[]>([]);
|
||
const levelAiLoading = ref<boolean>(false);
|
||
const openLevelSheet = async () => {
|
||
sheet.value = "level";
|
||
if (levelAiLoading.value) return;
|
||
levelAiLoading.value = true;
|
||
try {
|
||
const dims = LEVEL_DIMS.map((d, i) => ({ dim: `DIM_${i + 1}`, name: d.name, max: Math.max(...d.options.map((o) => o.score)) }));
|
||
const res = await aiLevelScore(desc.value, compareRows(), dims);
|
||
// 返回维度按 name 匹配、匹配不到按顺序对齐回填各维度得分;总分/等级阈值换算沿用页面本地逻辑
|
||
const scores = [...levelScores.value];
|
||
(res?.dims ?? []).forEach((d, i) => {
|
||
const di = LEVEL_DIMS.findIndex((x) => x.name === d.name || x.name.endsWith(d.name));
|
||
const idx = di >= 0 ? di : i;
|
||
if (idx >= 0 && idx < scores.length && typeof d.score === "number") scores[idx] = d.score;
|
||
if (d.basis) levelAiBasis.value[idx] = d.basis;
|
||
});
|
||
levelScores.value = scores;
|
||
levelAiText.value = res?.analysis_text ?? "";
|
||
} catch (e: any) {
|
||
aiFail(e);
|
||
} finally {
|
||
levelAiLoading.value = false;
|
||
}
|
||
};
|
||
// ---------- 变更培训 ----------
|
||
// 注:培训区块暂无独立「AI 生成」触发按钮,保留静态示例内容;
|
||
// 接入时可在此调 aiTraining(desc.value, compareRows(), riskRecords.value),
|
||
// 将 training_text 填入 trainText、suggest_posts 展示为建议培训岗位。
|
||
const trainText = ref(
|
||
`培训对象:R-201 岗位操作人员(4 人)、维修班(2 人)
|
||
培训方式:现场讲解 + 操作演示
|
||
|
||
一、变更内容
|
||
M-201A 搅拌电机由 55kW 更换为 75kW 防爆电机(ExdⅡBT4 → ExdⅡCT4),额定电流 102A → 138A;电缆与开关容量同步升级,联锁跳车电流设定值同步校核调整。
|
||
|
||
二、变化的操作方式
|
||
1. 搅拌器启动:变更前直接启动、观察电流 ≤102A;变更后启动后核对电流 ≤138A,确认变频器参数已更新(注意:首次启动空载试运行 30 分钟)
|
||
2. 高粘度工况操作:变更前粘度 >3000cP 需降速运行;变更后粘度 ≤6000cP 可全速运行(注意:严禁超过 6000cP 上限)
|
||
3. 过载跳闸处置:变更前复位后直接重启;变更后先查明原因并记录,联锁复位需班长确认(注意:跳闸电流设定值已调整)
|
||
|
||
三、潜在风险与应对措施
|
||
1. 风险:电机 / 电缆过热 → 应对:投用首周每班测温并记录,红外点检复核
|
||
2. 风险:联锁设定错误导致拒动 / 误动 → 应对:投用前完成跳车试验并留存记录
|
||
3. 风险:防爆接合面或格兰头密封失效 → 应对:检查防爆接合面与格兰头,严禁带电开盖`
|
||
);
|
||
// ---------- PSSR 检查内容 ----------
|
||
// 注:PSSR 区块暂无独立「AI 生成」触发按钮,保留静态示例清单;
|
||
// 接入时可在此调 aiPssr({ content: desc.value, change_type: form.value.type, risk_records: riskRecords.value }),
|
||
// 将 groups(g, items: content/phase) 映射为 pssrGroups 的 { g, items: { c, p } } 结构。
|
||
const pssrDone = ref<Record<string, { by: string; at: string }>>({});
|
||
const pssrNa = ref<Record<string, boolean>>({});
|
||
const pssrActive = computed(() => pssrGroups.value.filter((g:any) => !pssrNa.value[g.g]));
|
||
const pssrTotal = computed(() => pssrActive.value.reduce((a:any, g:any) => a + g.items.length, 0));
|
||
const pssrDoneCount = computed(() => pssrActive.value.reduce((a:any, g:any) => a + g.items.filter((it:any) => pssrDone.value[it.c]).length, 0));
|
||
const pssrAllDone = computed(() => pssrDoneCount.value === pssrTotal.value && (pssrTotal.value > 0 || (pssrGroups.value.length > 0 && pssrGroups.value.every((g) => !!pssrNa.value[g.g]))));
|
||
const pssrFileRef = ref<any>(null);
|
||
const pssrSheet = ref<any>(null);
|
||
const closeDocs = ref([
|
||
{ name: "PID 图纸(PID-R201-03,受控最新版)", cat: "图纸与规程类", scope: "是", archived: true },
|
||
{ name: "操作规程(含超温应急处置卡)", cat: "图纸与规程类", scope: "是", archived: false },
|
||
{ name: "应急预案修订", cat: "图纸与规程类", scope: "不涉及", archived: false },
|
||
{ name: "总图更新", cat: "图纸与规程类", scope: "否", archived: false },
|
||
{ name: "DCS 联锁台账(新报警 / 联锁值)", cat: "台账与制度类", scope: "是", archived: false },
|
||
{ name: "设备台账与备件清单", cat: "台账与制度类", scope: "是", archived: true },
|
||
{ name: "风险分析报告(HAZOP / 检查表法)", cat: "过程与结果文件类", scope: "是", archived: true },
|
||
{ name: "培训签到与考核记录", cat: "过程与结果文件类", scope: "是", archived: true },
|
||
{ name: "PSSR 检查记录(含现场照片)", cat: "过程与结果文件类", scope: "是", archived: false },
|
||
{ name: "验收报告与化验数据", cat: "过程与结果文件类", scope: "是", archived: false },
|
||
]);
|
||
const onPssrFile = (e: Event) => {
|
||
const input = e.target as HTMLInputElement;
|
||
const f = input.files?.[0]; input.value = "";
|
||
if (!f) return;
|
||
pssrSheet.value = f.name;
|
||
const at = today();
|
||
const n = { ...pssrDone.value };
|
||
pssrGroups.value.forEach((g:any) => { if (!pssrNa.value[g.g]) g.items.forEach((it:any) => { n[it.c] = { by: "纸质确认表上传", at }; }); });
|
||
pssrDone.value = n;
|
||
const archGroup = pssrGroups.value[pssrGroups.value.length - 1];
|
||
if (archGroup && !pssrNa.value[archGroup.g]) {
|
||
closeDocs.value = closeDocs.value.map((d:any) => (d.name.includes("PID") || d.name.includes("设备台账") || d.name.includes("培训签到") ? { ...d, archived: true } : d));
|
||
}
|
||
window.$message?.success(`已上传 PSSR 纸质确认表「${f.name}」:不做内容识别,上传即视为全部检查项完成确认(「不涉及」组除外),原件归档至变更档案`);
|
||
};
|
||
|
||
// ---------- 验收评价 ----------
|
||
// 注:验收清单暂无独立「AI 生成」触发按钮,保留静态示例行;
|
||
// 接入时可在此调 aiAcceptance({ purpose: form.value.purposes.join('、'), effect: form.value.effect }),
|
||
// 将 rows(item/basis/standard/method/owner_suggest) 映射为 acceptRows 行结构(id 自行补,result 默认「待验收」)。
|
||
const acceptRows = ref([
|
||
{ id: 1, item: "反应转化率", basis: "转化率不低于 98%", standard: "转化率 ≥ 98%(连续 3 批化验合格)", method: "化验室逐批取样分析,出具检验报告", owner: "车间 · 张工艺", result: "待验收" },
|
||
{ id: 2, item: "单批反应时间 / 产能", basis: "反应时间缩短 50%、产能提升一倍", standard: "保温时间 4h±0.5h,产能提升 ≥ 80%", method: "DCS 批次记录统计,对比变更前 30 天均值", owner: "车间 · 李主任", result: "待验收" },
|
||
{ id: 3, item: "未知杂质含量", basis: "杂质水平受控、产品质量合格", standard: "杂质 ≤ 0.15%,产品质量指标全部合格", method: "每批化验跟踪,连续 5 批数据趋势评估", owner: "质量 · 王海燕", result: "待验收" },
|
||
{ id: 4, item: "温控与联锁有效性", basis: "温度窗口上调后保护层有效", standard: "温度波动 ≤ ±2℃,高报/联锁动作正确率 100%", method: "DCS 趋势核查 + 联锁测试记录复核", owner: "仪表 · 王仪表", result: "待验收" },
|
||
{ id: 5, item: "设备运行状态", basis: "负荷变化后设备长周期稳定", standard: "搅拌电流、釜壁温度无异常趋势,无泄漏", method: "设备巡检 + 振动/温度记录评估", owner: "设备 · 周设备", result: "待验收" },
|
||
]);
|
||
const acceptNote = ref("");
|
||
|
||
// ---------- 变更关闭确认表 ----------
|
||
const closeFold = ref<Record<string, boolean>>({});
|
||
const uploadDoc = ref<string | null>(null);
|
||
const uploadSel = ref<string | null>(null);
|
||
const UPDATE_DOC_KW: Record<string, string> = { PID图纸: "PID", 操作规程: "操作规程", 总图: "总图", 设备台账: "设备台账", 工艺卡片: "工艺卡片", 联锁台账: "联锁台账", 应急预案: "应急预案" };
|
||
const needUpdate = (name: string) => updateDocs.value.some((d:any) => name.includes(UPDATE_DOC_KW[d] ?? d));
|
||
|
||
const changeScope = (d: any, v: string) => {
|
||
closeDocs.value = closeDocs.value.map((x:any) => (x.name === d.name ? { ...x, scope: v } : x));
|
||
if (v !== d.scope) window.$message?.info(`演示:「${d.name}」改判为「${v}」,改判理由与操作人已留痕`);
|
||
}
|
||
const uploadDocInfo = computed(() => {
|
||
if (!uploadDoc.value) return null;
|
||
const doc = closeDocs.value.find((x) => x.name === uploadDoc.value);
|
||
const base = uploadDoc.value.replace(/[((].*$/, "");
|
||
return {
|
||
storePath: `\\\\PLANT-DMS\\变更管理\\2026\\MOC-2026-R01-0041\\${doc?.cat ?? ""}\\`,
|
||
candidates: [`${base}_RevC_受控版.pdf`, `${base}_2026-08 更新版.docx`],
|
||
base,
|
||
};
|
||
});
|
||
const openDir = () => {
|
||
window.$message?.info('演示:已在文档管理系统中打开该目录')
|
||
}
|
||
const uploadFiles = () => {
|
||
uploadSel.value = `本地文件:${uploadDocInfo.value?.base}_最新版.pdf`;
|
||
window.$message?.info('演示:已接收拖拽 / 选择的本地文件');
|
||
}
|
||
const uploadConfirm = () => {
|
||
closeDocs.value = closeDocs.value.map((x) => (x.name === uploadDoc.value ? { ...x, archived: true } : x));
|
||
window.$message?.success(`「${uploadDoc.value}」已上传并归档(${uploadSel.value},受控版本,哈希留痕)`);
|
||
uploadDoc.value = null;
|
||
}
|
||
|
||
const pluginDrawer = ref<boolean>(false);
|
||
const currentPlugin = ref<string>("");
|
||
// 插件选择
|
||
// 注:风险核查(RISK_CHECK)的「AI 预填检查表」交互在 modules/riskCheck.vue 插件内部,
|
||
// 本页 toggleTool 仅负责打开插件抽屉,无独立 AI 预填入口,故不在此接 aiRiskCheck。
|
||
const toggleTool = (t: any) => {
|
||
if (!tools.value.includes(t.key))
|
||
tools.value = tools.value.includes(t.key) ? tools.value.filter((x) => x !== t.key) : [...tools.value, t.key];
|
||
currentPlugin.value = t.key;
|
||
pluginDrawer.value = true;
|
||
};
|
||
|
||
|
||
const exportAll = (items: any[], mode: "manual" | "ai") => {
|
||
riskRecords.value = [...riskRecords.value, ...items];
|
||
if (mode === "ai") {
|
||
pluginDrawer.value = false;
|
||
currentPlugin.value = '';
|
||
window.$message?.success(`AI 已自动整理形成风险分析记录表(新增 ${items.length} 项),支持手动修改`);
|
||
} else {
|
||
window.$message?.success(`已选入 ${items.length} 条风险记录,可继续挑选或返回查看记录表`);
|
||
}
|
||
}
|
||
const onBack = () => {
|
||
pluginDrawer.value = false;
|
||
}
|
||
|
||
|
||
</script>
|
||
|
||
<template>
|
||
<NSpace vertical :size="16">
|
||
<!-- 发起变更 -->
|
||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px', height: '100%' }">
|
||
<div class="border" :style="{ borderRadius: themeStore.themeRadius + 'px', height: '100%' }">
|
||
<!-- 顶部 -->
|
||
<div
|
||
class="border-b bg-[var(--bg-color)]"
|
||
:style="{
|
||
'--bg-color': getColorWithOpacity(themeStore.themeColor, 0.1)
|
||
}"
|
||
>
|
||
<div
|
||
class="flex flex-wrap items-center gap-x-3 gap-y-2 border-b px-4 py-2 border-[var(--border-color)]"
|
||
:style="{'--border-color': getColorWithOpacity(themeStore.themeColor, 0.2)}"
|
||
>
|
||
<span class="whitespace-nowrap text-sm font-medium text-slate-600">变更名称 <span class="text-red-500">*</span></span>
|
||
<NInput v-model:value="form.name" maxlength="28" show-count placeholder="AI 自动总结(28 字以内),可手动修改" class="!w-420px" />
|
||
<span class="ml-auto whitespace-nowrap text-sm text-slate-500">变更编号</span>
|
||
<NInput value="MOC-2026-R01-0041" disabled class="!w-44" />
|
||
</div>
|
||
<div class="flex items-center gap-1 overflow-x-auto px-3 pt-2">
|
||
<div v-for="t in TABS" :key="t.id" @click="tab = t.id"
|
||
class="flex items-center gap-1.5 whitespace-nowrap px-3 py-2 text-sm transition cursor-pointer border hover:bg-white/60 rounded-t-lg"
|
||
:class="tab === t.id ? 'border-b-transparent border-[var(--border-color)] bg-white text-[var(--active-color)]':'border-transparent text-slate-500'"
|
||
:style="{
|
||
'--border-color': getColorWithOpacity(themeStore.themeColor, 0.3),
|
||
'--active-color': themeStore.themeColor
|
||
}"
|
||
>
|
||
<Icon :icon="t.icon" class="size-14px" />
|
||
{{ t.name }}
|
||
<span v-if="t.id === 'form' && formGen === 'generating'"
|
||
class="flex items-center gap-1 rounded-full border border-violet-300 bg-violet-50 px-1.5 py-0.5 text-[10px] text-violet-600">
|
||
<Icon icon="ri:loader-4-fill" class="size-14px animate-spin" />
|
||
AI 生成中
|
||
</span>
|
||
<Icon v-if="confirmed[t.id]" icon="material-symbols:check-circle-outline" class="size-14px text-emerald-500" />
|
||
<span v-if="!confirmed[t.id] && blockedTabs.includes(t.id)" class="h-1.5 w-1.5 rounded-full bg-red-500" title="本页尚未确认完毕" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 中间 -->
|
||
<div class="p-4">
|
||
<div class="space-y-4" :class="tab === 'pre'?'scroll':'scroll2'">
|
||
<!-- ===== 变更预识别 ===== -->
|
||
<template v-if="tab === 'pre'">
|
||
<!-- 变更内容描述 -->
|
||
<div class="border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||
<div class="flex items-center justify-between px-4 py-2 border-b">
|
||
<div class="flex items-center">
|
||
<Icon icon="lucide:sparkles" class="size-16px text-violet-500" />
|
||
<span class="font-semibold text-base ml-2">变更内容描述</span>
|
||
</div>
|
||
<NButton v-if="aiEnabled" ghost type="primary" @click="runIdentify" :disabled="analyzing">
|
||
<Icon v-if="analyzing" icon="ri:loader-4-fill" class="size-16px animate-spin" />
|
||
<Icon v-else icon="fluent:wand-24-regular" class="size-16px" />
|
||
变更识别
|
||
</NButton>
|
||
<NTag type="info" v-else>AI 未启用</NTag>
|
||
</div>
|
||
<div class="p-4">
|
||
<NInput type="textarea" v-model:value="desc" rows="4" placeholder="请输入变更内容描述,例如:将反应釜R-101的搅拌速度从120rpm提高至180rpm,同时将反应温度从80℃提高至95℃…" />
|
||
<div v-if="!aiEnabled" class="mt-2 flex items-center gap-2 rounded-lg bg-amber-50 px-3 py-2 text-xs text-amber-700">
|
||
<Icon icon="proicons:alert-triangle" class="size-14px" />
|
||
AI 辅助功能未启用(可在「系统设置 → AI 辅助功能」开启),识别结果与申请表需手动填写。
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 变更识别结果 -->
|
||
<div class="border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||
<div class="flex items-center justify-between px-4 py-2 border-b">
|
||
<div class="flex items-center">
|
||
<Icon icon="lucide:list-checks" class="size-16px text-emerald-500" />
|
||
<span class="font-semibold text-base ml-2">{{rows ? `变更识别结果(${rows.length}项)` : '变更识别结果(空表 · 可手动录入,或由 AI 识别生成)'}}</span>
|
||
</div>
|
||
<span v-if="rows" class="inline-flex items-center gap-1 rounded-full border border-violet-200 bg-violet-100 px-2 py-0.5 text-xs font-medium text-violet-700">
|
||
✨ AI 生成 · 需人工确认
|
||
</span>
|
||
</div>
|
||
<div class="p-4">
|
||
<div class="overflow-x-auto">
|
||
<table class="w-full min-w-[640px] border-collapse text-sm">
|
||
<thead>
|
||
<tr class="bg-slate-50 text-left text-slate-500">
|
||
<th class="w-28 border-b px-3 py-2 font-medium">变更项目</th>
|
||
<th class="border-b px-3 py-2 font-medium">变更前</th>
|
||
<th class="border-b px-3 py-2 font-medium">变更后</th>
|
||
<th class="w-10 border-b px-2 py-2" />
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="row in rows ?? []" :key="row.id" class="align-top">
|
||
<td class="border-b px-3 py-3">
|
||
<NInput
|
||
v-if="editing === `${row.id}-item`"
|
||
class="text-left"
|
||
type="textarea"
|
||
size="small"
|
||
autofocus
|
||
v-model:value="row.item"
|
||
:autosize="{ minRows: 1 }"
|
||
placeholder="请输入"
|
||
@blur="editing = null"
|
||
/>
|
||
<p v-else class="font-medium text-slate-700 hover:text-[#17407F] cursor-pointer" @click="editing = `${row.id}-item`" title="点击修改项目名称">
|
||
{{ row.item }}
|
||
</p>
|
||
</td>
|
||
<td v-for="field in (['before', 'after'])" :key="field" class="border-b px-3 py-3">
|
||
<NInput
|
||
v-if="editing === `${row.id}-${field}`"
|
||
class="text-left"
|
||
type="textarea"
|
||
size="small"
|
||
autofocus
|
||
v-model:value="row[field]"
|
||
:autosize="{ minRows: 1 }"
|
||
placeholder="请输入"
|
||
@blur="editing = null"
|
||
/>
|
||
<p v-else class="min-h-30px cursor-pointer rounded-lg border bg-slate-50/60 px-2.3 py-0.55 text-left text-sm leading-relaxed text-slate-700 transition hover:border-[#6E93C6] hover:bg-white" @click="editing = `${row.id}-${field}`" title="点击编辑">
|
||
<HighlightText v-if="field === 'after'" :text="row[field]" :hl="row.hl" />
|
||
<span v-else class="whitespace-pre-line">{{ row[field] }}</span>
|
||
</p>
|
||
</td>
|
||
<td class="border-b px-2 py-3 text-center">
|
||
<NButton text @click="rows = rows && rows.filter((x) => x.id !== row.id)" class="text-slate-300 hover:text-red-500" title="删除此行">
|
||
<Icon icon="line-md:close-circle" class="size-16px" />
|
||
</NButton>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="mt-3">
|
||
<NButton ghost
|
||
@click="rows = [...(rows ?? []), { id: Date.now(), item: '新项目', before: '', after: '' }]">
|
||
<Icon icon="ic:round-plus" class="size-16px" /> 添加行
|
||
</NButton>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 风险预分析报告 -->
|
||
<div v-if="preRiskShown" class="border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||
<div class="flex items-center justify-between px-4 py-2 border-b">
|
||
<div class="flex items-center">
|
||
<Icon icon="lucide:list-checks" class="size-16px text-emerald-500" />
|
||
<span class="font-semibold text-base ml-2">风险预分析报告</span>
|
||
</div>
|
||
<span class="inline-flex items-center gap-1 rounded-full border border-violet-200 bg-violet-100 px-2 py-0.5 text-xs font-medium text-violet-700">
|
||
✨ AI 生成 · 需人工确认
|
||
</span>
|
||
</div>
|
||
<div class="p-4">
|
||
<div class="space-y-4">
|
||
<div v-for="s in ([{ key: 'severity', name: '危害严重性分析' }, { key: 'probability', name: '事件概率分析' }, { key: 'protection', name: '保护措施影响分析' }] as const)" :key="s.key" class="rounded-lg border">
|
||
<div class="flex items-center gap-2 border-b bg-slate-50/60 px-4 py-2">
|
||
<span class="text-sm font-semibold text-slate-700">{{ s.name }}</span>
|
||
<NButton text type="primary" class="ml-auto text-xs hover:underline" @click="preRiskEdit = preRiskEdit === s.key ? null : s.key">
|
||
{{ preRiskEdit === s.key ? "完成" : "编辑" }}
|
||
</NButton>
|
||
</div>
|
||
<NInput v-if="preRiskEdit === s.key" type="textarea" :bordered="false" v-model:value="preRiskText[s.key]" autofocus rows="6" />
|
||
<p v-else class="whitespace-pre-wrap px-3 py-1.5 text-sm leading-relaxed text-slate-700">{{ preRiskText[s.key] }}</p>
|
||
</div>
|
||
<div class="rounded-lg bg-amber-50 px-3 py-2 text-xs leading-5 text-amber-700">
|
||
AI 风险预分析仅供参考,用于申请阶段的快速研判;正式风险识别请前往「风险分析记录表」标签页,使用 HAZOP / JSA / 检查表法开展并留存记录。
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<!-- ===== 变更申请表 ===== -->
|
||
<template v-if="tab === 'form' && form">
|
||
<!-- 申请部门 / 申请人 -->
|
||
<div class="grid gap-4 grid-cols-2">
|
||
<div class="flex gap-3">
|
||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">申请部门 <span class="text-red-500">*</span></div>
|
||
<NInput v-model:value="form.dept" placeholder="请输入申请部门" />
|
||
</div>
|
||
<div class="flex gap-3">
|
||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">申请人</div>
|
||
<NInput v-model:value="ROLES[0].person" disabled />
|
||
</div>
|
||
</div>
|
||
<!-- 变更目的 -->
|
||
<div class="flex gap-3">
|
||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">变更目的</div>
|
||
<div class="rounded-lg border border-[#ACC6E5] bg-[#EAF0F9]/40 p-3">
|
||
<NCheckboxGroup v-model:value="form.purposes">
|
||
<div class="flex flex-wrap gap-y-2 gap-x-4">
|
||
<NCheckbox v-for="p in PURPOSE_OPTIONS" :key="p" :value="p" :label="p" />
|
||
</div>
|
||
</NCheckboxGroup>
|
||
</div>
|
||
</div>
|
||
<!-- 预期效果 -->
|
||
<div class="flex gap-3">
|
||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">预期效果</div>
|
||
<NInput type="textarea" v-model:value="form.effect" rows="2" placeholder="" />
|
||
</div>
|
||
<!-- 主要变更内容 -->
|
||
<div class="flex gap-3">
|
||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">主要变更内容 <span class="text-red-500">*</span></div>
|
||
<div class="flex-1 space-y-2">
|
||
<div v-for="(m, i) in form.mainChanges" :key="i" class="flex items-center gap-2">
|
||
<span class="w-5 shrink-0 text-sm text-slate-400">{{ i + 1 }}.</span>
|
||
<NInput v-model:value="m as string" placeholder="" />
|
||
<NButton text type="error" @click="form = { ...form, mainChanges: form.mainChanges.filter((_, j) => j !== i) }">
|
||
<Icon icon="line-md:close-circle" class="size-16px" />
|
||
</NButton>
|
||
</div>
|
||
<NButton text type="primary" class="text-xs" @click="form = { ...form, mainChanges: [...form.mainChanges, ''] }">
|
||
<Icon icon="ic:round-plus" class="size-14px" /> 添加变更点
|
||
</NButton>
|
||
</div>
|
||
</div>
|
||
<!-- 连带变更内容 -->
|
||
<div class="flex gap-3">
|
||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600">连带变更内容</div>
|
||
<div class="flex-1 space-y-2">
|
||
<div v-for="(m, i) in (form.related ? form.related.split('、') : [''])" :key="i" class="flex items-center gap-2">
|
||
<span class="w-5 shrink-0 text-sm text-slate-400">{{ i + 1 }}.</span>
|
||
<NInput v-model:value="m as string" placeholder="连带变更点(如:更新操作规程)" />
|
||
<NButton text type="error" @click="form = { ...form, related: (form.related ? form.related.split('、') : ['']).filter((_, j) => j !== i).join('、') }">
|
||
<Icon icon="line-md:close-circle" class="size-16px" />
|
||
</NButton>
|
||
</div>
|
||
<NButton text type="primary" class="text-xs" @click="form = { ...form, related: form.related ? `${form.related}、` : '、' }">
|
||
<Icon icon="ic:round-plus" class="size-14px" /> 添加连带变更点
|
||
</NButton>
|
||
</div>
|
||
</div>
|
||
<!-- 变更类型 / 变更等级 -->
|
||
<div class="grid gap-4 grid-cols-2">
|
||
<div class="flex gap-3">
|
||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">变更类型 <span class="text-red-500">*</span></div>
|
||
<div class="flex flex-1 items-center gap-4 rounded-lg bg-slate-50 px-3 py-2">
|
||
<NRadioGroup v-model:value="form.type">
|
||
<div class="flex gap-x-2">
|
||
<NRadio v-for="t in ['工艺', '设备', '管理']" :key="t" :value="t">{{t}}</NRadio>
|
||
</div>
|
||
</NRadioGroup>
|
||
<NButton text type="primary" class="ml-auto text-xs hover:underline" @click="openTypeSheet">
|
||
<Icon icon="lucide:sparkles" class="size-12px" />
|
||
AI 判定说明
|
||
</NButton>
|
||
</div>
|
||
</div>
|
||
<div class="flex gap-3">
|
||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">变更等级 <span class="text-red-500">*</span></div>
|
||
<div class="flex flex-1 items-center gap-4 rounded-lg bg-slate-50 px-3 py-2">
|
||
<NRadioGroup v-model:value="form.level">
|
||
<div class="flex gap-x-2">
|
||
<NRadio v-for="t in ['一般', '重要']" :key="t" :value="t">{{t}}</NRadio>
|
||
</div>
|
||
</NRadioGroup>
|
||
<NButton text type="primary" class="ml-auto text-xs hover:underline" @click="openLevelSheet">
|
||
<Icon icon="lucide:list-checks" class="size-12px" /> 等级判定表 ({{ totalScore }}分) →
|
||
</NButton>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 变更时限 / 风险分析 -->
|
||
<div class="grid gap-4 grid-cols-2">
|
||
<div class="flex gap-3">
|
||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">变更时限 <span class="text-red-500">*</span></div>
|
||
<div class="flex flex-1 items-center gap-4 rounded-lg bg-slate-50 px-3 py-2">
|
||
<NRadioGroup v-model:value="form.duration">
|
||
<div class="flex gap-x-2">
|
||
<NRadio v-for="t in ['永久', '临时']" :key="t" :value="t">{{t}}</NRadio>
|
||
</div>
|
||
</NRadioGroup>
|
||
<NCheckbox v-model:checked="form.urgent">紧急</NCheckbox>
|
||
</div>
|
||
</div>
|
||
<div class="flex gap-3">
|
||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">风险分析</div>
|
||
<div class="flex flex-1 flex-wrap items-center gap-2 rounded-lg bg-slate-50 px-3 py-2">
|
||
<NButton size="small" round v-for="t in RISK_TOOLS_FORM" :key="t.key" @click="toggleTool(t.key)" title="点击选用 / 取消;AI 辅助分析在各工具插件内使用"
|
||
class="text-sm"
|
||
:ghost="tools.includes(t.key) ? false : true"
|
||
:type="tools.includes(t.key) ? 'primary' : 'default'"
|
||
>
|
||
{{ t.label }}
|
||
</NButton>
|
||
<NButton type="primary" ghost dashed size="small" round @click="recommendTools" class="text-xs">
|
||
按等级重新推荐
|
||
</NButton>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 计划投用时间 -->
|
||
<div class="flex gap-3">
|
||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">计划投用时间</div>
|
||
<div class="flex flex-1 flex-wrap items-center gap-2">
|
||
<NDatePicker v-model:value="form.date" type="date" class="!max-w-[220px]" />
|
||
<template v-if="form.duration === '临时'">
|
||
<span class="whitespace-nowrap text-sm text-slate-500">计划恢复时间 <span class="text-red-500">*</span></span>
|
||
<NDatePicker v-model:value="form.restoreDate" type="date" class="!max-w-[220px]" />
|
||
<span class="text-xs text-amber-600">临时变更到期系统自动提醒恢复原状</span>
|
||
</template>
|
||
<span v-else class="whitespace-nowrap text-xs text-slate-300">(勾选「临时」后在此并排填写计划恢复时间)</span>
|
||
</div>
|
||
</div>
|
||
<div v-if="form.urgent" class="text-xs text-red-500">紧急变更为单独标记:走快速通道,事后限期补办风险分析与完整审批</div>
|
||
<!-- 所需材料 -->
|
||
<div class="flex gap-3">
|
||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">所需材料</div>
|
||
<div class="flex-1 space-y-2">
|
||
<div class="text-[11px] text-slate-400">施工所需材料(生成变更申请表时系统自动生成,支持手动修改)</div>
|
||
<NInput type="textarea" v-model:value="materialsText" rows="4" placeholder="点击「生成变更申请单」后由系统按变更内容自动生成,也可直接填写" />
|
||
</div>
|
||
</div>
|
||
<!-- 资料上传 -->
|
||
<div class="flex gap-3">
|
||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">资料上传</div>
|
||
<div class="flex flex-1 flex-wrap items-center gap-2 rounded-lg border border-dashed p-3">
|
||
<NButton size="small" class="text-xs" v-for="f in files" :key="f" @click="previewFile = f" title="点击在线预览">
|
||
<Icon icon="akar-icons:file" class="size-14px" />
|
||
{{ f }}
|
||
</NButton>
|
||
<NButton @click="files = [...files, `资料_${files.length + 1}.pdf`]">
|
||
<Icon icon="material-symbols:upload" class="size-16px" /> 资料上传
|
||
</NButton>
|
||
<span class="text-xs text-slate-400">(点击附件名弹窗预览,支持 PDF / Word / 图片在线查看)</span>
|
||
</div>
|
||
</div>
|
||
<!-- 需更新的资料 -->
|
||
<div class="flex gap-3">
|
||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600">需更新的资料</div>
|
||
<div class="flex flex-1 flex-wrap items-center gap-x-4 gap-y-1.5 rounded-lg bg-slate-50 px-3 py-2">
|
||
<NCheckboxGroup v-model:value="updateDocs">
|
||
<div class="flex flex-wrap gap-y-2 gap-x-4">
|
||
<NCheckbox v-for="p in ['PID图纸', '操作规程', '总图', '设备台账', '工艺卡片', '联锁台账', '应急预案']" :key="p" :value="p" :label="p" />
|
||
</div>
|
||
</NCheckboxGroup>
|
||
<span class="self-center text-xs text-slate-400">(多选,变更关闭前逐项确认上传最新版本)</span>
|
||
</div>
|
||
</div>
|
||
<!-- 涉及专业 -->
|
||
<div class="flex gap-3">
|
||
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600">涉及专业</div>
|
||
<div class="flex-1">
|
||
<div class="flex flex-wrap items-center gap-x-4 gap-y-1.5 rounded-lg bg-slate-50 px-3 py-2">
|
||
<NCheckboxGroup v-model:value="disciplines" @update:value="toggleDiscipline">
|
||
<div class="flex flex-wrap gap-y-2 gap-x-4">
|
||
<NCheckbox v-for="p in ['技术', '仪表', '电气', '环保', '质量']" :key="p" :value="p" :label="p" />
|
||
</div>
|
||
</NCheckboxGroup>
|
||
<span class="self-center text-xs text-slate-400">(多选,决定审批流中专业会签/审批的路由范围)</span>
|
||
</div>
|
||
<div v-if="Object.keys(signers).length > 0" class="mt-1.5 flex flex-wrap items-center gap-1.5 text-xs text-slate-500">
|
||
<Icon icon="lucide:users" class="size-12px" />已联动专业会签:
|
||
<NTag size="small" type="info" v-for="(p, d) in signers" :key="d">{{ d }} · {{ p }}</NTag>
|
||
<span class="text-slate-400">(在底部「审批流程设置」中可调整人选)</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</template>
|
||
<!-- ===== 风险分析记录表 ===== -->
|
||
<template v-if="tab === 'risk'">
|
||
<div class="border p-3" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||
<div class="flex flex-wrap items-center gap-2">
|
||
<span class="flex items-center gap-1.5 text-sm font-semibold text-slate-700">
|
||
<Icon icon="lucide:shield-alert" class="size-16px" />
|
||
风险识别工具(按变更等级匹配)
|
||
</span>
|
||
<NTag size="small" :type="form?.level === '重要' ? 'error' : 'default'">
|
||
{{ form?.level === '重要' ? '重要变更' : '一般变更' }}
|
||
</NTag>
|
||
<span class="text-xs text-slate-400">
|
||
{{ form?.level === '重要' ? '规则:HAZOP 必做,同时开展 JSA 与检查表法(SCL)' : '规则:JSA + 检查表法(SCL),涉及工艺安全边界的应升级 HAZOP' }};可在申请表「风险分析」栏手动重选
|
||
</span>
|
||
</div>
|
||
<div class="grid gap-2 grid-cols-4 mt-3">
|
||
<div v-for="t in TOOL_CARDS" :key="t.key" :title="t.desc"
|
||
class="flex items-center gap-2 rounded-lg border px-3 py-2 transition"
|
||
:class="pluginDone[t.key] !== undefined ? 'border-[var(--theme-color)] bg-[var(--theme-color)]' : tools.includes(t.key) ? 'border-[var(--theme-color)] bg-white' : 'border-slate-200 bg-white'"
|
||
:style="{'--theme-color': themeStore.themeColor}"
|
||
>
|
||
<div class="min-w-0 flex-1">
|
||
<div
|
||
class="flex items-center gap-1.5 text-xs font-semibold"
|
||
:class="pluginDone[t.key] !== undefined ? 'text-white' : tools.includes(t.key) ? 'text-[var(--theme-color)]' : 'text-slate-500'"
|
||
:style="{'--theme-color': themeStore.themeColor}"
|
||
>
|
||
<Icon icon="fluent:wand-24-regular" class="size-16px" />
|
||
{{ t.name }}
|
||
</div>
|
||
<div class="mt-0.5 text-[10px]" :class="pluginDone[t.key] !== undefined ? 'text-white/75' : 'text-slate-400'">
|
||
{{ pluginDone[t.key] !== undefined ? `已使用并分析了 ${pluginDone[t.key]} 条内容` : '未使用此工具分析' }}
|
||
</div>
|
||
</div>
|
||
<NButton size="small" class="text-xs" :type="pluginDone[t.key] !== undefined ? 'default' : tools.includes(t.key) ? 'primary' : 'default'"
|
||
@click="toggleTool(t)">
|
||
使用
|
||
</NButton>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||
<div class="flex flex-wrap items-center gap-2 bg-slate-100 px-3 py-2">
|
||
<span class="flex items-center gap-1.5 text-sm font-medium text-slate-700">
|
||
<Icon icon="proicons:alert-triangle" class="size-16px text-amber-500" />
|
||
风险分析记录表
|
||
</span>
|
||
<span class="text-[11px] text-slate-400">共 {{ riskRecords.length }} 项</span>
|
||
<span class="ml-auto hidden text-[11px] text-slate-400 lg:block">插件结果「入表」或 AI 自动整理形成;单元格点击修改,风险等级点击切换</span>
|
||
<NButton ghost size="small" color="#7c3aed" class="text-xs" @click="toPssrAll">
|
||
<Icon icon="tabler:clipboard-check" class="size-14px" />
|
||
全部转 PSSR 行动项
|
||
</NButton>
|
||
<NButton ghost size="small" type="primary" class="text-xs" @click="aiOrganize">
|
||
<Icon icon="lucide:sparkles" class="size-12px" />
|
||
AI 自动整理
|
||
</NButton>
|
||
</div>
|
||
<div v-if="riskRecords.length === 0" class="px-3 py-6 text-center text-xs text-slate-400">
|
||
尚无风险记录:调用上方插件完成分析后逐条「入表」,或点击「AI 自动整理」形成记录表
|
||
</div>
|
||
<div v-else class="overflow-x-auto p-3">
|
||
<table class="w-full min-w-[960px] border-collapse text-xs">
|
||
<thead>
|
||
<tr>
|
||
<th v-for="h in ['风险项', '场景描述', '固有风险', '现有措施', '建议措施', '剩余风险', '来源', '操作']" :key="h"
|
||
class="border bg-slate-50 px-2 py-1.5 text-[11px] font-medium text-slate-500">{{ h }}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="(rec, i) in riskRecords" :key="i">
|
||
<td class="border px-1.5 py-1 align-top font-medium text-slate-700">
|
||
<Ed :v="rec.item" @update="(x: string) => updRec(i, { item: x })" />
|
||
</td>
|
||
<td class="border px-1.5 py-1 align-top"><Ed :v="rec.scene" @update="(x: string) => updRec(i, { scene: x })" /></td>
|
||
<td class="border px-1.5 py-1 text-center align-top">
|
||
<NTag v-if="rec.inherent==='高'" type="error" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { inherent: lvlNext(rec.inherent) as '高' | '中' | '低' })">
|
||
{{ rec.inherent }}
|
||
</NTag>
|
||
<NTag v-if="rec.inherent==='中'" type="warning" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { inherent: lvlNext(rec.inherent) as '高' | '中' | '低' })">
|
||
{{ rec.inherent }}
|
||
</NTag>
|
||
<NTag v-if="rec.inherent==='低'" type="success" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { inherent: lvlNext(rec.inherent) as '高' | '中' | '低' })">
|
||
{{ rec.inherent }}
|
||
</NTag>
|
||
</td>
|
||
<td class="border px-1.5 py-1 align-top"><Ed :v="rec.existing" @update="(x: string) => updRec(i, { existing: x })" /></td>
|
||
<td class="border px-1.5 py-1 align-top"><Ed :v="rec.suggest" @update="(x: string) => updRec(i, { suggest: x })" /></td>
|
||
<td class="border px-1.5 py-1 text-center align-top">
|
||
<NTag v-if="rec.residual==='高'" type="error" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { residual: lvlNext(rec.residual) as '高' | '中' | '低' })">
|
||
{{ rec.residual }}
|
||
</NTag>
|
||
<NTag v-if="rec.residual==='中'" type="warning" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { residual: lvlNext(rec.residual) as '高' | '中' | '低' })">
|
||
{{ rec.residual }}
|
||
</NTag>
|
||
<NTag v-if="rec.residual==='低'" type="success" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { residual: lvlNext(rec.residual) as '高' | '中' | '低' })">
|
||
{{ rec.residual }}
|
||
</NTag>
|
||
</td>
|
||
<td class="border px-1.5 py-1 text-center align-top">
|
||
<NTag size="small" type="info">{{ rec.source }}</NTag>
|
||
</td>
|
||
<td class="border px-1.5 py-1 text-center align-top">
|
||
<NTag v-if="rec.pssr" size="small" :color="{ color: '#f5f3ff', textColor: '#7c3aed', borderColor: '#f5f3ff' }">已转 PSSR</NTag>
|
||
<NButton text size="tiny" color="#7c3aed" v-else class="hover:underline" @click="toPssr(i)">转 PSSR</NButton>
|
||
<NButton text type="error" size="tiny" class="mt-2" @click="riskRecords = riskRecords.filter((_, j) => j !== i)">删除</NButton>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<!-- 变更培训内容 -->
|
||
<template v-if="tab === 'train'">
|
||
<div class="flex flex-wrap items-center gap-3 mb-3">
|
||
<span class="flex items-center gap-1.5 text-sm font-semibold text-slate-700">
|
||
<Icon icon="lucide:graduation-cap" class="size-16px text-violet-500" />
|
||
变更培训内容
|
||
</span>
|
||
<span class="inline-flex items-center gap-1 rounded-full border border-violet-200 bg-violet-100 px-2 py-0.5 text-xs font-medium text-violet-700">
|
||
✨ AI 生成 · 需人工确认
|
||
</span>
|
||
<span class="ml-auto text-xs text-slate-400">AI 生成内容均可点击修改自定义;审批通过后,相关人员在 PC / 移动端完成学习与签到确认</span>
|
||
</div>
|
||
<!-- 培训内容 -->
|
||
<div class="border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||
<div class="flex flex-wrap items-center gap-2 bg-slate-100 px-3 py-2">
|
||
<span class="text-sm font-medium text-slate-700">培训内容</span>
|
||
<span class="text-[11px] text-slate-400">含培训对象、培训方式、变更内容、操作方式、潜在风险与应对措施,一个文本框自动分段换行</span>
|
||
<span class="ml-auto text-[11px] text-slate-400">内容支持直接修改</span>
|
||
</div>
|
||
<NInput v-model:value="trainText" type="textarea" :bordered="false" :autosize="{minRows: 4}" class="font-mono text-sm" placeholder="" />
|
||
</div>
|
||
</template>
|
||
<!-- PSSR 检查内容 -->
|
||
<template v-if="tab === 'pssr'">
|
||
<div class="flex flex-wrap items-center gap-3">
|
||
<span class="flex items-center gap-1.5 text-sm font-semibold text-slate-700">
|
||
<Icon icon="tabler:clipboard-check" class="size-16px text-violet-500" />
|
||
PSSR 投用前安全检查内容
|
||
</span>
|
||
<span class="inline-flex items-center gap-1 rounded-full border border-violet-200 bg-violet-100 px-2 py-0.5 text-xs font-medium text-violet-700">
|
||
✨ AI 生成 · 需人工确认
|
||
</span>
|
||
<NTag size="small" :type="pssrAllDone ? 'success' : 'info'">
|
||
已完成 {{ pssrDoneCount }}/{{ pssrTotal }} 项
|
||
</NTag>
|
||
<span class="flex items-center gap-2">
|
||
<NButton ghost size="small" class="text-xs" title="上传 PSSR 纸质确认表照片 / 扫描件(JPG / PNG / PDF),系统不做内容识别,上传即视为全部检查项完成确认" @click="pssrFileRef?.click()">
|
||
<Icon icon="material-symbols:upload" class="size-14px" />
|
||
上传纸质确认表
|
||
</NButton>
|
||
<input ref="pssrFileRef" type="file" accept="image/*,.pdf" class="hidden" @change="onPssrFile" />
|
||
<span class="hidden text-xs text-slate-400 xl:inline">申请阶段:点击条目内容直接编辑,可增删项,完成时点互斥选择;审批通过后逐项点击确认(PC / 移动端均可),全部完成自动推送属地负责人</span>
|
||
</span>
|
||
</div>
|
||
<div v-if="pssrSheet" class="flex items-center gap-2 rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-2 text-xs text-emerald-700">
|
||
<Icon icon="ix:success" class="size-16px" />
|
||
已上传 PSSR 纸质确认表「{{ pssrSheet }}」:上传即视为全部检查项完成确认(未做内容识别),原件已归档至变更档案
|
||
</div>
|
||
<div v-if="pssrAllDone" class="flex items-center gap-2 rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-2.5 text-xs text-emerald-700">
|
||
<Icon icon="ix:success" class="size-16px" />
|
||
全部 PSSR 检查项已确认完成,「具备投用条件」信息已自动推送属地负责人(车间 / 装置主任),可进行投用确认
|
||
</div>
|
||
|
||
<div v-for="(g, gi) in pssrGroups" :key="g.g" class="border" :class="pssrNa[g.g] ? 'opacity-70' : ''" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||
<div class="flex items-center bg-slate-100 px-3 py-2 text-sm font-medium text-slate-700">
|
||
{{ g.g }}
|
||
<NTag v-if="pssrNa[g.g]" size="small" class="ml-2">不涉及</NTag>
|
||
<span v-else class="ml-2 text-xs font-normal text-slate-400">{{ g.items.filter((it) => pssrDone[it.c]).length }}/{{ g.items.length }} 已确认</span>
|
||
<NButton text type="primary" class="ml-auto text-xs font-normal hover:underline"
|
||
:title="pssrNa[g.g] ? '恢复本组检查清单' : '本次变更不涉及本组内容时点击标记,标记后不计入完成统计'"
|
||
@click="pssrNa = { ...pssrNa, [g.g]: !pssrNa[g.g] }">
|
||
{{ pssrNa[g.g] ? '恢复清单' : '标记不涉及' }}
|
||
</NButton>
|
||
<NButton text type="primary" v-if="!pssrNa[g.g]" class="ml-3 text-xs font-normal hover:underline"
|
||
@click="pssrGroups = pssrGroups.map((x, xi) => xi === gi ? { ...x, items: [...x.items, { c: '新检查项(点击直接编辑内容)', p: '投用前' }] } : x)">
|
||
<Icon icon="ic:round-plus" class="size-14px" /> 添加检查项
|
||
</NButton>
|
||
</div>
|
||
<div v-if="pssrNa[g.g]" class="border-t bg-slate-50 px-3 py-3 text-xs text-slate-400">
|
||
本组内容经评估本次变更不涉及(如:无现场安装作业 / 不改变保护措施 / 无需归档资料),已不计入 PSSR 完成统计;如需恢复检查清单,点击右上角「恢复清单」。
|
||
</div>
|
||
<template v-else>
|
||
<div v-for="(it, i) in g.items" :key="`${gi}-${i}`" class="flex flex-wrap items-center gap-3 border-t px-3 py-2.5 text-sm">
|
||
<span class="w-5 text-slate-400">{{ i + 1 }}</span>
|
||
<NInput
|
||
v-if="editing === `pssr-c-${gi}-${i}`"
|
||
class="flex-1"
|
||
v-model:value="it.c"
|
||
type="textarea"
|
||
autofocus
|
||
rows="2"
|
||
@blur="editing = null"
|
||
/>
|
||
<button v-else @click="editing = `pssr-c-${gi}-${i}`" title="点击编辑内容"
|
||
class="min-w-0 flex-1 rounded-md px-1 text-left text-slate-800 transition hover:bg-[#EAF0F9] bg-transparent"
|
||
>
|
||
{{ it.c }}
|
||
</button>
|
||
<span class="flex items-center gap-1 py-1">
|
||
<NRadio :checked="it.p === '投用前'" :value="'投用前'" label="投用前完成"
|
||
@change="pssrGroups = pssrGroups.map((x, xi) => xi === gi ? { ...x, items: x.items.map((y, yi) => yi === i ? { ...y, p: '投用前' } : y) } : x)" />
|
||
<NRadio :checked="it.p === '关闭前'" :value="'关闭前'" label="关闭前完成"
|
||
@change="pssrGroups = pssrGroups.map((x, xi) => xi === gi ? { ...x, items: x.items.map((y, yi) => yi === i ? { ...y, p: '关闭前' } : y) } : x)" />
|
||
</span>
|
||
<NButton text type="error" title="删除该项"
|
||
@click="pssrGroups = pssrGroups.map((x, xi) => xi === gi ? { ...x, items: x.items.filter((_, yi) => yi !== i) } : x)"
|
||
>
|
||
<Icon icon="iconamoon:trash" class="size-16px" />
|
||
</NButton>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
<div class="rounded-lg bg-violet-50/70 px-4 py-2.5 text-xs text-violet-700">
|
||
完成时点说明:「投用前完成 / 关闭前完成」为互斥选择,点击即可切换。投用前完成的项未确认时属地负责人无法确认投用;关闭前完成的项不阻塞投用,但纳入关闭校验清单,未完成时变更主管部门专责无法确认关闭。
|
||
</div>
|
||
<div class="rounded-lg bg-violet-50/70 px-4 py-2.5 text-xs text-violet-700">
|
||
清单由 AI 依据变更类型与风险分析自动生成,专业人员审核后随审批发布;确认方式不作硬性要求——可在 PC 端直接点击确认,也可在移动端拍照上传作为佐证(系统自动记录确认人与日期);资料归档组确认后自动同步「变更关闭确认表」显示已归档。
|
||
</div>
|
||
</template>
|
||
<!-- 验收评价 -->
|
||
<template v-if="tab === 'accept'">
|
||
<div class="flex flex-wrap items-center gap-3">
|
||
<span class="flex items-center gap-1.5 text-sm font-semibold text-slate-700">
|
||
<Icon icon="ix:success" class="size-18px text-emerald-500" />
|
||
验收评价(依据预期效果设定可量化验收标准)
|
||
</span>
|
||
<span class="inline-flex items-center gap-1 rounded-full border border-violet-200 bg-violet-100 px-2 py-0.5 text-xs font-medium text-violet-700">
|
||
✨ AI 生成 · 需人工确认
|
||
</span>
|
||
<span class="ml-auto text-xs text-slate-400">申请阶段由 AI 依据「预期效果」生成量化标准与验收方式,可手动修改;投用运行后由车间和各专业按标准验收并给出评估结论</span>
|
||
</div>
|
||
<div
|
||
class="space-y-1.5 rounded-lg border border-[var(--border-color)] bg-[var(--bg-color)] px-4 py-3 text-xs leading-6 text-slate-700"
|
||
:style="{
|
||
'--bg-color': getColorWithOpacity(themeStore.themeColor, 0.1),
|
||
'--border-color': getColorWithOpacity(themeStore.themeColor, 0.2)
|
||
}"
|
||
>
|
||
<div><b :style="{color: themeStore.themeColor}">变更目的:</b>{{ form?.purposes?.join('、') || '提高生产效能、改善操作条件' }}</div>
|
||
<div><b :style="{color: themeStore.themeColor}">预期效果:</b>{{ form?.effect || '在保持转化率不低于98%的前提下,反应时间缩短50%,产能提升一倍' }}</div>
|
||
<div
|
||
class="border-t border-[var(--border-color)] pt-1.5 text-slate-500"
|
||
:style="{'--border-color': getColorWithOpacity(themeStore.themeColor, 0.2)}"
|
||
>
|
||
<b :style="{color: themeStore.themeColor}">对应说明:</b>验收项目由预期效果逐条拆解设定,各项目对应的预期效果来源见表格「验收项目」列下方灰色小字(点击可修改);验收时须逐项对照预期效果判定达成程度。
|
||
</div>
|
||
</div>
|
||
|
||
<div class="overflow-x-auto border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||
<table class="w-full min-w-[900px] border-collapse text-sm">
|
||
<thead>
|
||
<tr class="bg-slate-100 text-left text-slate-600">
|
||
<th class="w-8 px-3 py-2 font-medium">#</th>
|
||
<th class="w-56 px-3 py-2 font-medium">验收项目(含对应预期效果)</th>
|
||
<th class="px-3 py-2 font-medium">量化验收标准</th>
|
||
<th class="px-3 py-2 font-medium">验收方式(可执行)</th>
|
||
<th class="w-32 px-3 py-2 font-medium">责任单位/人</th>
|
||
<th class="px-3 py-2 font-medium">评估结论</th>
|
||
<th class="w-12 px-2 py-2 font-medium text-center">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="(row, i) in acceptRows" :key="row.id" class="align-top">
|
||
<td class="border-t px-3 py-2.5 text-slate-400">{{ i + 1 }}</td>
|
||
<td class="border-t px-3 py-2.5">
|
||
<NInput
|
||
v-if="editing === `acc-${row.id}-item`"
|
||
class="text-xs"
|
||
v-model:value="row.item"
|
||
type="textarea"
|
||
autofocus
|
||
rows="2"
|
||
@blur="editing = null"
|
||
/>
|
||
<button v-else @click="editing = `acc-${row.id}-item`" title="点击修改"
|
||
class="bg-transparent block w-full rounded-md px-2 py-1 text-left text-xs font-medium leading-relaxed text-slate-700 transition hover:bg-[#EAF0F9]"
|
||
>
|
||
{{ row.item }}
|
||
</button>
|
||
<NInput
|
||
v-if="editing === `acc-${row.id}-basis`"
|
||
class="text-[11px]"
|
||
size="small"
|
||
v-model:value="row.basis"
|
||
autofocus
|
||
@blur="editing = null"
|
||
/>
|
||
<button v-else @click="editing = `acc-${row.id}-basis`" title="本项目对应的预期效果来源,点击修改"
|
||
class="bg-transparent mt-0.5 block w-full rounded px-2 text-left text-[11px] text-slate-400 transition hover:bg-[#EAF0F9] hover:text-slate-500"
|
||
>
|
||
对应预期:{{ row.basis || '(点击填写对应预期效果)' }}
|
||
</button>
|
||
</td>
|
||
<td v-for="field in (['standard', 'method', 'owner'])" :key="field" class="border-t px-3 py-2.5">
|
||
<NInput
|
||
v-if="editing === `acc-${row.id}-${field}`"
|
||
class="text-xs"
|
||
v-model:value="row[field]"
|
||
type="textarea"
|
||
autofocus
|
||
rows="2"
|
||
@blur="editing = null"
|
||
/>
|
||
<button v-else @click="editing = `acc-${row.id}-${field}`" title="点击修改"
|
||
class="bg-transparent block w-full rounded-md px-2 py-1 text-left text-xs leading-relaxed text-slate-700 transition hover:bg-[#EAF0F9]"
|
||
>
|
||
{{ row[field] }}
|
||
</button>
|
||
</td>
|
||
<td class="border-t px-3 py-2.5">
|
||
<div class="flex flex-nowrap items-center gap-1">
|
||
<NButton v-for="v in (['完全达到', '基本达到', '未达成'])" :key="v"
|
||
size="small"
|
||
round
|
||
@click="acceptRows = acceptRows.map((x) => (x.id === row.id ? { ...x, result: x.result === v ? '待验收' : v } : x))"
|
||
class="text-[11px]"
|
||
:type="row.result === v
|
||
? v === '未达成' ? 'error' : v === '基本达到' ? 'warning' : 'success'
|
||
: 'default'">
|
||
{{ v }}
|
||
</NButton>
|
||
</div>
|
||
</td>
|
||
<td class="border-t px-2 py-2.5 text-center">
|
||
<NButton text type="error" title="删除本验收项" @click="acceptRows = acceptRows.filter((x) => x.id !== row.id)">
|
||
<Icon icon="iconamoon:trash" class="size-16px" />
|
||
</NButton>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="flex items-center gap-3">
|
||
<NButton ghost class="text-xs"
|
||
@click="acceptRows = [...acceptRows, { id: Date.now(), item: '新验收项目', basis: '', standard: '', method: '', owner: '', result: '待验收' }]">
|
||
<Icon icon="ic:round-plus" class="size-16px" />添加验收项
|
||
</NButton>
|
||
<span class="text-xs text-slate-400">评估结论在验收阶段由责任单位填写:点击选择「完全达到 / 基本达到 / 未达成」,再次点击取消;行尾可删除验收项</span>
|
||
</div>
|
||
<div class="rounded-lg border">
|
||
<div class="border-b bg-slate-100 px-3 py-2 text-sm font-medium text-slate-700">综合验收结论(验收阶段由车间会同各专业评估填写)</div>
|
||
<div class="space-y-3 p-3">
|
||
<div class="flex flex-wrap items-center gap-2">
|
||
<span v-for="v in (['完全达到', '基本达到', '未达成'])" :key="v">
|
||
<NRadio :checked="acceptNote !== '' && acceptNote.startsWith(v)" :value="'v'" :label="v" @change="acceptNote = v + ':'"></NRadio>
|
||
</span>
|
||
<span class="text-xs text-slate-400">「基本达到 / 未达成」须说明偏差原因与处置措施,并纳入变更关闭审核</span>
|
||
</div>
|
||
<NInput v-model:value="acceptNote" type="textarea" rows="2" placeholder="验收意见:总体评价、偏差说明、后续跟踪措施…(验收阶段填写)" />
|
||
</div>
|
||
</div>
|
||
<div class="rounded-lg bg-emerald-50/80 px-4 py-2.5 text-xs leading-5 text-emerald-700">
|
||
流程说明:投用运行满验收周期后,系统向车间(属地)与涉及专业推送验收任务;各责任单位按上表逐项给出评估结论并上传佐证(数据报表 / 化验报告 / 现场照片),全部完成后汇总至综合验收结论,结论与佐证资料一并纳入变更关闭校验,验收不通过时退回整改或评估恢复原状。
|
||
</div>
|
||
</template>
|
||
<!-- 变更关闭确认表 -->
|
||
<template v-if="tab === 'close'">
|
||
<div class="flex flex-wrap items-center gap-3">
|
||
<span class="flex items-center gap-1.5 text-sm font-semibold text-slate-700">
|
||
<Icon icon="quill:folder-open" class="size-16px" />
|
||
变更关闭确认表(资料归档清单)
|
||
</span>
|
||
<span class="inline-flex items-center gap-1 rounded-full border border-violet-200 bg-violet-100 px-2 py-0.5 text-xs font-medium text-violet-700">
|
||
✨ AI 生成 · 需人工确认
|
||
</span>
|
||
<NTag size="small" class="text-xs">AI 辅助判定一次 · 支持人工改判留痕</NTag>
|
||
<span class="ml-auto text-xs text-slate-400">判定为「是」的资料全部归档后,变更主管部门专责方可确认关闭</span>
|
||
</div>
|
||
<div class="border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||
<div class="flex items-center gap-3 bg-slate-100 px-3 py-2 text-xs font-medium text-slate-500">
|
||
<span class="w-5" />
|
||
<span class="flex-1">资料名称(与申请表「需更新的资料」勾选联动,勾选资料标记「需更新版本」)</span>
|
||
<span class="w-48 text-center">是否涉及(AI 判定,可改判)</span>
|
||
<span class="w-32 text-center">归档状态</span>
|
||
</div>
|
||
<template v-for="cat in (['图纸与规程类', '台账与制度类', '过程与结果文件类'])" :key="cat">
|
||
<div v-if="closeDocs.filter((d) => d.cat === cat).length">
|
||
<button @click="closeFold = { ...closeFold, [cat]: !closeFold[cat] }"
|
||
class="flex w-full items-center gap-2 border-t bg-slate-50 px-3 py-2 text-left text-xs font-semibold text-slate-600 transition hover:bg-[#EAF0F9]">
|
||
<Icon icon="formkit:right" class="size-14px text-slate-400 transition" :class="closeFold[cat] ? '' : 'rotate-90'" />
|
||
{{ cat }}
|
||
<span class="font-normal text-slate-400">
|
||
{{ closeDocs.filter((d) => d.cat === cat).length }} 项 · 已归档
|
||
{{ closeDocs.filter((d) => d.cat === cat && d.scope === '是' && d.archived).length }}/{{ closeDocs.filter((d) => d.cat === cat && d.scope === '是').length }}
|
||
</span>
|
||
<span class="ml-auto text-[11px] font-normal text-slate-400">{{ closeFold[cat] ? '点击展开' : '点击折叠' }}</span>
|
||
</button>
|
||
<template v-if="!closeFold[cat]">
|
||
<div v-for="d in closeDocs.filter((x) => x.cat === cat)" :key="d.name" class="flex flex-wrap items-center gap-3 border-t px-3 py-2.5 text-sm">
|
||
<span class="w-5 text-slate-400">{{ closeDocs.indexOf(d) + 1 }}</span>
|
||
<span class="min-w-0 flex-1 text-slate-800">
|
||
{{ d.name }}
|
||
<NTag v-if="needUpdate(d.name)" size="small" type="info" class="ml-2">需更新版本</NTag>
|
||
</span>
|
||
<span class="flex w-48 shrink-0 justify-center gap-1.5">
|
||
<NButton size="tiny" round v-for="v in (['是', '否', '不涉及'])" :key="v"
|
||
@click="changeScope(d, v)"
|
||
class="text-[11px]"
|
||
:type="d.scope === v ? 'primary' : 'default'">
|
||
{{ v }}
|
||
</NButton>
|
||
</span>
|
||
<span class="flex w-32 shrink-0 items-center justify-center gap-2">
|
||
<template v-if="d.scope === '是'">
|
||
<NTag v-if="d.archived" size="small" type="success">已归档</NTag>
|
||
<template v-else>
|
||
<NTag size="small" type="warning">待上传</NTag>
|
||
<NButton text type="primary" class="text-xs hover:underline" @click="uploadDoc = d.name; uploadSel = null;">上传</NButton>
|
||
</template>
|
||
</template>
|
||
<NTag v-else size="small">{{ d.scope }}</NTag>
|
||
</span>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
<div class="flex flex-wrap items-center gap-4 text-xs text-slate-500">
|
||
<span>
|
||
统计:涉及 <b class="text-[#17407F]">{{ closeDocs.filter((d) => d.scope === '是').length }}</b> 项 ·
|
||
已归档 <b class="text-emerald-600">{{ closeDocs.filter((d) => d.scope === '是' && d.archived).length }}</b> 项 ·
|
||
待上传 <b class="text-amber-600">{{ closeDocs.filter((d) => d.scope === '是' && !d.archived).length }}</b> 项 ·
|
||
需更新版本 <b class="text-[#1D4E9C]">{{ closeDocs.filter((d) => needUpdate(d.name)).length }}</b> 项(联动申请表勾选)
|
||
</span>
|
||
<span class="text-slate-400">资料上传支持 PC / 移动端(拍照上传),版本受控</span>
|
||
</div>
|
||
<div class="rounded-lg bg-[#EAF0F9] px-4 py-2.5 text-xs leading-5 text-[#17407F]">
|
||
关闭校验说明:判定为「是」的资料项全部归档后,系统将全部流程完毕信号推送变更主管部门专责,专责确认后点击关闭,系统记录变更关闭时间;「否 / 不涉及」由 AI 辅助判定一次,人工改判须填写理由并留痕。临时变更恢复完成后,恢复操作票与参数核对记录自动追加至本清单。
|
||
</div>
|
||
</template>
|
||
|
||
</div>
|
||
</div>
|
||
<!-- 底部 -->
|
||
<!-- 全局按钮:下载 / 重新生成 / 确认完毕 -->
|
||
<div v-if="tab !== 'pre'" class="flex flex-wrap items-center justify-between gap-2 border-t p-4">
|
||
<div class="flex flex-wrap gap-2">
|
||
<NButton ghost type="success" @click="download">
|
||
<Icon icon="material-symbols:download" class="size-16px" /> 下载{{ TAB_NAME[tab] }}
|
||
</NButton>
|
||
<NButton ghost type="warning" @click="regenerate">
|
||
<Icon icon="tdesign:refresh" class="size-14px" /> 重新生成{{ TAB_NAME[tab] }}
|
||
</NButton>
|
||
<NButton ghost :type="confirmed[tab] ? 'success' : 'primary'" @click="confirmTab(tab)">
|
||
<Icon icon="ix:success" class="size-16px" /> {{ confirmed[tab] ? '已确认完毕' : '本标签内容确认完毕' }}
|
||
</NButton>
|
||
</div>
|
||
<span class="text-xs text-slate-400">审批人 / 保存 / 提交均在底部固定行,滚动时保持可见</span>
|
||
</div>
|
||
<div class="flex flex-wrap items-center gap-x-4 gap-y-2 bg-[#EAF0F9] px-4 py-2.5 shadow-[0_-2px_8px_rgba(0,0,0,0.05)]">
|
||
<template v-if="tab === 'pre'">
|
||
<span class="text-xs text-slate-400">预识别阶段不设审批人与提交:完成识别与预分析后,请切换至「变更申请表」选择审批人并提交</span>
|
||
<div class="ml-auto flex shrink-0 gap-2">
|
||
<NButton ghost type="success" size="small" @click="genForm">
|
||
<Icon icon="akar-icons:file" class="size-14px" /> 生成变更申请单
|
||
</NButton>
|
||
<NButton ghost type="warning" size="small" @click="runRiskAnalysis" :disabled="analyzing">
|
||
<Icon v-if="analyzing" icon="ri:loader-4-fill" class="size-14px animate-spin" />
|
||
<Icon v-else icon="mdi:shield-alert-outline" class="size-14px" />风险预分析
|
||
</NButton>
|
||
<NButton type="primary" size="small" @click="saveDraft">保存</NButton>
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<div class="flex min-w-0 flex-1 flex-wrap items-center gap-x-2 gap-y-1 text-xs text-slate-500">
|
||
<Icon icon="lucide:users" class="size-14px" />
|
||
<span class="min-w-0 truncate">
|
||
部门审核:{{ approvers.部门审核 }} → 专业会签:{{ Object.keys(signers).length ? Object.values(signers).join('、') : '未涉及专业 · 免会签' }} → 最终批准:{{ approvers.最终批准 }}
|
||
</span>
|
||
<NButton ghost size="small" type="primary" @click="sheet = 'approver'">
|
||
<Icon icon="octicon:sliders-24" class="size-14px" />
|
||
审批流程设置
|
||
</NButton>
|
||
</div>
|
||
<div class="ml-auto flex shrink-0 items-center gap-2">
|
||
<NButton size="small" type="primary" @click="saveDraft">保存</NButton>
|
||
<span v-if="!form" class="flex items-center text-xs text-amber-600">请先在「变更预识别」页生成变更申请单</span>
|
||
<NButton size="small" type="primary" @click="submitApply" :disabled="!form" :title="form ? '提交进入审批流' : '请先生成变更申请单'">
|
||
提交 <Icon icon="formkit:right" class="size-14px" />
|
||
</NButton>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
<!-- 附件在线预览弹窗 -->
|
||
<NModal
|
||
:show="previewFile !== null"
|
||
preset="card"
|
||
:auto-focus="false"
|
||
:style="{ width: '700px', height: 'auto' }"
|
||
:segmented="{ content: true, footer: true }"
|
||
@update:show="(v: boolean) => !v && (previewFile = null)"
|
||
>
|
||
<template #header>
|
||
<div class="flex items-center gap-2 text-base font-semibold text-slate-800">
|
||
<Icon icon="akar-icons:file" class="size-18px" /> {{ previewFile }}
|
||
</div>
|
||
</template>
|
||
<template #default>
|
||
<div class="space-y-3">
|
||
<div class="flex flex-wrap gap-x-4 gap-y-1 text-xs text-slate-400">
|
||
<span>上传人:{{ ROLES[0].person }}</span><span>上传时间:{{ today() }}</span><span>版本:V1(受控)</span>
|
||
</div>
|
||
<div class="flex h-72 flex-col items-center justify-center gap-2 rounded-lg border bg-slate-50 text-slate-400">
|
||
<Icon icon="akar-icons:file" class="size-42px" />
|
||
<span class="text-sm">文件在线预览区(演示)</span>
|
||
<span class="text-xs">正式环境由文档预览服务渲染 PDF / Word / 图片内容,支持缩放、翻页与移动端查看</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<template #footer>
|
||
<div class="flex justify-end gap-[10px]">
|
||
<NButton ghost size="small" @click="downloadPdf">
|
||
<Icon icon="material-symbols:download" class="size-16px" /> 下载
|
||
</NButton>
|
||
<NButton size="small" type="primary" @click="previewFile = null">关闭</NButton>
|
||
</div>
|
||
</template>
|
||
</NModal>
|
||
<!-- 侧边抽屉:AI 类型判定 / 等级判定表 / 审批流程设置 -->
|
||
<NDrawer :show="sheet !== null" @update:show="(v: boolean) => !v && (sheet = null)" placement="right" :width="sheet === 'level' ? '45rem' : '28rem'">
|
||
<NDrawerContent>
|
||
<template #header>
|
||
<div v-if="sheet === 'type'" class="text-base font-semibold text-slate-800">AI 变更类型判定说明</div>
|
||
<div v-if="sheet === 'level'" class="text-base font-semibold text-slate-800">变更等级判定表</div>
|
||
<div v-if="sheet === 'approver'" class="text-base font-semibold text-slate-800">审批流程设置</div>
|
||
</template>
|
||
<div class="h-full space-y-4 overflow-y-auto">
|
||
<!-- AI 变更类型判定说明 -->
|
||
<template v-if="sheet === 'type'">
|
||
<div class="flex items-center gap-2">
|
||
<span class="inline-flex items-center gap-1 rounded-full border border-violet-200 bg-violet-100 px-2 py-0.5 text-xs font-medium text-violet-700">
|
||
✨ AI 生成 · 需人工确认
|
||
</span>
|
||
<NTag v-if="typeAiLoading" type="info" size="small">
|
||
<Icon icon="ri:loader-4-fill" class="size-12px animate-spin" /> AI 判定中…
|
||
</NTag>
|
||
<template v-else>
|
||
<NTag type="info" size="small">判定:{{ typeAi?.type || '工艺' }}</NTag>
|
||
<NTag size="small">置信度:{{ typeAi ? `${Math.round(typeAi.confidence * 100)}%` : '高' }}</NTag>
|
||
</template>
|
||
</div>
|
||
<div v-if="typeAi" class="space-y-3 text-sm leading-relaxed text-slate-700">
|
||
<p><b>判定结论:</b>{{ typeAi.conclusion }}</p>
|
||
<p><b>判定依据:</b></p>
|
||
<ul class="list-disc space-y-1 pl-5">
|
||
<li v-for="r in typeAi.hit_rules" :key="r">命中规则:{{ r }}</li>
|
||
<li v-for="r in typeAi.exclude_rules" :key="r">排除规则:{{ r }}</li>
|
||
</ul>
|
||
<p v-if="typeAi.knowledge_refs.length"><b>知识库引用:</b>{{ typeAi.knowledge_refs.join('、') }}。</p>
|
||
<p class="rounded-lg bg-amber-50 p-3 text-xs text-amber-700">AI 判定结果仅供参考,最终类型以人工确认为准;如调整为其他类型,系统将记录修改留痕。</p>
|
||
</div>
|
||
<div v-else class="space-y-3 text-sm leading-relaxed text-slate-700">
|
||
<p><b>判定结论:</b>本次变更涉及反应温度(80℃→90℃)、保温时间(8h→4h)等工艺参数调整及 DCS 报警/联锁值修改,未涉及设备本体更换或材质变更,依据变更类型规则判定为「工艺」类变更。</p>
|
||
<p><b>判定依据:</b></p>
|
||
<ul class="list-disc space-y-1 pl-5">
|
||
<li>命中规则 2.1:操作参数超出原设计控制范围 → 工艺类</li>
|
||
<li>命中规则 2.4:DCS 报警值/联锁值调整 → 工艺类(仪表联动)</li>
|
||
<li>排除规则 3.2:无设备规格型号变化 → 不构成设备类</li>
|
||
</ul>
|
||
<p><b>知识库引用:</b>工艺卡片 R-101(Rev B)、DCS 联锁台账、《变更管理制度》第 4.2 条。</p>
|
||
<p class="rounded-lg bg-amber-50 p-3 text-xs text-amber-700">AI 判定结果仅供参考,最终类型以人工确认为准;如调整为其他类型,系统将记录修改留痕。</p>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 变更等级判定表 -->
|
||
<template v-if="sheet === 'level'">
|
||
<div class="flex items-center gap-2">
|
||
<span class="inline-flex items-center gap-1 rounded-full border border-violet-200 bg-violet-100 px-2 py-0.5 text-xs font-medium text-violet-700">
|
||
✨ AI 生成 · 需人工确认
|
||
</span>
|
||
<NTag size="small" :type="totalScore >= 20 ? 'error' : 'warning'">
|
||
{{ levelResult }}
|
||
</NTag>
|
||
<span class="text-xs text-slate-400">合计得分 <b class="text-[#17407F]">{{ totalScore }}</b> 分(<20 分为一般,≥20 分为重要)</span>
|
||
</div>
|
||
<div class="grid gap-3 grid-cols-2">
|
||
<div v-for="(dim, di) in LEVEL_DIMS" :key="dim.name" class="rounded-lg border p-3">
|
||
<div class="mb-2 flex items-baseline justify-between gap-2">
|
||
<span class="text-sm font-semibold text-slate-700">
|
||
{{ dim.name }}
|
||
<span v-if="dim.note" class="ml-1 text-xs font-normal text-slate-400">({{ dim.note }})</span>
|
||
</span>
|
||
<span class="text-sm text-slate-500">得分 <b class="text-[#17407F]">{{ levelScores[di] }}</b></span>
|
||
</div>
|
||
<div class="space-y-1.5">
|
||
<button v-for="op in dim.options" :key="op.score" @click="levelScores = levelScores.map((s, i) => (i === di ? op.score : s))"
|
||
class="flex w-full items-start gap-2 rounded-lg px-2 py-1.5 text-left text-xs transition"
|
||
:class="levelScores[di] === op.score ? 'bg-[#EAF0F9] text-[var(--theme-color)]' : 'text-slate-600 hover:bg-slate-50 bg-white'"
|
||
:style="{'--theme-color': themeStore.themeColor}"
|
||
>
|
||
<span
|
||
class="mt-0.5 flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded-full border"
|
||
:class="levelScores[di] === op.score ? 'border-[var(--theme-color)]' : 'border-slate-300'"
|
||
:style="{'--theme-color': themeStore.themeColor}"
|
||
>
|
||
<span
|
||
v-if="levelScores[di] === op.score"
|
||
class="h-1.5 w-1.5 rounded-full bg-[var(--theme-color)]"
|
||
:style="{'--theme-color': themeStore.themeColor}"
|
||
/>
|
||
</span>
|
||
<span class="flex-1 leading-snug">{{ op.label }}({{ op.score }}分)</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="rounded-lg border border-violet-200 bg-violet-50/50 p-4">
|
||
<div class="mb-2 flex items-center gap-2 text-sm font-semibold text-violet-700">
|
||
<Icon icon="lucide:sparkles" class="size-14px" />
|
||
AI 评分分析过程
|
||
<span v-if="levelAiLoading" class="flex items-center gap-1 text-xs font-normal text-violet-500">
|
||
<Icon icon="ri:loader-4-fill" class="size-12px animate-spin" /> AI 评分中…
|
||
</span>
|
||
</div>
|
||
<div v-if="levelAiText || levelAiBasis.some((x) => x)" class="space-y-3 text-xs leading-relaxed text-slate-600">
|
||
<p v-if="levelAiText">{{ levelAiText }}</p>
|
||
<p v-for="(dim, di) in LEVEL_DIMS" :key="dim.name">
|
||
<template v-if="levelAiBasis[di]"><b class="text-[#17407F]">{{ dim.name }}:</b>{{ levelAiBasis[di] }}</template>
|
||
</p>
|
||
<p class="text-slate-400">(各维度得分均可人工改判,修改留痕;改判后等级自动回填至申请表)</p>
|
||
</div>
|
||
<div v-else class="space-y-3 text-xs leading-relaxed text-slate-600">
|
||
<p>我们分析变更内容:缩合反应温度从80℃提高到90℃,反应时间从8小时缩短到4小时。设备材质SS304,设计温度100℃,设计压力0.2MPa,压力不变(常压)。物料为原料A与原料B,未具体说明毒性、腐蚀性。逐维度评分如下。</p>
|
||
<p><b class="text-[#17407F]">1. 设备影响:</b>温度从80℃升至90℃,仍在设计温度100℃范围内,但接近上限。SS304 在 80→90℃ 下腐蚀速率可能略有增加,但变化不大;保守起见给 2 分(中等损害)。</p>
|
||
<p><b class="text-[#17407F]">2. 财务影响:</b>变更目的是提升产能,但若设计错误可能导致反应失败、批次报废。温度提高10℃、时间缩短一半,控制不好可能影响产品质量,损失可能中等(1万~10万),取 2 分。</p>
|
||
<p><b class="text-[#17407F]">3. 质量影响:</b>涉及主要工艺参数变更且有未知杂质(0.05%→0.12%)结构未定,对质量带来明显风险,取 3 分。</p>
|
||
<p class="text-slate-400">(各维度得分均可人工改判,修改留痕;改判后等级自动回填至申请表)</p>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 审批流程设置 -->
|
||
<template v-if="sheet === 'approver'">
|
||
<p class="text-xs leading-relaxed text-slate-400">审批链:部门审核 → 专业会签 → 最终批准。专业会签人由申请表「涉及专业」勾选自动增减,可在此调整人选;重要变更自动追加安全审查与分管领导终批。</p>
|
||
<div class="rounded-lg border p-3">
|
||
<div class="mb-2 flex items-center gap-2 text-sm font-semibold text-slate-700">
|
||
<NTag type="info" size="small">1</NTag> 部门审核
|
||
</div>
|
||
<NSelect :value="approvers.部门审核" :options="DEPT_APPROVER_OPTS"
|
||
@update:value="(v: string) => approvers = { ...approvers, 部门审核: v }" />
|
||
</div>
|
||
<div class="rounded-lg border p-3">
|
||
<div class="mb-2 flex items-center gap-2 text-sm font-semibold text-slate-700">
|
||
<NTag type="info" size="small">2</NTag> 专业会签
|
||
<span class="text-[11px] font-normal text-slate-400">(由「涉及专业」勾选联动)</span>
|
||
</div>
|
||
<p v-if="Object.keys(signers).length === 0" class="text-xs text-slate-400">未勾选涉及专业,本环节免会签;如需会签,请回申请表勾选「涉及专业」。</p>
|
||
<div v-else class="space-y-2">
|
||
<div v-for="(p, d) in signers" :key="d" class="flex items-center gap-2">
|
||
<NTag size="small">{{ d }}</NTag>
|
||
<NSelect class="flex-1" :value="p" :options="signerOpts(p)"
|
||
@update:value="(v: string) => signers = { ...signers, [d]: v }" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="rounded-lg border p-3">
|
||
<div class="mb-2 flex items-center gap-2 text-sm font-semibold text-slate-700">
|
||
<NTag type="info" size="small">3</NTag> 最终批准
|
||
<NTag size="small" class="text-11px">制度固定</NTag>
|
||
</div>
|
||
<NInput :value="approvers.最终批准" disabled />
|
||
<p class="mt-1.5 text-[11px] text-slate-400">重要变更:自动追加「安全审查(安全环保部)」与「分管领导终批」节点,无需手动设置。</p>
|
||
</div>
|
||
<NButton type="primary" class="w-full" @click="finishApprover">完成设置</NButton>
|
||
</template>
|
||
</div>
|
||
</NDrawerContent>
|
||
</NDrawer>
|
||
<!-- 资料上传弹窗 -->
|
||
<NModal
|
||
:show="uploadDoc !== null"
|
||
preset="card"
|
||
:auto-focus="false"
|
||
:style="{ width: '550px', height: 'auto' }"
|
||
:segmented="{ content: true, footer: true }"
|
||
@update:show="(v: boolean) => !v && (uploadDoc = null)"
|
||
>
|
||
<template #header>
|
||
<div class="flex items-center gap-2 text-base font-semibold text-slate-800">
|
||
<Icon icon="material-symbols:upload" class="size-20px" /> 资料上传归档 — {{ uploadDoc }}
|
||
</div>
|
||
</template>
|
||
<div class="space-y-3">
|
||
<div>
|
||
<div class="mb-1 text-xs font-medium text-slate-500">系统文档存储地址(受控目录)</div>
|
||
<div class="flex items-center gap-2 rounded-lg border bg-slate-50 px-3 py-2">
|
||
<Icon icon="quill:folder-open" class="size-16px" />
|
||
<code class="min-w-0 flex-1 truncate text-xs text-slate-600">{{ uploadDocInfo?.storePath }}</code>
|
||
<NButton text type="primary" class="text-xs hover:underline" @click="openDir">打开目录</NButton>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div class="mb-1 text-xs font-medium text-slate-500">方式一:从存储地址选择资料</div>
|
||
<div class="space-y-1.5">
|
||
<p v-for="f in uploadDocInfo?.candidates" :key="f" @click="uploadSel = f"
|
||
class="flex w-full items-center gap-2 rounded-lg border px-3 py-2 text-left text-xs transition cursor-pointer"
|
||
:class="uploadSel === f ? 'border-[var(--theme-color)] bg-[var(--theme-color)] text-[var(--theme-color)] text-white' : 'bg-white text-slate-600 hover:border-[var(--theme-color)] hover:text-[var(--theme-color)]'"
|
||
:style="{'--theme-color': themeStore.themeColor}"
|
||
>
|
||
<Icon icon="akar-icons:file" class="size-14px mr-1" /> {{ f }}
|
||
<Icon v-if="uploadSel === f" icon="ix:success" class="ml-auto size-16px" />
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div class="mb-1 text-xs font-medium text-slate-500">方式二:拖拽本地文件到区域</div>
|
||
<button @click="uploadFiles"
|
||
class="flex h-20 w-full flex-col items-center justify-center gap-1 rounded-lg border-2 border-dashed px-3 text-xs transition"
|
||
:class="uploadSel?.startsWith('本地文件') ? 'border-[var(--theme-color)] bg-[#EAF0F9] text-[var(--theme-color)]' : 'border-slate-300 text-slate-400 hover:border-[var(--theme-color)] hover:text-[var(--theme-color)]'"
|
||
:style="{'--theme-color': themeStore.themeColor}"
|
||
>
|
||
<Icon icon="material-symbols:upload" class="size-24px" />
|
||
{{ uploadSel?.startsWith('本地文件') ? uploadSel : '将资料文件拖拽到此处,或点击选择本地文件' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<template #footer>
|
||
<div class="flex justify-end gap-2">
|
||
<NButton @click="uploadDoc = null">取消</NButton>
|
||
<NButton type="primary" :disabled="!uploadSel" @click="uploadConfirm">
|
||
确认上传归档
|
||
</NButton>
|
||
</div>
|
||
</template>
|
||
</NModal>
|
||
<!-- 插件抽屉 -->
|
||
<NDrawer v-model:show="pluginDrawer" :width="1200" placement="right">
|
||
<NDrawerContent>
|
||
<template v-if="currentPlugin === 'HAZOP'">
|
||
<Hazop @back="onBack" @export="exportAll" />
|
||
</template>
|
||
<template v-else-if="currentPlugin === 'JSA'">
|
||
<Jsa @back="onBack" @export="exportAll" />
|
||
</template>
|
||
<template v-else-if="currentPlugin === 'SCL'">
|
||
<Scl @back="onBack" @export="exportAll" />
|
||
</template>
|
||
<template v-else-if="currentPlugin === 'RISK_CHECK'">
|
||
<RiskCheck @back="onBack" />
|
||
</template>
|
||
</NDrawerContent>
|
||
</NDrawer>
|
||
</NCard>
|
||
</NSpace>
|
||
</template>
|
||
|
||
<style scoped lang="scss">
|
||
.scroll {
|
||
height: calc(100vh - 326px);
|
||
overflow-y: auto;
|
||
}
|
||
.scroll2 {
|
||
height: calc(100vh - 393px);
|
||
overflow-y: auto;
|
||
}
|
||
</style>
|