生成变更申请单一键生成全部内容:回填申请表后并行生成等级判定表/风险检查表/培训内容/PSSR/验收评价;风险检查表数据提升至风险分析页并接入aiRiskCheck预填(42901自动重试一次),各标签页重新生成仅生成本页内容

This commit is contained in:
2026-09-08 13:26:53 +08:00
parent 376f6b7cd5
commit 30bf3aa01a
7 changed files with 397 additions and 340 deletions
+19 -4
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, provide, ref } from 'vue'; import { computed, nextTick, onMounted, provide, ref } from 'vue';
import { Icon } from '@iconify/vue' import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme'; import { useThemeStore } from '@/store/modules/theme';
import { getColorWithOpacity } from "@/utils/common"; import { getColorWithOpacity } from "@/utils/common";
@@ -62,14 +62,28 @@ const changeApplyForm = ref<any>(null)
// 当前申请表所选变更类型的文本编码(供 PSSR 等 AI 接口使用) // 当前申请表所选变更类型的文本编码(供 PSSR 等 AI 接口使用)
const applyTypeLabel = computed(() => typeList.value.find((t) => t.value === changeApplyForm.value?.change_type)?.label ?? ''); const applyTypeLabel = computed(() => typeList.value.find((t) => t.value === changeApplyForm.value?.change_type)?.label ?? '');
// AI 生成变更申请单(变更预识别页派发):回填申请表并切换标签 // AI 生成变更申请单(变更预识别页派发):回填申请表,随后后台并行生成
const onApplyGenerated = (res: any) => { // 等级判定表 / 风险检查表 / 培训内容 / PSSR / 验收评价(各组件自行处理 loading 与报错,失败不阻断)
const onApplyGenerated = async (res: any) => {
if (res?.name) title.value = res.name; if (res?.name) title.value = res.name;
changeApplyRef.value?.applyAiResult(res); changeApplyRef.value?.applyAiResult(res);
// 直接切 tab 不经过 tabChange,需同步捕获预识别表单供其他标签页 AI 接口使用 // 直接切 tab 不经过 tabChange,需同步捕获预识别/申请表表单供其他标签页 AI 接口使用
changePreForm.value = changePreRef.value?.form; changePreForm.value = changePreRef.value?.form;
changeApplyForm.value = changeApplyRef.value?.form;
tab.value = 'form'; tab.value = 'form';
window.$message?.success("变更申请单已生成:AI 自动总结名称、预填内容,均可手动修改"); window.$message?.success("变更申请单已生成:AI 自动总结名称、预填内容,均可手动修改");
// 等 props 同步后并行生成其余内容(等级判定/风险检查表/培训/PSSR/验收评价)
await nextTick();
const jobs = [
changeApplyRef.value?.generateLevel?.(),
riskAnalysisRef.value?.generateRiskCheck?.(),
changeTrainingRef.value?.onRegenerate?.(),
pssrRef.value?.onRegenerate?.(),
evaluationRef.value?.onRegenerate?.(),
].filter(Boolean);
window.$message?.info("正在后台生成:等级判定表 / 风险检查表 / 变更培训内容 / PSSR检查内容 / 验收评价(风险检查表约 1~3 分钟)");
await Promise.allSettled(jobs);
window.$message?.info("全部 AI 内容已生成完毕:请逐标签页检查确认;如有失败项,可在对应标签页点击「重新生成」单独重试");
} }
// **********************tab切换*********************** // **********************tab切换***********************
@@ -332,6 +346,7 @@ onMounted(() => {
:title="title" :title="title"
:currentId="currentId" :currentId="currentId"
:formInfo="changeApplyForm" :formInfo="changeApplyForm"
:preInfo="changePreForm"
@save="saveDraft" @save="saveDraft"
@submit="submitConfirm" @submit="submitConfirm"
@confirmTab="confirmTab" @confirmTab="confirmTab"
@@ -398,7 +398,7 @@ const confirmTab = (tab:string, val:boolean) => {
emit('confirmTab', tab, val); emit('confirmTab', tab, val);
} }
defineExpose({form, applyAiResult}) defineExpose({form, applyAiResult, generateLevel: useAiLevel})
</script> </script>
@@ -59,7 +59,7 @@ const confirmTab = (tab:string, val:boolean) => {
emit('confirmTab', tab, val); emit('confirmTab', tab, val);
} }
defineExpose({form}) defineExpose({form, onRegenerate})
</script> </script>
<template> <template>
@@ -114,7 +114,7 @@ const findNodeById = (tree:any,id:number) => {
return null; // 未找到 return null; // 未找到
} }
defineExpose({list}) defineExpose({list, onRegenerate})
</script> </script>
<template> <template>
+1 -1
View File
@@ -107,7 +107,7 @@ const confirmTab = (tab:string, val:boolean) => {
emit('confirmTab', tab, val); emit('confirmTab', tab, val);
} }
defineExpose({list}) defineExpose({list, onRegenerate})
</script> </script>
<template> <template>
@@ -3,6 +3,7 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { Icon } from '@iconify/vue' import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme'; import { useThemeStore } from '@/store/modules/theme';
import { AiError, aiRiskCheck } from "@/service/api/ai";
import Ed from "./Ed.vue"; import Ed from "./Ed.vue";
import Hazop from "./hazop.vue"; import Hazop from "./hazop.vue";
import Jsa from "./jsa.vue"; import Jsa from "./jsa.vue";
@@ -12,19 +13,75 @@ import Footer from './footer.vue'
const themeStore = useThemeStore(); const themeStore = useThemeStore();
const props = defineProps(['type','isAiOpen','title','currentId','formInfo']) const props = defineProps(['type','isAiOpen','title','currentId','formInfo','preInfo'])
const emit = defineEmits(['save','submit','confirmTab']); const emit = defineEmits(['save','submit','confirmTab']);
const footerRef = ref<any>(null);
const aiFail = (e: any) => {
if (e instanceof AiError) window.$message?.warning(`AI 分析失败:${e.message},可手动填写`);
else window.$message?.warning("AI 分析失败,可手动填写");
};
const form = ref<{ content: string }>({ const form = ref<{ content: string }>({
content: '' content: ''
}); });
const pluginDone = ref<Record<string, number>>({}); const pluginDone = ref<Record<string, number>>({});
const tools = ref<string[]>(["HAZOP","JSA", "SCL", "RISK_CHECK"]); const tools = ref<string[]>(["HAZOP","JSA", "SCL", "RISK_CHECK"]);
const riskRecords = ref<any>([]); const riskRecords = ref<any>([]);
// 风险检查表检查项(数据提升到本组件:AI 预填/重新生成随时可写,插件抽屉打开即渲染)
const riskChecks = ref<any[]>([]);
const riskCheckLoading = ref<boolean>(false);
const pluginDrawer = ref<boolean>(false); const pluginDrawer = ref<boolean>(false);
const currentPlugin = ref<string>(''); const currentPlugin = ref<string>('');
const lvlNext = (v: string) => (v === "高" ? "中" : v === "中" ? "低" : "高"); const lvlNext = (v: string) => (v === "高" ? "中" : v === "中" ? "低" : "高");
// AI 生成/重新生成风险检查表(footer 派发;整表生成约 1~3 分钟,42901 闸门满自动重试一次)
const generateRiskCheck = async () => {
const info = props.preInfo;
if (!info?.description) {
return window.$message?.warning("缺少变更描述,请先在 [变更预识别] 里面输入变更描述");
}
if (riskCheckLoading.value) return;
riskCheckLoading.value = true;
footerRef.value?.setRegenerateLoading(true);
try {
for (let attempt = 0; attempt < 2; attempt++) {
try {
const res = await aiRiskCheck(info.description, (info.compare_rows ?? []).map((x: any) => ({ item: x.item, before_text: x.before_text, after_text: x.after_text })));
const items = (res?.items ?? []).map((x: any, i: number) => ({
id: i + 1,
group: x.group || '安全健康',
q: x.q ?? '',
ans: x.ans ?? 'NA',
risk: x.risk ?? '',
measure: x.measure ?? '',
}));
if (!items.length) {
window.$message?.warning("AI 未生成检查项,可在风险检查表插件中手动维护");
return;
}
riskChecks.value = items;
window.$message?.success(`风险检查表已由 AI 生成(${items.length} 项),打开「风险检查表」工具卡逐项确认`);
return;
} catch (e: any) {
if (e instanceof AiError && e.code === 42901 && attempt === 0) {
const waitSec = (e.data?.retry_after as number) ?? 30;
window.$message?.info(`当前 AI 生成任务较多,${waitSec} 秒后自动重试一次…`);
await new Promise((r) => setTimeout(r, waitSec * 1000));
continue;
}
aiFail(e);
return;
}
}
} finally {
riskCheckLoading.value = false;
footerRef.value?.setRegenerateLoading(false);
}
};
const onRegenerate = generateRiskCheck;
// 风险识别分析记录表 // 风险识别分析记录表
const TOOL_CARDS = [ const TOOL_CARDS = [
{ key: "HAZOP" as const, name: "HAZOP 分析", desc: "节点-偏离结构化分析:原因 / 后果 / LS 风险矩阵 / 安全措施" }, { key: "HAZOP" as const, name: "HAZOP 分析", desc: "节点-偏离结构化分析:原因 / 后果 / LS 风险矩阵 / 安全措施" },
@@ -140,7 +197,7 @@ const confirmTab = (tab:string, val:boolean) => {
emit('confirmTab', tab, val); emit('confirmTab', tab, val);
} }
defineExpose({form}) defineExpose({form, generateRiskCheck})
</script> </script>
<template> <template>
@@ -268,6 +325,7 @@ defineExpose({form})
@save="saveDraft" @save="saveDraft"
@submit="submitConfirm" @submit="submitConfirm"
@confirmTab="confirmTab" @confirmTab="confirmTab"
@regenerate="onRegenerate"
/> />
<!-- 插件抽屉 --> <!-- 插件抽屉 -->
<NDrawer v-model:show="pluginDrawer" :width="1200" placement="right"> <NDrawer v-model:show="pluginDrawer" :width="1200" placement="right">
@@ -282,7 +340,7 @@ defineExpose({form})
<Scl @close="closePlugin" @export="exportAll" /> <Scl @close="closePlugin" @export="exportAll" />
</template> </template>
<template v-else-if="currentPlugin === 'RISK_CHECK'"> <template v-else-if="currentPlugin === 'RISK_CHECK'">
<RiskCheck @close="closePlugin" /> <RiskCheck v-model="riskChecks" :loading="riskCheckLoading" @close="closePlugin" @back="closePlugin" />
</template> </template>
</NDrawerContent> </NDrawerContent>
</NDrawer> </NDrawer>
@@ -4,48 +4,23 @@ import { useMessage } from "naive-ui";
import { Icon } from '@iconify/vue' import { Icon } from '@iconify/vue'
const message = useMessage(); const message = useMessage();
// 检查项数据由父组件(riskAnalysis)持有:AI 预填 / 重新生成在父级完成,此处负责渲染与人工确认
const props = defineProps<{ modelValue: any[]; loading?: boolean }>();
const emit = defineEmits<{ const emit = defineEmits<{
back: []; back: [];
"update:modelValue": [value: any[]];
}>(); }>();
const RISK_CHECKS_INIT = [ const riskChecks = computed<any[]>({
{ get: () => props.modelValue ?? [],
id: 1, group: "安全健康", ans: "是", set: (v) => emit("update:modelValue", v),
q: "是否存在任何可能导致安全问题的化学反应?检查化学反应禁忌表。", });
risk: "反应温度从80℃升至90℃,反应速率加快,放热更集中,可能引发反应失控或副反应,导致超温超压。", const setAns = (item: any, a: string) => {
measure: "1. 进行反应量热试验或查阅反应热数据,确认90℃下的放热速率和绝热温升;2. 评估现有冷却系统能力是否满足最大放热需求;3. 更新化学反应风险评估报告;4. 验证反应釜搅拌、冷却系统完好。", riskChecks.value = riskChecks.value.map((x) => (x.id === item.id ? { ...x, ans: a } : x));
}, };
{ id: 2, group: "安全健康", ans: "否", q: "是否存在任何易燃易爆化学物质或粉尘?" },
{
id: 3, group: "安全健康", ans: "是",
q: "是否存在任何有危险的原材料流速、成分或温度条件变更?",
risk: "反应温度条件从80±2℃变更至90±2℃,接近设备设计温度上限100℃,可能造成副反应增加或杂质生成。",
measure: "1. 通过小试实验验证90℃下的副反应种类及含量,确保产品杂质合格;2. 确认原料A和B在此温度下的稳定性;3. 将温度控制精度要求写入操作参数表。",
},
{
id: 4, group: "安全健康", ans: "是",
q: "是否会因加热、冷却或精确的温度控制失控而导致安全问题?",
risk: "温度控制失效(如加热阀门卡死、冷却中断)可能使实际温度超过100℃,导致设备超温、压力升高或反应失控。",
measure: "1. 调试并确认DCS温度高报警(95℃)和高高报警联锁(100℃)功能正常;2. 执行联锁回路功能测试(温度变送器、DCS卡件、电磁阀、切断阀);3. 确认加热介质切断阀为故障安全型。",
},
{
id: 5, group: "安全健康", ans: "是",
q: "是否具有为安全操作所必需的仪表、控制、紧急制动、开关或报警?",
risk: "变更后温度报警值上调,现有温度仪表量程可能不足或精度不够,影响安全操作。",
measure: "1. 检查温度变送器量程是否覆盖0~100℃,精度是否优于0.5级;2. 确认DCS报警值设定正确且已组态;3. 在DCS画面中明显标注新报警值。",
},
{
id: 6, group: "工艺与操作", ans: "是",
q: "变更后是否需要对操作人员进行专门培训?",
risk: "新工况(90℃ / 4小时)与原操作习惯差异较大,误操作可能引发超温或产品质量波动。",
measure: "1. 修订操作规程并组织专项培训考核;2. 投用首周实行双人复核操作;3. 关键参数调整权限上收至班长。",
},
{ id: 7, group: "工艺与操作", ans: "否", q: "是否涉及应急预案或消防设施的调整?" },
];
// 说明:本页面暂无「AI 预填检查表」交互按钮,检查项为静态示例数据,保持现状;
// 如需接入可调用 aiRiskCheck(content, rows)src/service/api/ai.ts,耗时约 1~3 分钟,需处理 42901 限流重试)。
const pluginDone = ref<Record<string, number>>({}); const pluginDone = ref<Record<string, number>>({});
const riskChecks = ref<any[]>(RISK_CHECKS_INIT.map((x) => ({ ...x })));
const riskGroups = computed(() => [...new Set(riskChecks.value.map((x) => x.group))]); const riskGroups = computed(() => [...new Set(riskChecks.value.map((x) => x.group))]);
const riskFilter = ref<string>("全部"); const riskFilter = ref<string>("全部");
const onBack = () => { const onBack = () => {
@@ -67,6 +42,12 @@ const save = () => {
<NButton size="small" ghost type="primary" class="text-xs" @click="save">保存</NButton> <NButton size="small" ghost type="primary" class="text-xs" @click="save">保存</NButton>
<span class="flex items-center gap-1.5 text-sm font-semibold text-slate-700">变更风险检查表通用</span> <span class="flex items-center gap-1.5 text-sm font-semibold text-slate-700">变更风险检查表通用</span>
<NTag size="small" type="info">插件</NTag> <NTag size="small" type="info">插件</NTag>
<NTag v-if="loading" size="small" type="warning">
<template #icon>
<Icon icon="ri:loader-4-fill" class="size-12px animate-spin" />
</template>
AI 生成中整表约 1~3 分钟
</NTag>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<NRadioGroup v-model:value="riskFilter" name="radiogroup"> <NRadioGroup v-model:value="riskFilter" name="radiogroup">
<NRadio v-for="song in [{label:'全部',value:'全部'},{label:'仅看「是」',value:'是'}]" :key="song.value" :value="song.value"> <NRadio v-for="song in [{label:'全部',value:'全部'},{label:'仅看「是」',value:'是'}]" :key="song.value" :value="song.value">
@@ -79,6 +60,9 @@ const save = () => {
</span> </span>
</div> </div>
<p class="text-[11px] text-slate-400">逐项判定 / / 不涉及判定为的检查项输出风险描述与管控措施完成后点返回工具列表回显完成项数</p> <p class="text-[11px] text-slate-400">逐项判定 / / 不涉及判定为的检查项输出风险描述与管控措施完成后点返回工具列表回显完成项数</p>
<div v-if="!riskChecks.length && !loading" class="rounded-lg border border-dashed px-4 py-6 text-center text-xs text-slate-400">
暂无检查项生成变更申请单自动预填或点击页脚重新生成 AI 生成耗时约 1~3 分钟
</div>
<div v-for="g in riskGroups" :key="g" class="overflow-hidden rounded-lg border"> <div v-for="g in riskGroups" :key="g" class="overflow-hidden rounded-lg border">
<div class="bg-slate-100 px-3 py-2 text-sm font-medium text-slate-700">{{ g }}</div> <div class="bg-slate-100 px-3 py-2 text-sm font-medium text-slate-700">{{ g }}</div>
<div v-for="item in riskChecks.filter((x) => x.group === g && (riskFilter === '全部' || x.ans === '是'))" :key="item.id" <div v-for="item in riskChecks.filter((x) => x.group === g && (riskFilter === '全部' || x.ans === '是'))" :key="item.id"
@@ -89,7 +73,7 @@ const save = () => {
<span v-for="a in ['是', '否', 'NA']" :key="a"> <span v-for="a in ['是', '否', 'NA']" :key="a">
<NRadio <NRadio
:checked="item.ans === a" :checked="item.ans === a"
@change="riskChecks = riskChecks.map((x) => (x.id === item.id ? { ...x, ans: a } : x))" @change="setAns(item, a)"
> >
{{ a }} {{ a }}
</NRadio> </NRadio>