发起变更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
+211 -207
View File
@@ -1,208 +1,212 @@
<!-- 通用底部操作 -->
<script setup lang="ts">
import { computed, inject, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { Icon } from '@iconify/vue'
import { useChangeStore } from '@/store/modules/change';
const changeStore = useChangeStore();
const { getStepsInfo, getFlowInfo } = storeToRefs(changeStore);
const props = defineProps([
'type',
'currentId',
'title',
'isAiOpen',
'currentForm'
])
const emit = defineEmits(['save','submit','confirmTab']);
const generalLoading = inject('generalLoading', {saveLoading: ref<boolean>(false),btnDisabled: ref<boolean>(false)})
const {saveLoading,btnDisabled} = generalLoading
const approvalDrawerShow = ref<boolean>(false);
const downloadLoading = ref<boolean>(false);
const regenerateLoading = ref<boolean>(false);
const submitLoading = ref<boolean>(false);
const confirmed = ref<boolean>(false);
// 存储每个步骤选中的审核人 user_id
const approvers = ref<Record<number, number[]>>({})
const flowList = ref<any[]>([]);
// 审批链
const approvalFlowText = computed(() => {
if (!flowList.value || flowList.value.length === 0) return '暂无步骤'
// 按 step_order 排序
const sorted = [...flowList.value].sort((a, b) => a.step_order - b.step_order)
return sorted.map(item => item.name).join(' → ')
})
// 清空审批人
const clearApprovers = () => {
approvers.value = {};
}
// 打开审批流程抽屉
const openApprovalDrawer = () => {
if(props.type==='form'){
if(!props.currentForm?.change_type || !props.currentForm?.change_level || !props.currentForm?.duration_type){
return window.$message?.warning('请先选择变更类型、变更等级、变更时限');
}
}
flowList.value = getStepsInfo.value;
if(Object.keys(approvers.value).length === 0){
flowList.value.forEach((item: any) => {
if (item.members && item.members.length > 0) {
// 优先选 is_sign=1 的成员 ,否则选第一个
const defaultMembers = item.members
.filter((m: any) => m.is_sign === 1)
.map((m: any) => m.user_id);
approvers.value[item.id] = defaultMembers.length > 0 ? defaultMembers : [item.members[0].user_id];
}
})
}
approvalDrawerShow.value = true;
}
// 审批链
const approvalChain = computed(() => {
let template = getFlowInfo.value;
if (!template || template.length === 0) return '暂无审批步骤'
const parts = template.map((step: any) => {
// 根据 countersign 决定显示名称
const displayName = step.countersign ? '专业会签' : step.name
const members = step.selectedMember
let memberText;
if (members && members.length > 0) {
memberText = members.map((m: any) => `${m.user_name}(${m.org_name})`).join('、');
} else {
memberText = '未指定';
}
return `${displayName}:${memberText}`;
})
return parts.join(' → ')
})
// 下载
const download = async () => {
}
// 重新生成
const regenerate = async () => {
}
// 确认标签内容
const confirmTab = async () => {
confirmed.value = !confirmed.value;
emit('confirmTab', props.type, confirmed.value);
}
// 保存
const saveDraft = async () => {
emit('save');
}
// 提交
const submitApply = () => {
emit('submit');
}
// 更新选中审批人 user_id
const updateApprover = (stepId: number, usersId: number[]) => {
approvers.value[stepId] = usersId
}
// 匹配审批人
function matchApprovers(steps: any[], mapping: Record<number, number[]>) {
const result = [];
for (const [stepId, usersId] of Object.entries(mapping)) {
const step = steps.find(s => s.id === Number(stepId));
if (step) {
const members = step.members.filter((m: any) => usersId.includes(m.user_id));
if (members.length > 0) {
result.push({
countersign: step.countersign,
stepId: step.id,
name: step.name,
selectedMember: members
});
}
}
}
return result;
}
// 完成设置
const finishApprover = async () => {
const matched = matchApprovers(flowList.value, approvers.value);
changeStore.updateFlowInfo(matched);
window.$message?.success("审批流程设置完成");
approvalDrawerShow.value = false;
}
defineExpose({clearApprovers})
</script>
<template>
<div>
<div class="flex flex-wrap items-center justify-between gap-2 border-t p-4">
<div class="flex flex-wrap gap-2">
<NButton ghost type="success" :disabled="btnDisabled" :loading="downloadLoading" @click="download">
<Icon icon="material-symbols:download" class="size-16px" />
<span v-if="props.type==='form'">下载变更申请表</span>
<span v-if="props.type==='risk'">下载风险分析记录表</span>
<span v-if="props.type==='train'">下载变更培训内容</span>
<span v-if="props.type==='pssr'">下载PSSR检查内容</span>
<span v-if="props.type==='evaluation'">下载验收评价</span>
<span v-if="props.type==='close'">下载变更关闭确认表</span>
</NButton>
<NButton v-if="props.isAiOpen" ghost type="warning" :disabled="btnDisabled" :loading="regenerateLoading" @click="regenerate">
<Icon icon="tdesign:refresh" class="size-14px mr-1" />
<span v-if="props.type==='form'">重新生成变更申请表</span>
<span v-if="props.type==='risk'">重新生成风险分析记录表</span>
<span v-if="props.type==='train'">重新生成变更培训内容</span>
<span v-if="props.type==='pssr'">重新生成PSSR检查内容</span>
<span v-if="props.type==='evaluation'">重新生成验收评价</span>
<span v-if="props.type==='close'">重新生成变更关闭确认表</span>
</NButton>
<NButton v-if="props.isAiOpen" ghost :type="confirmed ? 'success' : 'primary'" :disabled="btnDisabled" @click="confirmTab">
<Icon icon="ix:success" class="size-16px mr-1" /> {{ confirmed ? '已确认完毕' : '本标签内容确认完毕' }}
</NButton>
</div>
</div>
<div class="flex flex-wrap items-center gap-x-4 gap-y-2 bg-[#EAF0F9] px-4 py-2.5 shadow-[0_-2px_8px_rgba(0,0,0,0.05)]">
<div class="flex min-w-0 flex-1 flex-wrap items-center gap-x-2 gap-y-1 text-xs text-slate-500">
<Icon icon="lucide:users" class="size-14px" />
<span class="min-w-0 truncate">
部门审核:{{ approvalChain }}
</span>
<NButton v-if="getStepsInfo && getStepsInfo.length" ghost size="small" type="primary" :disabled="btnDisabled" @click="openApprovalDrawer">
<Icon icon="octicon:sliders-24" class="size-14px" />
审批流程设置
</NButton>
</div>
<div class="ml-auto flex shrink-0 items-center gap-2">
<NButton size="small" type="primary" :disabled="btnDisabled" :loading="saveLoading" @click="saveDraft">保存</NButton>
<NButton size="small" type="primary" @click="submitApply" :disabled="btnDisabled" :loading="submitLoading">
提交 <Icon icon="formkit:right" class="size-14px" />
</NButton>
</div>
</div>
</div>
<!-- 审批流程设置 -->
<NDrawer v-model:show="approvalDrawerShow" placement="right" width="28rem">
<NDrawerContent>
<template #header>
<div class="text-base font-semibold text-slate-800">审批流程设置</div>
</template>
<div class="h-full space-y-4 overflow-y-auto">
<p class="text-xs leading-relaxed text-slate-400">审批链:{{ approvalFlowText }}。专业会签人由申请表「涉及专业」勾选自动增减,可在此调整人选;重要变更自动追加安全审查与分管领导终批。</p>
<div class="rounded-lg border p-3" v-for="(item,index) in flowList" :key="item.id">
<div class="mb-2 flex items-center gap-2 text-sm font-semibold text-slate-700">
<NTag type="info" size="small">{{Number(index)+1}}</NTag> {{item.name}} {{item.countersign ? '(专业会签)' : ''}}
</div>
<NSelect :value="approvers[item.id]" multiple :options="item.members" label-field="user_name" value-field="user_id"
@update:value="(v) => updateApprover(item.id, v)" />
</div>
<NButton type="primary" class="w-full" @click="finishApprover">完成设置</NButton>
</div>
</NDrawerContent>
</NDrawer>
<!-- 通用底部操作 -->
<script setup lang="ts">
import { computed, inject, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { Icon } from '@iconify/vue'
import { useChangeStore } from '@/store/modules/change';
const changeStore = useChangeStore();
const { getStepsInfo, getFlowInfo } = storeToRefs(changeStore);
const props = defineProps([
'type',
'currentId',
'title',
'isAiOpen',
'currentForm'
])
const emit = defineEmits(['save','submit','confirmTab','regenerate']);
const generalLoading = inject('generalLoading', {saveLoading: ref<boolean>(false),btnDisabled: ref<boolean>(false)})
const {saveLoading,btnDisabled} = generalLoading
const approvalDrawerShow = ref<boolean>(false);
const downloadLoading = ref<boolean>(false);
const regenerateLoading = ref<boolean>(false);
const submitLoading = ref<boolean>(false);
const confirmed = ref<boolean>(false);
// 存储每个步骤选中的审核人 user_id
const approvers = ref<Record<number, number[]>>({})
const flowList = ref<any[]>([]);
// 审批链
const approvalFlowText = computed(() => {
if (!flowList.value || flowList.value.length === 0) return '暂无步骤'
// 按 step_order 排序
const sorted = [...flowList.value].sort((a, b) => a.step_order - b.step_order)
return sorted.map(item => item.name).join(' → ')
})
// 清空审批人
const clearApprovers = () => {
approvers.value = {};
}
// 打开审批流程抽屉
const openApprovalDrawer = () => {
if(props.type==='form'){
if(!props.currentForm?.change_type || !props.currentForm?.change_level || !props.currentForm?.duration_type){
return window.$message?.warning('请先选择变更类型、变更等级、变更时限');
}
}
flowList.value = getStepsInfo.value;
if(Object.keys(approvers.value).length === 0){
flowList.value.forEach((item: any) => {
if (item.members && item.members.length > 0) {
// 优先选 is_sign=1 的成员 ,否则选第一个
const defaultMembers = item.members
.filter((m: any) => m.is_sign === 1)
.map((m: any) => m.user_id);
approvers.value[item.id] = defaultMembers.length > 0 ? defaultMembers : [item.members[0].user_id];
}
})
}
approvalDrawerShow.value = true;
}
// 审批链
const approvalChain = computed(() => {
let template = getFlowInfo.value;
if (!template || template.length === 0) return '暂无审批步骤'
const parts = template.map((step: any) => {
// 根据 countersign 决定显示名称
const displayName = step.countersign ? '专业会签' : step.name
const members = step.selectedMember
let memberText;
if (members && members.length > 0) {
memberText = members.map((m: any) => `${m.user_name}(${m.org_name})`).join('、');
} else {
memberText = '未指定';
}
return `${displayName}:${memberText}`;
})
return parts.join(' → ')
})
// 下载
const download = async () => {
}
// 重新生成:AI 调用逻辑由各标签页组件实现(按 type 分发),此处仅派发事件
const regenerate = async () => {
emit('regenerate', props.type);
}
// 供父组件控制「重新生成」按钮 loading
const setRegenerateLoading = (v: boolean) => {
regenerateLoading.value = v;
}
// 确认标签内容
const confirmTab = async () => {
confirmed.value = !confirmed.value;
emit('confirmTab', props.type, confirmed.value);
}
// 保存
const saveDraft = async () => {
emit('save');
}
// 提交
const submitApply = () => {
emit('submit');
}
// 更新选中审批人 user_id
const updateApprover = (stepId: number, usersId: number[]) => {
approvers.value[stepId] = usersId
}
// 匹配审批人
function matchApprovers(steps: any[], mapping: Record<number, number[]>) {
const result = [];
for (const [stepId, usersId] of Object.entries(mapping)) {
const step = steps.find(s => s.id === Number(stepId));
if (step) {
const members = step.members.filter((m: any) => usersId.includes(m.user_id));
if (members.length > 0) {
result.push({
countersign: step.countersign,
stepId: step.id,
name: step.name,
selectedMember: members
});
}
}
}
return result;
}
// 完成设置
const finishApprover = async () => {
const matched = matchApprovers(flowList.value, approvers.value);
changeStore.updateFlowInfo(matched);
window.$message?.success("审批流程设置完成");
approvalDrawerShow.value = false;
}
defineExpose({clearApprovers, setRegenerateLoading})
</script>
<template>
<div>
<div class="flex flex-wrap items-center justify-between gap-2 border-t p-4">
<div class="flex flex-wrap gap-2">
<NButton ghost type="success" :disabled="btnDisabled" :loading="downloadLoading" @click="download">
<Icon icon="material-symbols:download" class="size-16px" />
<span v-if="props.type==='form'">下载变更申请表</span>
<span v-if="props.type==='risk'">下载风险分析记录表</span>
<span v-if="props.type==='train'">下载变更培训内容</span>
<span v-if="props.type==='pssr'">下载PSSR检查内容</span>
<span v-if="props.type==='evaluation'">下载验收评价</span>
<span v-if="props.type==='close'">下载变更关闭确认表</span>
</NButton>
<NButton v-if="props.isAiOpen" ghost type="warning" :disabled="btnDisabled" :loading="regenerateLoading" @click="regenerate">
<Icon icon="tdesign:refresh" class="size-14px mr-1" />
<span v-if="props.type==='form'">重新生成变更申请表</span>
<span v-if="props.type==='risk'">重新生成风险分析记录表</span>
<span v-if="props.type==='train'">重新生成变更培训内容</span>
<span v-if="props.type==='pssr'">重新生成PSSR检查内容</span>
<span v-if="props.type==='evaluation'">重新生成验收评价</span>
<span v-if="props.type==='close'">重新生成变更关闭确认表</span>
</NButton>
<NButton v-if="props.isAiOpen" ghost :type="confirmed ? 'success' : 'primary'" :disabled="btnDisabled" @click="confirmTab">
<Icon icon="ix:success" class="size-16px mr-1" /> {{ confirmed ? '已确认完毕' : '本标签内容确认完毕' }}
</NButton>
</div>
</div>
<div class="flex flex-wrap items-center gap-x-4 gap-y-2 bg-[#EAF0F9] px-4 py-2.5 shadow-[0_-2px_8px_rgba(0,0,0,0.05)]">
<div class="flex min-w-0 flex-1 flex-wrap items-center gap-x-2 gap-y-1 text-xs text-slate-500">
<Icon icon="lucide:users" class="size-14px" />
<span class="min-w-0 truncate">
部门审核:{{ approvalChain }}
</span>
<NButton v-if="getStepsInfo && getStepsInfo.length" ghost size="small" type="primary" :disabled="btnDisabled" @click="openApprovalDrawer">
<Icon icon="octicon:sliders-24" class="size-14px" />
审批流程设置
</NButton>
</div>
<div class="ml-auto flex shrink-0 items-center gap-2">
<NButton size="small" type="primary" :disabled="btnDisabled" :loading="saveLoading" @click="saveDraft">保存</NButton>
<NButton size="small" type="primary" @click="submitApply" :disabled="btnDisabled" :loading="submitLoading">
提交 <Icon icon="formkit:right" class="size-14px" />
</NButton>
</div>
</div>
</div>
<!-- 审批流程设置 -->
<NDrawer v-model:show="approvalDrawerShow" placement="right" width="28rem">
<NDrawerContent>
<template #header>
<div class="text-base font-semibold text-slate-800">审批流程设置</div>
</template>
<div class="h-full space-y-4 overflow-y-auto">
<p class="text-xs leading-relaxed text-slate-400">审批链:{{ approvalFlowText }}。专业会签人由申请表「涉及专业」勾选自动增减,可在此调整人选;重要变更自动追加安全审查与分管领导终批。</p>
<div class="rounded-lg border p-3" v-for="(item,index) in flowList" :key="item.id">
<div class="mb-2 flex items-center gap-2 text-sm font-semibold text-slate-700">
<NTag type="info" size="small">{{Number(index)+1}}</NTag> {{item.name}} {{item.countersign ? '(专业会签)' : ''}}
</div>
<NSelect :value="approvers[item.id]" multiple :options="item.members" label-field="user_name" value-field="user_id"
@update:value="(v) => updateApprover(item.id, v)" />
</div>
<NButton type="primary" class="w-full" @click="finishApprover">完成设置</NButton>
</div>
</NDrawerContent>
</NDrawer>
</template>