From 378829da21da65b166918b8b33084a4e1a26ea76 Mon Sep 17 00:00:00 2001 From: bestlee Date: Tue, 1 Sep 2026 16:12:33 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8F=98=E6=9B=B4=E9=A2=84=E8=AF=86=E5=88=AB?= =?UTF-8?q?=E6=9D=BF=E5=9D=97=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/service/api/ai.ts | 16 +- src/views/my/initiateChange/index.vue | 376 +++++++------- .../modules/ChangeCompareTable.vue | 165 ++++++ .../initiateChange/modules/HighlightText.vue | 36 -- .../initiateChange/modules/RichTextInput.vue | 472 ++++++++++++++++++ 5 files changed, 832 insertions(+), 233 deletions(-) create mode 100644 src/views/my/initiateChange/modules/ChangeCompareTable.vue delete mode 100644 src/views/my/initiateChange/modules/HighlightText.vue create mode 100644 src/views/my/initiateChange/modules/RichTextInput.vue diff --git a/src/service/api/ai.ts b/src/service/api/ai.ts index 539c47a..8501d83 100644 --- a/src/service/api/ai.ts +++ b/src/service/api/ai.ts @@ -6,7 +6,7 @@ * - 本地联调形态:前端直连 exchange(moc-server 的登录双令牌下发链路就绪后, * 改为登录时由 moc-server 下发 ai_token,本层仅需替换 getAiToken 来源) */ - +import { request } from '../request/ai'; /** ai-server 统一响应 */ interface AiResp { code: number; @@ -129,9 +129,8 @@ function nextRequestId(prefix: string): string { export interface CompareRow { item: string; - before: string; - after: string; - hl?: string[]; + before_text: string; + after_text: string; } export interface MatrixPayload { @@ -219,13 +218,20 @@ export interface PreRiskResult { probability: string; protection: string; } - +// 风险预分析 export const aiRiskPreAnalysis = (content: string, rows: CompareRow[]) => aiFetch('/open/v1/moc/change/risk-pre-analysis', { request_id: nextRequestId('pre'), content, rows }); +// export function aiRiskPreAnalysis (params: { content: string, rows: CompareRow[] }) { +// return request({ +// url: '/open/v1/moc/change/risk-pre-analysis', +// method: 'post', +// data: params +// }) +// } export interface TypeJudgeResult { type: string; diff --git a/src/views/my/initiateChange/index.vue b/src/views/my/initiateChange/index.vue index 57e9a3d..9c958c2 100644 --- a/src/views/my/initiateChange/index.vue +++ b/src/views/my/initiateChange/index.vue @@ -16,12 +16,12 @@ import { 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"; +import ChangeCompareTable from "./modules/ChangeCompareTable.vue"; const themeStore = useThemeStore(); @@ -47,33 +47,11 @@ const form = ref({ const confirmed = ref>({}); const blockedTabs = ref([]); -const aiEnabled = ref(true); -const analyzing = ref(false); // ---------- 变更预识别 ---------- const desc = ref( "某缩合反应现在控在80℃要反应8小时,车间反馈产能跟不上。我们小试做了几组,90℃下4小时转化率就能到98%,但有个未知杂质会从0.05%涨到0.12%。这个杂质结构还没定,不过不是基因毒性警示结构。想申请把温度提到90℃,同时把反应时间缩到4小时。", ); const rows = ref(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},可手动填写`); @@ -81,102 +59,20 @@ const aiFail = (e: any) => { }; // 页面当前对比行 → 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(null); // ---------- 风险预分析 ---------- -const preRiskEdit = ref(null); -const preRiskShown = ref(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("idle"); const materialsText = ref(""); const updateDocs = ref([]); const disciplines = ref([]); const tools = ref(["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>({ 部门审核: "李主任(一车间主任)", @@ -686,7 +582,25 @@ const onBack = () => { } -// ---------- tab 切换 ---------- +// **********************通用字段*********************** +// 是否开启ai +const aiEnabled = ref(true); +// 保存loading +const saveLoading = ref(false); +// 变更识别loading +const preLoading = ref(false); +// 风险预分析loading +const analyzing = ref(false); + // 生成变更申请单loading +const generateLoading = ref(false); +// btn disabled +const btnDisabled = computed(() => saveLoading.value || preLoading.value || analyzing.value || generateLoading.value); +// 变更名称 +const title = ref(''); +// 当前id +const currentId = ref(0); + +// **********************tab切换*********************** const tab = ref("pre"); const TABS = [ { id: "pre", name: "变更预识别", icon: 'lucide:sparkles' }, @@ -697,19 +611,153 @@ const TABS = [ { id: "accept", name: "验收评价", icon: 'material-symbols:check-circle-outline' }, { id: "close", name: "变更关闭确认表", icon: 'quill:folder-open' }, ]; -// 当前id -const currentId = ref(0); // tab 切换 const tabChange = (id: string) => { - if(form.value.name.trim() === '') { - return window.$message?.warning('请先填写变更名称'); - } - if(desc.value.trim() === '') { - return window.$message?.warning('请先填写变更内容描述'); + if(tab.value === id) { + return; } + // if(title.value.trim() === '') { + // return window.$message?.warning('请先填写变更名称'); + // } + // if(formOne.value.description.trim() === '') { + // return window.$message?.warning('请先填写变更内容描述'); + // } tab.value = id; } +// **********************变更预识别*********************** +// 是否是ai生成 +const isAiGenerated = ref(false); +// 表单 +const formOne = ref<{ + description: string, + compare_rows: {item: string, before_text: string, after_text: string}[], + severity: string, + probability: string, + protection: string +}>({ + description: '', + compare_rows: [], + severity: "", + probability: "", + protection: "", +}); +// AI 变更识别 +const runIdentify = async () => { + if (!formOne.value.description.trim()) { + return window.$message?.warning("请先输入变更内容描述"); + } + preLoading.value = true; + formOne.value.compare_rows = []; + try { + const res = await aiRecognize(formOne.value.description); + formOne.value.compare_rows = (res?.rows ?? []).map((x, i) => ({ item: x.item, before_text: x.before_text, after_text: x.after_text })); + window.$message?.success(`AI 变更识别完成,共 ${formOne.value.compare_rows.length} 项,请逐项人工确认(可直接修改)`); + } catch (e: any) { + formOne.value.compare_rows = []; + aiFail(e); + } finally { + preLoading.value = false; + } +}; + +// 编辑风险预分析 +const preRiskEdit = ref(''); +// 是否显示险预分析 +const preRiskShow = ref(false); +// 运行风险与分析 +const runRiskAnalysis = async () => { + if(formOne.value.description.trim() === '') { + return window.$message?.warning('请先填写变更内容描述'); + } + analyzing.value = true; + try { + const res = await aiRiskPreAnalysis(formOne.value.description, formOne.value.compare_rows); + formOne.value.severity = res?.severity ?? formOne.value.severity; + formOne.value.probability = res?.probability ?? formOne.value.probability + formOne.value.protection = res?.protection ?? formOne.value.protection + preRiskShow.value = true; + window.$message?.success("AI 风险预分析报告已生成(危害严重性 / 事件概率 / 保护措施影响),内容支持手动修改"); + } catch (e: any) { + aiFail(e); + } finally { + analyzing.value = false; + } + + // const {data,error} = await aiRiskPreAnalysis({ + // content: formOne.value.description, + // rows: formOne.value.compare_rows + // }); + // if(!error){ + // formOne.value.severity = data?.severity ?? formOne.value.severity; + // formOne.value.probability = data?.probability ?? formOne.value.probability + // formOne.value.protection = data?.protection ?? formOne.value.protection + // preRiskShow.value = true; + // window.$message?.success("AI 风险预分析报告已生成(危害严重性 / 事件概率 / 保护措施影响),内容支持手动修改"); + // } + // analyzing.value = false; +}; +// 生成变更申请单 +const genForm = async () => { + window.$message?.info("AI 正在生成变更申请单"); + generateLoading.value = true; + 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 = ""; + tab.value = "form"; + window.$message?.success("变更申请单已生成:AI 自动总结名称、预填内容,均可手动修改"); + } catch (e: any) { + aiFail(e); + } finally { + generateLoading.value = false; + } +}; + +// --------------------- 变更申请表 --------------------- +// 表单 +const changForm = ref<{ + name: string, + mainChanges: string[], + related: string, + purposes: string[], + effect: string, + type: string, + duration: string +}>({ + name: '', + mainChanges: [], + related: '', + purposes: [], + effect: '', + type: '', + duration: '' +}); @@ -730,7 +778,7 @@ const tabChange = (id: string) => { :style="{'--border-color': getColorWithOpacity(themeStore.themeColor, 0.2)}" > 变更名称 * - + 变更编号 @@ -745,13 +793,11 @@ const tabChange = (id: string) => { > {{ t.name }} - + - AI 生成中 - + @@ -767,15 +813,15 @@ const tabChange = (id: string) => { 变更内容描述 - - + + 变更识别 AI 未启用
- +
AI 辅助功能未启用(可在「系统设置 → AI 辅助功能」开启),识别结果与申请表需手动填写。 @@ -787,77 +833,22 @@ const tabChange = (id: string) => {
- {{rows ? `变更识别结果(${rows.length}项)` : '变更识别结果(空表 · 可手动录入,或由 AI 识别生成)'}} + {{formOne.compare_rows.length ? `变更识别结果(${formOne.compare_rows.length}项)` : '变更识别结果(空表 · 可手动录入,或由 AI 识别生成)'}}
- + ✨ AI 生成 · 需人工确认
-
- - - - - - - - - - - - - - - -
变更项目变更前变更后 -
- -

- {{ row.item }} -

-
- -

- - {{ row[field] }} -

-
- - - -
-
-
- - 添加行 - -
+
-
+
@@ -872,12 +863,12 @@ const tabChange = (id: string) => {
{{ s.name }} - + {{ preRiskEdit === s.key ? "完成" : "编辑" }}
- -

{{ preRiskText[s.key] }}

+ +

{{ formOne[s.key] }}

AI 风险预分析仅供参考,用于申请阶段的快速研判;正式风险识别请前往「风险分析记录表」标签页,使用 HAZOP / JSA / 检查表法开展并留存记录。 @@ -886,8 +877,9 @@ const tabChange = (id: string) => {
+ -