发起变更AI功能接线:变更预识别生成申请单、各标签页重新生成(申请表/培训内容/PSSR检查/验收评价)对接ai-server并适配新字段渲染;修复等级判定表AI评分重复点击总分累加问题

This commit is contained in:
2026-09-08 12:01:56 +08:00
parent 6f41d613ba
commit 376f6b7cd5
7 changed files with 1889 additions and 1701 deletions
+17
View File
@@ -59,6 +59,18 @@ const confirmed = ref<{[key: string]: boolean}>({
// 变更预分析表单数据
const changePreForm = ref<any>(null)
const changeApplyForm = ref<any>(null)
// 当前申请表所选变更类型的文本编码(供 PSSR 等 AI 接口使用)
const applyTypeLabel = computed(() => typeList.value.find((t) => t.value === changeApplyForm.value?.change_type)?.label ?? '');
// AI 生成变更申请单(变更预识别页派发):回填申请表并切换标签
const onApplyGenerated = (res: any) => {
if (res?.name) title.value = res.name;
changeApplyRef.value?.applyAiResult(res);
// 直接切 tab 不经过 tabChange,需同步捕获预识别表单供其他标签页 AI 接口使用
changePreForm.value = changePreRef.value?.form;
tab.value = 'form';
window.$message?.success("变更申请单已生成:AI 自动总结名称、预填内容,均可手动修改");
}
// **********************tab切换***********************
const TABS = [
@@ -289,6 +301,7 @@ onMounted(() => {
:title="title"
:currentId="currentId"
@save="saveDraft"
@apply-generated="onApplyGenerated"
/>
</div>
<!-- ===== 变更申请表 ===== -->
@@ -307,6 +320,7 @@ onMounted(() => {
@save="saveDraft"
@submit="submitConfirm"
@confirmTab="confirmTab"
@update:title="(v: string) => title = v"
/>
</div>
<!-- ===== 风险分析记录表 ===== -->
@@ -331,6 +345,7 @@ onMounted(() => {
:isAiOpen="aiEnabled"
:title="title"
:currentId="currentId"
:formInfo="changePreForm"
@save="saveDraft"
@submit="submitConfirm"
@confirmTab="confirmTab"
@@ -344,6 +359,8 @@ onMounted(() => {
:isAiOpen="aiEnabled"
:title="title"
:currentId="currentId"
:formInfo="changePreForm"
:changeType="applyTypeLabel"
@save="saveDraft"
@submit="submitConfirm"
@confirmTab="confirmTab"
@@ -6,7 +6,7 @@ import { useThemeStore } from '@/store/modules/theme';
import { tagTextColor, tagBgColor, tagBorderColor } from '@/utils/common';
import { localStg } from '@/utils/storage';
import { useChangeStore } from '@/store/modules/change';
import { AiError, aiTypeJudge, aiLevelScore } from "@/service/api/ai";
import { AiError, aiTypeJudge, aiLevelScore, aiApplication } from "@/service/api/ai";
import { uploadFileApi, deleteFileApi } from "@/service/api/file";
import Footer from './footer.vue'
@@ -18,7 +18,7 @@ const themeStore = useThemeStore();
const changeStore = useChangeStore();
const props = defineProps(['type','orgTree','templateList','typeList','levelList','isAiOpen','title','currentId','formInfo'])
const emit = defineEmits(['save','submit','confirmTab']);
const emit = defineEmits(['save','submit','confirmTab','update:title']);
const aiFail = (e: any) => {
if (e instanceof AiError) window.$message?.warning(`AI 分析失败:${e.message},可手动填写`);
@@ -83,6 +83,8 @@ const PURPOSE_OPTIONS = [
"达到环保要求", "法律合规整改", "减少能耗物损", "稳定系统运行",
"完善管理制度", "达到质量改进", "设备可靠性升级",
];
// 需更新的资料选项
const UPDATE_DOC_OPTIONS = ['PID图纸', '操作规程', '总图', '设备台账', '工艺卡片', '联锁台账', '应急预案'];
// 风险分析选项
const RISK_TOOLS_FORM = [
{ key: "HAZOP", label: "HAZOP" },
@@ -247,10 +249,12 @@ const useAiLevel = async () => {
let form = props.formInfo;
if (levelAiLoading.value) return;
levelAiLoading.value = true;
levelAiLoading.value = true;
try {
const dims = LEVEL_DIMS.value.map((d:any, i:any) => ({ dim: `DIM_${i + 1}`, name: d.name, max: Math.max(...d.options.map((o:any) => o.score)) }));
const res = await aiLevelScore(form.description, (form.compare_rows ?? []).map((x: any) => ({ item: x.item, before_text: x.before_text, after_text: x.after_text })), dims);
// 重新评分前清零,避免多次点击分数累加
totalScore.value = 0;
levelAiBasis.value = [];
res?.dims?.forEach((d, i) => {
totalScore.value += d.score ?? 0;
LEVEL_DIMS.value[i].score = d.score ?? 0;
@@ -263,6 +267,53 @@ const useAiLevel = async () => {
levelAiLoading.value = false;
}
}
// AI 申请单结果回填(生成变更申请单 / 重新生成变更申请表共用)
const applyAiResult = (res: any) => {
if (!res) return;
const f = form.value;
const main = (res.main_changes ?? []).filter((x: string) => x && x.trim());
if (main.length) f.main_changes = main;
const related = (res.related_changes ?? []).filter((x: string) => x && x.trim());
if (related.length) f.related_changes = related;
const purposes = (res.purposes ?? []).filter((x: string) => PURPOSE_OPTIONS.includes(x));
if (purposes.length) f.purposes = purposes;
if (res.effect) f.effect = res.effect;
// 变更类型:AI 输出 MOC 中文编码 → typeList 数值
if (res.change_type) {
const t = props.typeList.find((x: any) => x.label === res.change_type || x.label.includes(res.change_type) || res.change_type.includes(x.label));
if (t) f.change_type = t.value;
}
// 变更时限:永久→1,临时→2
if (res.duration_suggestion === '永久') f.duration_type = 1;
else if (res.duration_suggestion === '临时') f.duration_type = 2;
if (res.materials_text) f.materials = res.materials_text;
const docs = (res.update_docs ?? []).filter((x: string) => UPDATE_DOC_OPTIONS.includes(x));
if (docs.length) f.update_docs = docs;
const discs = (res.disciplines ?? []).filter((x: string) => specialties.includes(x));
if (discs.length) f.disciplines = discs;
const aiTools = (res.risk_tools ?? [])
.map((x: string) => RISK_TOOLS_FORM.find((t) => t.key === x || t.label === x)?.key)
.filter((x: string | undefined): x is string => !!x);
if (aiTools.length) f.risk_tools = aiTools;
}
// 重新生成变更申请表(footer 派发)
const onRegenerate = async () => {
const info = props.formInfo;
if (!info?.description) {
return window.$message?.warning("缺少变更描述,请先在 [变更预识别] 里面输入变更描述");
}
footerRef.value?.setRegenerateLoading(true);
try {
const res = await aiApplication(info.description, (info.compare_rows ?? []).map((x: any) => ({ item: x.item, before_text: x.before_text, after_text: x.after_text })));
applyAiResult(res);
if (res?.name) emit('update:title', res.name);
window.$message?.success("变更申请表已由 AI 重新生成,内容均可手动修改");
} catch (e: any) {
aiFail(e);
} finally {
footerRef.value?.setRegenerateLoading(false);
}
}
// 处理等级判定表评分变化
const handleScoreChange = (val:number,index:number) => {
LEVEL_DIMS.value[index].score = val;
@@ -347,7 +398,7 @@ const confirmTab = (tab:string, val:boolean) => {
emit('confirmTab', tab, val);
}
defineExpose({form})
defineExpose({form, applyAiResult})
</script>
@@ -576,7 +627,7 @@ defineExpose({form})
<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="form.update_docs">
<div class="flex flex-wrap gap-y-2 gap-x-4">
<NCheckbox v-for="p in ['PID图纸', '操作规程', '总图', '设备台账', '工艺卡片', '联锁台账', '应急预案']" :key="p" :value="p" :label="p" />
<NCheckbox v-for="p in UPDATE_DOC_OPTIONS" :key="p" :value="p" :label="p" />
</div>
</NCheckboxGroup>
<span class="self-center text-xs text-slate-400">多选变更关闭前逐项确认上传最新版本</span>
@@ -612,6 +663,7 @@ defineExpose({form})
@save="saveDraft"
@submit="submitConfirm"
@confirmTab="confirmTab"
@regenerate="onRegenerate"
/>
<!-- 侧边抽屉AI 类型判定 / 等级判定表 -->
<NDrawer :show="sheet !== ''" @update:show="(v) => !v && (sheet = '')" placement="right" :width="sheet === 'level' ? '45rem' : '28rem'">
@@ -3,12 +3,12 @@
import { inject, ref } from 'vue';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { AiError, aiRecognize, aiRiskPreAnalysis } from "@/service/api/ai";
import { AiError, aiApplication, aiRecognize, aiRiskPreAnalysis } from "@/service/api/ai";
import ChangeCompareTable from "./ChangeCompareTable.vue";
const themeStore = useThemeStore();
const emit = defineEmits(['save'])
const emit = defineEmits(['save', 'apply-generated'])
const generalLoading = inject('generalLoading', {saveLoading: ref<boolean>(false),btnDisabled: ref<boolean>(false)})
const {saveLoading,btnDisabled} = generalLoading
@@ -89,44 +89,21 @@ const runRiskAnalysis = async () => {
};
// 生成变更申请单
const genForm = async () => {
// window.$message?.info("AI 正在生成变更申请单");
// generateLoading.value = true;
// try {
// const res = await aiApplication(desc.value, (form.compare_rows ?? []).map((x: any) => ({ item: x.item, before_text: x.before_text, after_text: x.after_text })));
// 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;
// }
if (!form.value.description.trim()) {
return window.$message?.warning("请先输入变更内容描述");
}
generateLoading.value = true;
btnDisabled.value = true;
try {
const res = await aiApplication(form.value.description, form.value.compare_rows);
// 由父组件把 AI 结果回填到「变更申请表」并切换标签
emit('apply-generated', res);
} catch (e: any) {
aiFail(e);
} finally {
generateLoading.value = false;
btnDisabled.value = false;
}
};
// 保存草稿
@@ -3,18 +3,49 @@
import { ref } from 'vue';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { AiError, aiTraining } from "@/service/api/ai";
import Footer from './footer.vue';
const themeStore = useThemeStore();
const props = defineProps(['type','isAiOpen','title','currentId'])
const props = defineProps(['type','isAiOpen','title','currentId','formInfo'])
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 分析失败,可手动填写");
};
// 是否是ai生成
const isAiGenerated = ref<boolean>(false);
const form = ref<{ content: string }>({
content: ''
});
// 重新生成变更培训内容(footer 派发)
const onRegenerate = async () => {
const info = props.formInfo;
if (!info?.description) {
return window.$message?.warning("缺少变更描述,请先在 [变更预识别] 里面输入变更描述");
}
footerRef.value?.setRegenerateLoading(true);
try {
const res = await aiTraining(info.description, (info.compare_rows ?? []).map((x: any) => ({ item: x.item, before_text: x.before_text, after_text: x.after_text })));
if (res?.training_text) {
form.value.content = res.training_text;
isAiGenerated.value = true;
window.$message?.success("变更培训内容已由 AI 生成,可直接修改");
} else {
window.$message?.warning("AI 未返回培训内容,请手动填写");
}
} catch (e: any) {
aiFail(e);
} finally {
footerRef.value?.setRegenerateLoading(false);
}
}
// 保存草稿
const saveDraft = () => {
emit('save');
@@ -64,6 +95,7 @@ defineExpose({form})
@save="saveDraft"
@submit="submitConfirm"
@confirmTab="confirmTab"
@regenerate="onRegenerate"
/>
</template>
<style scoped lang="scss">
@@ -4,16 +4,70 @@ import { h, ref } from 'vue';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { getColorWithOpacity } from "@/utils/common";
import { AiError, aiAcceptance } from "@/service/api/ai";
import Footer from './footer.vue';
const themeStore = useThemeStore();
const props = defineProps(['type','orgTree','isAiOpen','title','currentId','formInfo'])
const emit = defineEmits(['save','submit','confirmTab']);
const footerRef = ref<any>(null);
const editing = ref<any>(null);
const list = ref<{id:number,item:string,basis:string,standard:string,method:string,org_id:number}[]>([]);
const aiFail = (e: any) => {
if (e instanceof AiError) window.$message?.warning(`AI 分析失败:${e.message},可手动填写`);
else window.$message?.warning("AI 分析失败,可手动填写");
};
// 重新生成验收评价(footer 派发):依据申请表的变更目的与预期效果生成量化验收标准
const onRegenerate = async () => {
const purpose = (props.formInfo?.purposes ?? []).filter((x: string) => x && x.trim()).join('、');
const effect = props.formInfo?.effect ?? '';
if (!purpose && !effect.trim()) {
return window.$message?.warning("缺少变更目的与预期效果,请先在 [变更申请表] 填写");
}
footerRef.value?.setRegenerateLoading(true);
try {
const res = await aiAcceptance({ purpose, effect });
const rows = res?.rows ?? [];
if (!rows.length) {
return window.$message?.warning("AI 未生成验收项,可手动添加");
}
list.value = rows.map((r: any) => ({
id: 0,
item: r.item ?? '',
basis: r.basis ?? '',
standard: r.standard ?? '',
method: r.method ?? '',
// 建议责任方文本 → 组织树节点(按名称模糊匹配,匹配不到留空人工选择)
org_id: matchOrgId(r.owner_suggest),
}));
window.$message?.success("验收量化标准已由 AI 生成,均可手动修改");
} catch (e: any) {
aiFail(e);
} finally {
footerRef.value?.setRegenerateLoading(false);
}
}
// 按名称在组织树中查找节点 id
const matchOrgId = (name: string): number => {
if (!name || !name.trim()) return 0;
const hit = findNodeByLabel(props.orgTree ?? [], name.trim());
return hit?.id ?? 0;
}
const findNodeByLabel = (tree: any[], name: string): any => {
for (const node of tree) {
if (node.label && (name.includes(node.label) || node.label.includes(name))) return node;
if (node.children && node.children.length) {
const found: any = findNodeByLabel(node.children, name);
if (found) return found;
}
}
return null;
}
// 添加验收项
const addRow = () => {
list.value.push({ id: 0, item: '', basis: '', standard: '', method: '',org_id: 0 });
@@ -230,6 +284,7 @@ defineExpose({list})
@save="saveDraft"
@submit="submitConfirm"
@confirmTab="confirmTab"
@regenerate="onRegenerate"
/>
</template>
<style scoped lang="scss">
@@ -15,7 +15,7 @@ const props = defineProps([
'isAiOpen',
'currentForm'
])
const emit = defineEmits(['save','submit','confirmTab']);
const emit = defineEmits(['save','submit','confirmTab','regenerate']);
const generalLoading = inject('generalLoading', {saveLoading: ref<boolean>(false),btnDisabled: ref<boolean>(false)})
const {saveLoading,btnDisabled} = generalLoading
@@ -87,9 +87,13 @@ const approvalChain = computed(() => {
const download = async () => {
}
// 重新生成
// 重新生成:AI 调用逻辑由各标签页组件实现(按 type 分发),此处仅派发事件
const regenerate = async () => {
emit('regenerate', props.type);
}
// 供父组件控制「重新生成」按钮 loading
const setRegenerateLoading = (v: boolean) => {
regenerateLoading.value = v;
}
// 确认标签内容
@@ -137,7 +141,7 @@ const finishApprover = async () => {
approvalDrawerShow.value = false;
}
defineExpose({clearApprovers})
defineExpose({clearApprovers, setRegenerateLoading})
</script>
<template>
+52 -1
View File
@@ -4,17 +4,24 @@ import { ref } from 'vue';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import Footer from './footer.vue';
import { AiError, aiPssr } from "@/service/api/ai";
import { uploadFileApi } from "@/service/api/file";
const themeStore = useThemeStore();
const props = defineProps(['type','isAiOpen','title','currentId'])
const props = defineProps(['type','isAiOpen','title','currentId','formInfo','changeType'])
const emit = defineEmits(['save','submit','confirmTab']);
const footerRef = ref<any>(null);
const uploadLoading = ref<boolean>(false);
const uploadFileRef = ref<any>(null);
const isUploaded = ref<boolean>(false);
const editing = ref<any>(null);
const aiFail = (e: any) => {
if (e instanceof AiError) window.$message?.warning(`AI 分析失败:${e.message},可手动填写`);
else window.$message?.warning("AI 分析失败,可手动填写");
};
const list = ref<{ g: string; type:number; na_flag:number; items: any[] }[]>([
{ g: "一、风险分析行动项落实", type:1, na_flag:0, items: [] },
{ g: "二、现场设备安装检查确认", type:2, na_flag:0, items: [] },
@@ -23,6 +30,49 @@ const list = ref<{ g: string; type:number; na_flag:number; items: any[] }[]>([
]);
const pssrNa = ref<Record<string, boolean>>({});
// 重新生成 PSSR 检查内容(footer 派发):AI 输出按固定四组名称匹配回填
const onRegenerate = async () => {
const info = props.formInfo;
if (!info?.description) {
return window.$message?.warning("缺少变更描述,请先在 [变更预识别] 里面输入变更描述");
}
footerRef.value?.setRegenerateLoading(true);
try {
const res = await aiPssr({
content: info.description,
...(props.changeType ? { change_type: props.changeType } : {}),
});
const groups = (res?.groups ?? []).filter((g: any) => g && Array.isArray(g.items));
if (!groups.length) {
return window.$message?.warning("AI 未生成检查项,可手动添加");
}
const used = new Set<number>();
list.value = list.value.map((g) => {
// 优先按分组名匹配,兜底按剩余顺序对齐
let gi = groups.findIndex((x: any, i: number) => !used.has(i) && (x.g === g.g || x.g.includes(g.g) || g.g.includes(x.g)));
if (gi < 0) gi = groups.findIndex((_: any, i: number) => !used.has(i));
if (gi < 0) return g;
used.add(gi);
return {
...g,
na_flag: 0,
items: groups[gi].items.map((it: any) => ({
id: 0,
group_type: g.type,
content: it.content ?? '',
phase: it.phase_value ?? (it.phase === '关闭前' ? 2 : 1),
na_flag: 0,
})),
};
});
window.$message?.success("PSSR 检查清单已由 AI 生成,点击条目可直接编辑");
} catch (e: any) {
aiFail(e);
} finally {
footerRef.value?.setRegenerateLoading(false);
}
}
// 添加检查项
const addItem = (g:any, gi: number) => {
list.value[gi].items.push({id:0, group_type:g.type, content: '新检查项(点击直接编辑内容)', phase: 1, na_flag: 0 });
@@ -161,6 +211,7 @@ defineExpose({list})
@save="saveDraft"
@submit="submitConfirm"
@confirmTab="confirmTab"
@regenerate="onRegenerate"
/>
</template>
<style scoped lang="scss">