发起变更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
+294 -239
View File
@@ -1,240 +1,295 @@
<!-- 验收评价 -->
<script setup lang="ts">
import { h, ref } from 'vue';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { getColorWithOpacity } from "@/utils/common";
import Footer from './footer.vue';
const themeStore = useThemeStore();
const props = defineProps(['type','orgTree','isAiOpen','title','currentId','formInfo'])
const emit = defineEmits(['save','submit','confirmTab']);
const editing = ref<any>(null);
const list = ref<{id:number,item:string,basis:string,standard:string,method:string,org_id:number}[]>([]);
// 添加验收项
const addRow = () => {
list.value.push({ id: 0, item: '', basis: '', standard: '', method: '',org_id: 0 });
}
// 删除验收项
const deleteRow = (i:number) => {
list.value.splice(i, 1);
}
// 自定义渲染展开图标
const renderSwitcherIcon = (node:any) => {
// 如果节点没有 children 或 children 为空数组,则不显示图标
if (!node.option.children || node.option.children.length === 0) {
return null; // 返回 null 表示不渲染任何图标
}
// 有子节点:返回 undefined 表示使用默认展开图标,也可自定义
return h(Icon,{
icon: node.option.expanded ? 'bi:caret-down-fill' : 'bi:caret-right-fill',
class: 'size-12px',
});
}
// 保存草稿
const saveDraft = () => {
emit('save');
}
// 提交
const submitConfirm = () => {
emit('submit', props.type);
}
// // 确认标签内容
const confirmTab = (tab:string, val:boolean) => {
emit('confirmTab', tab, val);
}
const findNodeById = (tree:any,id:number) => {
for (const node of tree) {
if (node.id === id) return node;
if (node.children && node.children.length) {
const found:any = findNodeById(node.children,id);
if (found) return found;
}
}
return null; // 未找到
}
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="ix:success" class="size-18px text-emerald-500" />
验收评价(依据预期效果设定可量化验收标准)
</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="ml-auto text-xs text-slate-400">申请阶段由 AI 依据「预期效果」生成量化标准与验收方式,可手动修改;投用运行后由车间和各专业按标准验收并给出评估结论</span>
</div>
<div
class="space-y-1.5 rounded-lg border border-[var(--border-color)] bg-[var(--bg-color)] px-4 py-3 text-xs leading-6 text-slate-700"
:style="{
'--bg-color': getColorWithOpacity(themeStore.themeColor, 0.1),
'--border-color': getColorWithOpacity(themeStore.themeColor, 0.2)
}"
>
<div><b :style="{color: themeStore.themeColor}">变更目的:</b>{{ formInfo?.purposes?.join('、') || '' }}</div>
<div><b :style="{color: themeStore.themeColor}">预期效果:</b>{{ formInfo?.effect || '' }}</div>
<div
class="border-t border-[var(--border-color)] pt-1.5 text-slate-500"
:style="{'--border-color': getColorWithOpacity(themeStore.themeColor, 0.2)}"
>
<b :style="{color: themeStore.themeColor}">对应说明:</b>验收项目由预期效果逐条拆解设定,各项目对应的预期效果来源见表格「验收项目」列下方灰色小字(点击可修改);验收时须逐项对照预期效果判定达成程度。
</div>
</div>
<div class="overflow-x-auto border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<table class="w-full min-w-[900px] border-collapse text-sm">
<thead>
<tr class="bg-slate-100 text-left text-slate-600">
<th class="w-12 px-3 py-2 font-medium">#</th>
<th class="w-56 px-3 py-2 font-medium">验收项目(含对应预期效果)</th>
<th class="px-3 py-2 font-medium">量化验收标准</th>
<th class="px-3 py-2 font-medium">验收方式(可执行)</th>
<th class="w-46 px-3 py-2 font-medium">组织层级</th>
<th class="w-12 px-2 py-2 font-medium text-center">操作</th>
</tr>
</thead>
<tbody>
<tr v-if="list.length===0">
<td colspan="6" class="py-4 px-2 text-center text-slate-400">暂无验收项目</td>
</tr>
<tr v-for="(row, i) in list" :key="i" class="align-top">
<td class="border-t px-3 py-2.5 text-slate-400">{{ i + 1 }}</td>
<td class="border-t px-3 py-2.5">
<NInput
v-if="editing === `item-${i}-${row.item}`"
class="text-xs"
v-model:value="row.item"
type="textarea"
autofocus
rows="2"
@blur="editing = null"
/>
<button v-else @click="editing = `item-${i}-${row.item}`" title="点击修改"
class="bg-transparent block w-full rounded-md px-2 py-1 text-left text-xs font-medium leading-relaxed text-slate-700 transition hover:bg-[#EAF0F9]"
>
{{ row.item || '点击填写验收项目' }}
</button>
<NInput
v-if="editing === `basis-${i}-${row.basis}`"
class="text-[11px]"
size="small"
v-model:value="row.basis"
autofocus
@blur="editing = null"
/>
<button v-else @click="editing = `basis-${i}-${row.basis}`" title="本项目对应的预期效果来源,点击修改"
class="bg-transparent mt-0.5 block w-full rounded px-2 text-left text-[11px] text-slate-400 transition hover:bg-[#EAF0F9] hover:text-slate-500"
>
对应预期:{{ row.basis || '(点击填写对应预期效果)' }}
</button>
</td>
<td class="border-t px-3 py-2.5">
<NInput
v-if="editing === `standard-${i}-${row.standard}`"
class="text-xs"
v-model:value="row.standard"
type="textarea"
autofocus
rows="2"
@blur="editing = null"
/>
<button v-else @click="editing = `standard-${i}-${row.standard}`" title="点击修改"
class="bg-transparent block w-full rounded-md px-2 py-1 text-left text-xs leading-relaxed text-slate-700 transition hover:bg-[#EAF0F9]"
>
{{ row.standard || '点击填写量化验收标准' }}
</button>
</td>
<td class="border-t px-3 py-2.5">
<NInput
v-if="editing === `method-${i}-${row.method}`"
class="text-xs"
v-model:value="row.method"
type="textarea"
autofocus
rows="2"
@blur="editing = null"
/>
<button v-else @click="editing = `method-${i}-${row.method}`" title="点击修改"
class="bg-transparent block w-full rounded-md px-2 py-1 text-left text-xs leading-relaxed text-slate-700 transition hover:bg-[#EAF0F9]"
>
{{ row.method || '点击填写验收方式' }}
</button>
</td>
<td class="border-t px-3 py-2.5">
<NTreeSelect
v-if="editing === `org-${i}-${row.org_id}`"
size="small"
v-model:value="row.org_id"
default-expand-all
label-field="label"
key-field="id"
:options="props.orgTree"
:render-switcher-icon="renderSwitcherIcon"
placeholder="请选择组织层级"
@blur="editing = null"
/>
<button v-else @click="editing = `org-${i}-${row.org_id}`" title="点击修改"
class="bg-transparent block w-full rounded-md px-2 py-1 text-left text-xs leading-relaxed text-slate-700 transition hover:bg-[#EAF0F9]"
>
{{ findNodeById(orgTree,row.org_id)?.label || '点击选择组织层级' }}
</button>
</td>
<td class="border-t px-2 py-2.5 text-center">
<NPopconfirm
positive-text="确定"
:positiveButtonProps="{ size: 'tiny' }"
:negativeButtonProps="{ size: 'tiny' }"
@positive-click="deleteRow(i)"
>
<template #trigger>
<NButton text type="error" class="mt-1.5" title="删除本验收项">
<Icon icon="iconamoon:trash" class="size-16px" />
</NButton>
</template>
确定删除吗?
</NPopconfirm>
</td>
</tr>
</tbody>
</table>
</div>
<div class="flex items-center gap-3">
<NButton ghost class="text-xs" @click="addRow">
<Icon icon="ic:round-plus" class="size-16px" />添加验收项
</NButton>
<span class="text-xs text-slate-400">评估结论在验收阶段由责任单位填写:点击选择「完全达到 / 基本达到 / 未达成」,再次点击取消;行尾可删除验收项</span>
</div>
<div class="rounded-lg bg-emerald-50/80 px-4 py-2.5 text-xs leading-5 text-emerald-700">
流程说明:投用运行满验收周期后,系统向车间(属地)与涉及专业推送验收任务;各责任单位按上表逐项给出评估结论并上传佐证(数据报表 / 化验报告 / 现场照片),全部完成后汇总至综合验收结论,结论与佐证资料一并纳入变更关闭校验,验收不通过时退回整改或评估恢复原状。
</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;
}
<!-- 验收评价 -->
<script setup lang="ts">
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 });
}
// 删除验收项
const deleteRow = (i:number) => {
list.value.splice(i, 1);
}
// 自定义渲染展开图标
const renderSwitcherIcon = (node:any) => {
// 如果节点没有 children 或 children 为空数组,则不显示图标
if (!node.option.children || node.option.children.length === 0) {
return null; // 返回 null 表示不渲染任何图标
}
// 有子节点:返回 undefined 表示使用默认展开图标,也可自定义
return h(Icon,{
icon: node.option.expanded ? 'bi:caret-down-fill' : 'bi:caret-right-fill',
class: 'size-12px',
});
}
// 保存草稿
const saveDraft = () => {
emit('save');
}
// 提交
const submitConfirm = () => {
emit('submit', props.type);
}
// // 确认标签内容
const confirmTab = (tab:string, val:boolean) => {
emit('confirmTab', tab, val);
}
const findNodeById = (tree:any,id:number) => {
for (const node of tree) {
if (node.id === id) return node;
if (node.children && node.children.length) {
const found:any = findNodeById(node.children,id);
if (found) return found;
}
}
return null; // 未找到
}
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="ix:success" class="size-18px text-emerald-500" />
验收评价(依据预期效果设定可量化验收标准)
</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="ml-auto text-xs text-slate-400">申请阶段由 AI 依据「预期效果」生成量化标准与验收方式,可手动修改;投用运行后由车间和各专业按标准验收并给出评估结论</span>
</div>
<div
class="space-y-1.5 rounded-lg border border-[var(--border-color)] bg-[var(--bg-color)] px-4 py-3 text-xs leading-6 text-slate-700"
:style="{
'--bg-color': getColorWithOpacity(themeStore.themeColor, 0.1),
'--border-color': getColorWithOpacity(themeStore.themeColor, 0.2)
}"
>
<div><b :style="{color: themeStore.themeColor}">变更目的:</b>{{ formInfo?.purposes?.join('、') || '' }}</div>
<div><b :style="{color: themeStore.themeColor}">预期效果:</b>{{ formInfo?.effect || '' }}</div>
<div
class="border-t border-[var(--border-color)] pt-1.5 text-slate-500"
:style="{'--border-color': getColorWithOpacity(themeStore.themeColor, 0.2)}"
>
<b :style="{color: themeStore.themeColor}">对应说明:</b>验收项目由预期效果逐条拆解设定,各项目对应的预期效果来源见表格「验收项目」列下方灰色小字(点击可修改);验收时须逐项对照预期效果判定达成程度。
</div>
</div>
<div class="overflow-x-auto border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<table class="w-full min-w-[900px] border-collapse text-sm">
<thead>
<tr class="bg-slate-100 text-left text-slate-600">
<th class="w-12 px-3 py-2 font-medium">#</th>
<th class="w-56 px-3 py-2 font-medium">验收项目(含对应预期效果)</th>
<th class="px-3 py-2 font-medium">量化验收标准</th>
<th class="px-3 py-2 font-medium">验收方式(可执行)</th>
<th class="w-46 px-3 py-2 font-medium">组织层级</th>
<th class="w-12 px-2 py-2 font-medium text-center">操作</th>
</tr>
</thead>
<tbody>
<tr v-if="list.length===0">
<td colspan="6" class="py-4 px-2 text-center text-slate-400">暂无验收项目</td>
</tr>
<tr v-for="(row, i) in list" :key="i" class="align-top">
<td class="border-t px-3 py-2.5 text-slate-400">{{ i + 1 }}</td>
<td class="border-t px-3 py-2.5">
<NInput
v-if="editing === `item-${i}-${row.item}`"
class="text-xs"
v-model:value="row.item"
type="textarea"
autofocus
rows="2"
@blur="editing = null"
/>
<button v-else @click="editing = `item-${i}-${row.item}`" title="点击修改"
class="bg-transparent block w-full rounded-md px-2 py-1 text-left text-xs font-medium leading-relaxed text-slate-700 transition hover:bg-[#EAF0F9]"
>
{{ row.item || '点击填写验收项目' }}
</button>
<NInput
v-if="editing === `basis-${i}-${row.basis}`"
class="text-[11px]"
size="small"
v-model:value="row.basis"
autofocus
@blur="editing = null"
/>
<button v-else @click="editing = `basis-${i}-${row.basis}`" title="本项目对应的预期效果来源,点击修改"
class="bg-transparent mt-0.5 block w-full rounded px-2 text-left text-[11px] text-slate-400 transition hover:bg-[#EAF0F9] hover:text-slate-500"
>
对应预期:{{ row.basis || '(点击填写对应预期效果)' }}
</button>
</td>
<td class="border-t px-3 py-2.5">
<NInput
v-if="editing === `standard-${i}-${row.standard}`"
class="text-xs"
v-model:value="row.standard"
type="textarea"
autofocus
rows="2"
@blur="editing = null"
/>
<button v-else @click="editing = `standard-${i}-${row.standard}`" title="点击修改"
class="bg-transparent block w-full rounded-md px-2 py-1 text-left text-xs leading-relaxed text-slate-700 transition hover:bg-[#EAF0F9]"
>
{{ row.standard || '点击填写量化验收标准' }}
</button>
</td>
<td class="border-t px-3 py-2.5">
<NInput
v-if="editing === `method-${i}-${row.method}`"
class="text-xs"
v-model:value="row.method"
type="textarea"
autofocus
rows="2"
@blur="editing = null"
/>
<button v-else @click="editing = `method-${i}-${row.method}`" title="点击修改"
class="bg-transparent block w-full rounded-md px-2 py-1 text-left text-xs leading-relaxed text-slate-700 transition hover:bg-[#EAF0F9]"
>
{{ row.method || '点击填写验收方式' }}
</button>
</td>
<td class="border-t px-3 py-2.5">
<NTreeSelect
v-if="editing === `org-${i}-${row.org_id}`"
size="small"
v-model:value="row.org_id"
default-expand-all
label-field="label"
key-field="id"
:options="props.orgTree"
:render-switcher-icon="renderSwitcherIcon"
placeholder="请选择组织层级"
@blur="editing = null"
/>
<button v-else @click="editing = `org-${i}-${row.org_id}`" title="点击修改"
class="bg-transparent block w-full rounded-md px-2 py-1 text-left text-xs leading-relaxed text-slate-700 transition hover:bg-[#EAF0F9]"
>
{{ findNodeById(orgTree,row.org_id)?.label || '点击选择组织层级' }}
</button>
</td>
<td class="border-t px-2 py-2.5 text-center">
<NPopconfirm
positive-text="确定"
:positiveButtonProps="{ size: 'tiny' }"
:negativeButtonProps="{ size: 'tiny' }"
@positive-click="deleteRow(i)"
>
<template #trigger>
<NButton text type="error" class="mt-1.5" title="删除本验收项">
<Icon icon="iconamoon:trash" class="size-16px" />
</NButton>
</template>
确定删除吗?
</NPopconfirm>
</td>
</tr>
</tbody>
</table>
</div>
<div class="flex items-center gap-3">
<NButton ghost class="text-xs" @click="addRow">
<Icon icon="ic:round-plus" class="size-16px" />添加验收项
</NButton>
<span class="text-xs text-slate-400">评估结论在验收阶段由责任单位填写:点击选择「完全达到 / 基本达到 / 未达成」,再次点击取消;行尾可删除验收项</span>
</div>
<div class="rounded-lg bg-emerald-50/80 px-4 py-2.5 text-xs leading-5 text-emerald-700">
流程说明:投用运行满验收周期后,系统向车间(属地)与涉及专业推送验收任务;各责任单位按上表逐项给出评估结论并上传佐证(数据报表 / 化验报告 / 现场照片),全部完成后汇总至综合验收结论,结论与佐证资料一并纳入变更关闭校验,验收不通过时退回整改或评估恢复原状。
</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>