变更预识别提交
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
countOpenStatus, type ChangeLevel, type ChangeOrder, type ChangeStatus, type ChangeType,
|
||||
} from "@/typings/model";
|
||||
import { getColorWithOpacity } from "@/utils/common";
|
||||
import HighlightText from "@/views/home/modules/HighlightText.vue";
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
@@ -38,55 +39,464 @@ const TABS = [
|
||||
{ id: "accept", name: "验收评价", icon: 'material-symbols:check-circle-outline' },
|
||||
{ id: "close", name: "变更关闭确认表", icon: 'quill:folder-open' },
|
||||
];
|
||||
const formGen = ref<string>("idle");
|
||||
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 变更识别 ----------
|
||||
const runIdentify = () => {
|
||||
if (!desc.value.trim()) { window.$message?.warning("请先输入变更内容描述"); return; }
|
||||
analyzing.value = true;
|
||||
rows.value = null;
|
||||
setTimeout(() => {
|
||||
rows.value = AI_ROWS.map((x) => ({ ...x }));
|
||||
analyzing.value = false;
|
||||
window.$message?.success("AI 变更识别完成,共 4 项,请逐项人工确认(可直接修改)");
|
||||
}, 1200);
|
||||
};
|
||||
|
||||
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 = () => {
|
||||
analyzing.value = true;
|
||||
setTimeout(() => {
|
||||
analyzing.value = false;
|
||||
preRiskShown.value = true;
|
||||
window.$message?.success("AI 风险预分析报告已生成(危害严重性 / 事件概率 / 保护措施影响),内容支持手动修改");
|
||||
}, 1300);
|
||||
};
|
||||
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[]>(["JSA", "检查表法", "通用检查表"]);
|
||||
const plugin = ref<"HAZOP" | "JSA" | "检查表法" | "通用检查表" | null>(null);
|
||||
const genForm = () => {
|
||||
if (formGen.value === "generating") return;
|
||||
formGen.value = "generating";
|
||||
window.$message?.info("AI 正在生成变更申请单…(「变更申请表」标签已显示生成中标识)");
|
||||
setTimeout(() => {
|
||||
const main: string[] = [];
|
||||
for (const row of rows.value ?? []) {
|
||||
if (row.after && row.after !== "无变化" && row.item === "运行条件") {
|
||||
main.push("缩合反应温度从80±2℃提升至90±2℃");
|
||||
main.push("保温时间从8小时缩短至4小时");
|
||||
main.push("DCS温度高报警从85℃上调至95℃,高高报警及联锁值从90℃上调至100℃");
|
||||
}
|
||||
}
|
||||
form.value = {
|
||||
name: "缩合反应温度及时间优化",
|
||||
dept: "",
|
||||
mainChanges: main.length ? main : ["(请根据识别结果填写主要变更)"],
|
||||
related: "更新操作规程、DCS联锁设定值、工艺安全信息、岗位操作法",
|
||||
purposes: ["提高生产效能", "改善操作条件"],
|
||||
effect: "在保持转化率不低于98%的前提下,反应时间缩短50%,产能提升一倍",
|
||||
type: "工艺", level: "一般", duration: "永久", urgent: false, date: "", restoreDate: "",
|
||||
};
|
||||
materialsText.value = "1. 施工方案(含作业步骤、隔离与置换方案、作业许可)\n2. 保温材料(釜体保温更换,硅酸铝纤维毡,厚度按设计核算)\n3. 紧固件与垫片(法兰螺栓、金属缠绕垫,材质与原设计一致)\n4. 防腐涂料(釜体外表面除锈防腐)\n5. 脚手架搭拆材料及验收记录";
|
||||
updateDocs.value = ["PID图纸", "操作规程", "总图", "设备台账"];
|
||||
disciplines.value = ["技术", "仪表"];
|
||||
signers.value = { 技术: "王仪表(技术部)", 仪表: "王仪表(技术部 · 仪表)" };
|
||||
tools.value = ["JSA", "检查表法", "通用检查表"];
|
||||
plugin.value = null;
|
||||
formGen.value = "done";
|
||||
tab.value = "form";
|
||||
window.$message?.success("变更申请单已生成:AI 自动总结名称、预填内容,均可手动修改");
|
||||
}, 1400);
|
||||
};
|
||||
// ---------- 审批流程设置 ----------
|
||||
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: r.person,
|
||||
org: form.value.dept || r.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);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<!-- 发起变更 -->
|
||||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px', height: '100%' }">
|
||||
<div
|
||||
class="sticky top-0 z-20 border-b bg-[var(--bg-color)]"
|
||||
:style="{
|
||||
borderRadius: themeStore.themeRadius + 'px',
|
||||
'--bg-color': getColorWithOpacity(themeStore.themeColor, 0.1)
|
||||
}"
|
||||
>
|
||||
<div class="border" :style="{ borderRadius: themeStore.themeRadius + 'px', height: '100%' }">
|
||||
<!-- 顶部 -->
|
||||
<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)}"
|
||||
class="border-b bg-[var(--bg-color)]"
|
||||
:style="{
|
||||
'--bg-color': getColorWithOpacity(themeStore.themeColor, 0.1)
|
||||
}"
|
||||
>
|
||||
<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
|
||||
}"
|
||||
<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)}"
|
||||
>
|
||||
<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="本页尚未确认完毕" />
|
||||
<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 scroll">
|
||||
<!-- ===== 变更内容描述 ===== -->
|
||||
<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>
|
||||
</div>
|
||||
</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">
|
||||
<Users class="h-3.5 w-3.5 shrink-0 text-[#1D4E9C]" />
|
||||
<span class="min-w-0 truncate">
|
||||
部门审核:{{ approvers.部门审核 }} → 专业会签:{{ Object.keys(signers).length ? Object.values(signers).join('、') : '未涉及专业 · 免会签' }} → 最终批准:{{ approvers.最终批准 }}
|
||||
</span>
|
||||
<button :class="['shrink-0 border-[#6E93C6] bg-white text-[#1D4E9C] hover:bg-[#DFE9F6]']" @click="sheet = 'approver'">
|
||||
<SlidersHorizontal class="h-3 w-3" /> 审批流程设置
|
||||
</button>
|
||||
</div>
|
||||
<div class="ml-auto flex shrink-0 items-center gap-2">
|
||||
<button :class="['!h-8 !px-3 !text-xs']" @click="saveDraft">保存</button>
|
||||
<span v-if="!form" class="flex items-center text-xs text-amber-600">请先在「变更预识别」页生成变更申请单</span>
|
||||
<button :class="['!h-8 !px-3 !text-xs']" @click="submitApply" :disabled="!form" :title="form ? '提交进入审批流' : '请先生成变更申请单'">
|
||||
提交 <ChevronRight class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped lang="scss">
|
||||
.scroll {
|
||||
height: calc(100vh - 326px);
|
||||
overflow-y: auto;
|
||||
@include scrollbar();
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
// 变更后单元格蓝色高亮
|
||||
import { computed } from "vue";
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
const props = defineProps<{ text: string; hl?: string[] }>();
|
||||
interface Seg { t: string; hit: boolean }
|
||||
const segs = computed<Seg[]>(() => {
|
||||
const text = props.text;
|
||||
if (!props.hl || props.hl.length === 0) return [{ t: text, hit: false }];
|
||||
const out: Seg[] = [];
|
||||
let rest = text;
|
||||
const hls = [...props.hl].sort((a, b) => b.length - a.length);
|
||||
while (rest.length) {
|
||||
let idx = -1, kw = "";
|
||||
for (const h of hls) {
|
||||
const i = rest.indexOf(h);
|
||||
if (i >= 0 && (idx < 0 || i < idx)) { idx = i; kw = h; }
|
||||
}
|
||||
if (idx < 0) { out.push({ t: rest, hit: false }); break; }
|
||||
if (idx > 0) out.push({ t: rest.slice(0, idx), hit: false });
|
||||
out.push({ t: kw, hit: true });
|
||||
rest = rest.slice(idx + kw.length);
|
||||
}
|
||||
return out;
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<span class="whitespace-pre-line"><template v-for="(s, i) in segs" :key="i">
|
||||
<mark
|
||||
v-if="s.hit" class="rounded bg-blue-100 px-0.5"
|
||||
:style="{color: themeStore.themeColor}"
|
||||
>{{ s.t }}</mark>
|
||||
<template v-else>{{ s.t }}</template></template></span>
|
||||
</template>
|
||||
Reference in New Issue
Block a user