发起变更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
+221 -170
View File
@@ -1,171 +1,222 @@
<!-- PSSR检查内容 -->
<script setup lang="ts">
import { ref } from 'vue';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import Footer from './footer.vue';
import { uploadFileApi } from "@/service/api/file";
const themeStore = useThemeStore();
const props = defineProps(['type','isAiOpen','title','currentId'])
const emit = defineEmits(['save','submit','confirmTab']);
const uploadLoading = ref<boolean>(false);
const uploadFileRef = ref<any>(null);
const isUploaded = ref<boolean>(false);
const editing = ref<any>(null);
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: [] },
{ g: "三、保护措施确认", type:3, na_flag:0, items: [] },
{ g: "四、资料归档确认", type:4, na_flag:0, items: [] },
]);
const pssrNa = ref<Record<string, boolean>>({});
// 添加检查项
const addItem = (g:any, gi: number) => {
list.value[gi].items.push({id:0, group_type:g.type, content: '新检查项(点击直接编辑内容)', phase: 1, na_flag: 0 });
}
// 上传纸质确认表
const uploadFile = async (e: any) => {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('file', file);
formData.append('step', '5');
uploadLoading.value = true;
const {error} = await uploadFileApi(props.currentId, formData);
if(!error){
isUploaded.value = true;
window.$message?.success(`已上传 PSSR 纸质确认表:不做内容识别,上传即视为全部检查项完成确认(「不涉及」组除外),原件归档至变更档案`);
}
uploadLoading.value = false;
};
// 保存草稿
const saveDraft = () => {
emit('save');
}
// 提交
const submitConfirm = () => {
emit('submit', props.type);
}
// // 确认标签内容
const confirmTab = (tab:string, val:boolean) => {
emit('confirmTab', tab, val);
}
defineExpose({list})
</script>
<template>
<div class="p-4">
<div class="space-y-4 scroll">
<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>
<span class="flex items-center gap-2">
<NButton ghost size="small" :disabled="uploadLoading" class="text-xs" title="上传 PSSR 纸质确认表照片 / 扫描件(JPG / PNG / PDF),系统不做内容识别,上传即视为全部检查项完成确认" @click="uploadFileRef?.click()">
<Icon v-if="uploadLoading" icon="ri:loader-4-fill" class="size-14px animate-spin" />
<Icon v-else icon="material-symbols:upload" class="size-14px" />
上传纸质确认表
</NButton>
<input ref="uploadFileRef" type="file" accept="image/*,.pdf" class="hidden" @change="uploadFile" />
<span class="hidden text-xs text-slate-400 xl:inline">申请阶段点击条目内容直接编辑可增删项</span>
</span>
</div>
<div v-if="isUploaded" 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 纸质确认表上传即视为全部检查项完成确认未做内容识别原件已归档至变更档案
</div>
<div v-for="(g, gi) in list" :key="g.g" class="border" :class="g.na_flag ? '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="g.na_flag" size="small" class="ml-2">不涉及</NTag>
<div class="ml-auto flex items-center">
<NButton v-if="g.items.length" text type="primary" class="text-xs font-normal hover:underline"
:title="pssrNa[g.g] ? '恢复本组检查清单' : '本次变更不涉及本组内容时点击标记,标记后不计入完成统计'"
@click="g.na_flag = g.na_flag ? 0 : 1">
{{ g.na_flag ? '恢复清单' : '标记不涉及' }}
</NButton>
<NButton text type="primary" v-if="!g.na_flag" class="ml-3 text-xs font-normal hover:underline" @click="addItem(g,gi)">
<Icon icon="ic:round-plus" class="size-14px" /> 添加检查项
</NButton>
</div>
</div>
<div v-if="g.na_flag" class="border-t bg-slate-50 px-3 py-3 text-xs text-slate-400">
本组内容经评估本次变更不涉及无现场安装作业 / 不改变保护措施 / 无需归档资料已不计入 PSSR 完成统计如需恢复检查清单点击右上角恢复清单
</div>
<template v-else>
<div v-if="g.items.length===0" class="py-3 px-2 text-center text-slate-400 text-xs">暂无检查项</div>
<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.content"
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.content }}
</button>
<span class="flex items-center gap-1 py-1">
<NRadio :checked="it.phase === 1" :value="1" label="投用前完成"
@change="list = list.map((x, xi) => xi === gi ? { ...x, items: x.items.map((y, yi) => yi === i ? { ...y, phase: 1 } : y) } : x)" />
<NRadio :checked="it.phase === 2" :value="2" label="关闭前完成"
@change="list = list.map((x, xi) => xi === gi ? { ...x, items: x.items.map((y, yi) => yi === i ? { ...y, phase: 2 } : y) } : x)" />
</span>
<NPopconfirm
positive-text="确定"
:positiveButtonProps="{ size: 'tiny' }"
:negativeButtonProps="{ size: 'tiny' }"
@positive-click="list = list.map((x, xi) => xi === gi ? { ...x, items: x.items.filter((_, yi) => yi !== i) } : x)"
>
<template #trigger>
<NButton text type="error" title="删除该项">
<Icon icon="iconamoon:trash" class="size-16px" />
</NButton>
</template>
确定删除吗?
</NPopconfirm>
</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 依据变更类型与风险分析自动生成,专业人员审核后随审批发布;确认方式不作硬性要求——可直接点击确认;资料归档组确认后自动同步「变更关闭确认表」显示已归档。
</div>
</div>
</div>
<Footer
ref="footerRef"
:type="props.type"
:currentId="props.currentId"
:title="props.title"
:isAiOpen="props.isAiOpen"
@save="saveDraft"
@submit="submitConfirm"
@confirmTab="confirmTab"
/>
</template>
<style scoped lang="scss">
.scroll {
height: calc(100vh - 393px);
overflow-y: auto;
}
<!-- PSSR检查内容 -->
<script setup lang="ts">
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','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: [] },
{ g: "三、保护措施确认", type:3, na_flag:0, items: [] },
{ g: "四、资料归档确认", type:4, na_flag:0, items: [] },
]);
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 });
}
// 上传纸质确认表
const uploadFile = async (e: any) => {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('file', file);
formData.append('step', '5');
uploadLoading.value = true;
const {error} = await uploadFileApi(props.currentId, formData);
if(!error){
isUploaded.value = true;
window.$message?.success(`已上传 PSSR 纸质确认表:不做内容识别,上传即视为全部检查项完成确认(「不涉及」组除外),原件归档至变更档案`);
}
uploadLoading.value = false;
};
// 保存草稿
const saveDraft = () => {
emit('save');
}
// 提交
const submitConfirm = () => {
emit('submit', props.type);
}
// // 确认标签内容
const confirmTab = (tab:string, val:boolean) => {
emit('confirmTab', tab, val);
}
defineExpose({list})
</script>
<template>
<div class="p-4">
<div class="space-y-4 scroll">
<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>
<span class="flex items-center gap-2">
<NButton ghost size="small" :disabled="uploadLoading" class="text-xs" title="上传 PSSR 纸质确认表照片 / 扫描件(JPG / PNG / PDF),系统不做内容识别,上传即视为全部检查项完成确认" @click="uploadFileRef?.click()">
<Icon v-if="uploadLoading" icon="ri:loader-4-fill" class="size-14px animate-spin" />
<Icon v-else icon="material-symbols:upload" class="size-14px" />
上传纸质确认表
</NButton>
<input ref="uploadFileRef" type="file" accept="image/*,.pdf" class="hidden" @change="uploadFile" />
<span class="hidden text-xs text-slate-400 xl:inline">申请阶段点击条目内容直接编辑可增删项</span>
</span>
</div>
<div v-if="isUploaded" 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 纸质确认表上传即视为全部检查项完成确认未做内容识别原件已归档至变更档案
</div>
<div v-for="(g, gi) in list" :key="g.g" class="border" :class="g.na_flag ? '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="g.na_flag" size="small" class="ml-2">不涉及</NTag>
<div class="ml-auto flex items-center">
<NButton v-if="g.items.length" text type="primary" class="text-xs font-normal hover:underline"
:title="pssrNa[g.g] ? '恢复本组检查清单' : '本次变更不涉及本组内容时点击标记,标记后不计入完成统计'"
@click="g.na_flag = g.na_flag ? 0 : 1">
{{ g.na_flag ? '恢复清单' : '标记不涉及' }}
</NButton>
<NButton text type="primary" v-if="!g.na_flag" class="ml-3 text-xs font-normal hover:underline" @click="addItem(g,gi)">
<Icon icon="ic:round-plus" class="size-14px" /> 添加检查项
</NButton>
</div>
</div>
<div v-if="g.na_flag" class="border-t bg-slate-50 px-3 py-3 text-xs text-slate-400">
本组内容经评估本次变更不涉及无现场安装作业 / 不改变保护措施 / 无需归档资料已不计入 PSSR 完成统计如需恢复检查清单点击右上角恢复清单
</div>
<template v-else>
<div v-if="g.items.length===0" class="py-3 px-2 text-center text-slate-400 text-xs">暂无检查项</div>
<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.content"
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.content }}
</button>
<span class="flex items-center gap-1 py-1">
<NRadio :checked="it.phase === 1" :value="1" label="投用前完成"
@change="list = list.map((x, xi) => xi === gi ? { ...x, items: x.items.map((y, yi) => yi === i ? { ...y, phase: 1 } : y) } : x)" />
<NRadio :checked="it.phase === 2" :value="2" label="关闭前完成"
@change="list = list.map((x, xi) => xi === gi ? { ...x, items: x.items.map((y, yi) => yi === i ? { ...y, phase: 2 } : y) } : x)" />
</span>
<NPopconfirm
positive-text="确定"
:positiveButtonProps="{ size: 'tiny' }"
:negativeButtonProps="{ size: 'tiny' }"
@positive-click="list = list.map((x, xi) => xi === gi ? { ...x, items: x.items.filter((_, yi) => yi !== i) } : x)"
>
<template #trigger>
<NButton text type="error" title="删除该项">
<Icon icon="iconamoon:trash" class="size-16px" />
</NButton>
</template>
确定删除吗?
</NPopconfirm>
</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 依据变更类型与风险分析自动生成,专业人员审核后随审批发布;确认方式不作硬性要求——可直接点击确认;资料归档组确认后自动同步「变更关闭确认表」显示已归档。
</div>
</div>
</div>
<Footer
ref="footerRef"
:type="props.type"
:currentId="props.currentId"
:title="props.title"
:isAiOpen="props.isAiOpen"
@save="saveDraft"
@submit="submitConfirm"
@confirmTab="confirmTab"
@regenerate="onRegenerate"
/>
</template>
<style scoped lang="scss">
.scroll {
height: calc(100vh - 393px);
overflow-y: auto;
}
</style>