发起变更审批流程弹框板块修改

This commit is contained in:
2026-10-09 15:12:01 +08:00
parent 5b981d4fa6
commit a5282019c0
8 changed files with 296 additions and 152 deletions
-18
View File
@@ -1,18 +0,0 @@
import { computed, ref } from 'vue';
import { defineStore } from 'pinia';
import { SetupStoreId } from '@/enum';
export const useChangeStore = defineStore(SetupStoreId.Change, () => {
const stepsInfo = ref<Api.Change.StepInfo[]>([]);
// 获取选中模板信息
const getStepsInfo = computed(() => stepsInfo.value);
// 设置选中模板信息
function setStepsInfo(data: Api.Change.StepInfo[]) {
stepsInfo.value = data;
}
return {
getStepsInfo,
setStepsInfo,
}
});
+45 -15
View File
@@ -5,8 +5,6 @@ import { useThemeStore } from '@/store/modules/theme';
import { useRoute } from 'vue-router';
import { getColorWithOpacity } from "@/utils/common";
import { localStg } from '@/utils/storage';
import { storeToRefs } from 'pinia';
import { useChangeStore } from '@/store/modules/change';
import { queryUnsavedApi, saveChangeApi, submitChangeApi, getChangeDetailApi } from '@/service/api/change';
import { systemConfigApi, templateListApi, peopleListApi } from '@/service/api/system';
import { orgListApi } from '@/service/api/user';
@@ -19,8 +17,6 @@ import Pssr from "./modules/pssr.vue";
import Evaluation from "./modules/evaluation.vue";
import CloseConfirm from "./modules/closeConfirm.vue";
const changeStore = useChangeStore();
const { getStepsInfo } = storeToRefs(changeStore);
const themeStore = useThemeStore();
const route = useRoute();
@@ -37,6 +33,7 @@ const evaluationRef = ref<any>(null);
const closeConfirmRef = ref<any>(null);
// 所有人员
const memberList = ref<any>([]);
const stepsList = ref<any>([]);
// 表单数据
const changePreForm = ref<any>(null)
const changeApplyForm = ref<any>(null)
@@ -150,12 +147,12 @@ const getAllFormData = () => {
let pssrForm = pssrRef.value?.list;
let allItems = pssrForm.flatMap((group:any) => group.items ?? []);
let evaluationForm = evaluationRef.value?.list;
let flowList = getStepsInfo.value.map((item:any) => ({
let flowList = stepsList.value.map((item:any) => ({
members: item.members.map((m:any) => ({
user_id: m.user_id,
org_id: m.org_id,
org_name: m.org_name,
specialty: m.specialty === '' ? null : (m.specialty ?? null),
specialty: m.specialty ?? null,
is_sign: m.is_sign
}))
}));
@@ -259,7 +256,9 @@ const getChangeDetail = async () => {
// 解析时间戳
changeApplyForm.value.plan_use_date = anyToTimestamp(changeApplyForm.value.plan_use_date);
changeApplyForm.value.restore_deadline = anyToTimestamp(changeApplyForm.value.restore_deadline);
// 审批信息
stepsList.value = changeApplyForm.value?.approval_flow?.steps || [];
riskAnalysisForm.value = data.risk_records;
changeTrainingForm.value = data.train;
pssrForm.value = data.pssr;
@@ -325,18 +324,42 @@ const getOrgList = async () => {
}
}
// 人员数据转换
const groupUsersByOrg = (users:any) => {
const orgMap = new Map(); // key: org.id, value: { id, label, children }
users.forEach((user:any) => {
const { id: userId, realname, user_departments } = user;
if (!user_departments || user_departments.length === 0) return;
user_departments.forEach((dept:any) => {
const org = dept.org;
if (!org || !org.id) return;
if (!orgMap.has(org.id)) {
orgMap.set(org.id, {
value: org.id,
label: org.label,
children: []
});
}
const targetOrg = orgMap.get(org.id);
// 避免同一个用户重复加入同一个组织(理论上不会发生,但做去重保护)
if (!targetOrg.children.some((child:any) => child.id === userId)) {
targetOrg.children.push({
value: userId.toString(),
label: realname,
user_id: userId.toString()
});
}
});
});
return Array.from(orgMap.values());
}
// 获取所有人员
const getAllMembers = async () => {
const {data,error} = await peopleListApi();
if(!error){
const transformed = data.map((item:any) => ({
user_id: item.id,
user_name: item.realname,
full_name: item.realname + (`(${item.user_departments.map((d:any) => d.label).join(';')})`),
org_id: item.user_departments.map((d:any) => d.organization_id).join(';'),
org_name: item.user_departments.map((d:any) => d.label).join(';')
}));
memberList.value = transformed;
memberList.value = groupUsersByOrg(data);
}
}
@@ -415,10 +438,12 @@ onMounted(() => {
:currentForm="changeApplyForm"
:confirmed="confirmed[tab]"
:members="memberList"
:steps="stepsList"
@save="saveDraft"
@submit="submitConfirm"
@confirmTab="confirmTab"
@update:title="(v: string) => title = v"
@update:steps="(v:any) => stepsList = v"
/>
</div>
<!-- ===== 风险分析记录表 ===== -->
@@ -434,6 +459,7 @@ onMounted(() => {
:currentForm="riskAnalysisForm"
:confirmed="confirmed[tab]"
:members="memberList"
:steps="stepsList"
@save="saveDraft"
@submit="submitConfirm"
@confirmTab="confirmTab"
@@ -451,6 +477,7 @@ onMounted(() => {
:currentForm="changeTrainingForm"
:confirmed="confirmed[tab]"
:members="memberList"
:steps="stepsList"
@save="saveDraft"
@submit="submitConfirm"
@confirmTab="confirmTab"
@@ -469,6 +496,7 @@ onMounted(() => {
:currentForm="pssrForm"
:confirmed="confirmed[tab]"
:members="memberList"
:steps="stepsList"
@save="saveDraft"
@submit="submitConfirm"
@confirmTab="confirmTab"
@@ -487,6 +515,7 @@ onMounted(() => {
:currentForm="evaluationForm"
:confirmed="confirmed[tab]"
:members="memberList"
:steps="stepsList"
@save="saveDraft"
@submit="submitConfirm"
@confirmTab="confirmTab"
@@ -501,6 +530,7 @@ onMounted(() => {
:title="title"
:currentId="currentId"
:members="memberList"
:steps="stepsList"
@save="saveDraft"
@submit="submitConfirm"
@confirmTab="confirmTab"
@@ -1,42 +1,24 @@
<!-- 变更申请表 -->
<script setup lang="ts">
import { ref, computed, h, watch } from 'vue';
import { ref, computed, h, watch, nextTick } from 'vue';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { tagTextColor, tagBgColor, tagBorderColor, downloadFileFun } from '@/utils/common';
import { localStg } from '@/utils/storage';
import { useChangeStore } from '@/store/modules/change';
import { AiError, aiTypeJudge, aiLevelScore, aiApplication } from "@/service/api/ai";
import { uploadFileApi, deleteFileApi } from "@/service/api/file";
import Footer from './footer.vue'
import previewComponent from '@/components/previewFile/index.vue'
const themeStore = useThemeStore();
const changeStore = useChangeStore();
const props = defineProps(['type','orgTree','templateList','typeList','levelList','isAiOpen','title','currentId','formInfo', 'currentForm', 'confirmed','members'])
const emit = defineEmits(['save','submit','confirmTab','update:title']);
const props = defineProps([
'type','orgTree','templateList','typeList','levelList','isAiOpen',
'title','currentId','formInfo', 'currentForm', 'confirmed','members','steps'
])
const emit = defineEmits(['save','submit','confirmTab','update:title','update:steps']);
watch(() => props.currentForm, (newVal:any) => {
// 过滤出需要的字段
const {
approval_flow,
attachments,
change_id,
created_at,
id,
updated_at,
...rest
} = newVal;
form.value = rest;
// 用 Map 建立 question_id -> score 的映射,查找 O(1)
const scoreMap = new Map(form.value?.level_score?.map((item:any) => [item.question_id, item.score]) || []);
questions.value = questions.value.map((item:any) => ({
...item,
score: scoreMap.has(item.question_id) ? scoreMap.get(item.question_id) : item.score
}));
totalScore.value = questions.value.reduce((sum:number, item:any) => sum + (item.score || 0), 0);
})
const isSyncingFromParent = ref(false);
const aiFail = (e: any) => {
if (e instanceof AiError) window.$message?.warning(`AI 分析失败:${e.message},可手动填写`);
@@ -58,11 +40,10 @@ const previewFile = ref<any>(null);
const sheet = ref<string>('');
// 上传文件
const uploadFile = ref<{id:number, file_name:string, file_url:string, uploader_name:string, file_type:string, created_at:string, version:string | null}[]>([]);
// 表单
const form = ref<{
change_type: number | null,
change_level: any,
change_level: number | null,
duration_type: number | null,
restore_deadline: number | null,
org_id: number | null,
@@ -79,7 +60,7 @@ const form = ref<{
level_score: any[],
}>({
change_type: null,
change_level: computed(() => totalScore.value >= 20 ? 2 : 1),
change_level: null,
duration_type: null,
restore_deadline: null,
org_id: null,
@@ -185,10 +166,59 @@ const questions = ref<any>([
},
]);
// 当前模板步骤
const currentSteps = ref<any>(null);
watch(() => props.currentForm, (newVal:any) => {
if (!newVal) return;
isSyncingFromParent.value = true;
// 过滤出需要的字段
const {
approval_flow,
attachments,
change_id,
created_at,
id,
updated_at,
...rest
} = newVal;
form.value = rest;
// 用 Map 建立 question_id -> score 的映射,查找 O(1)
const scoreMap = new Map(form.value?.level_score?.map((item:any) => [item.question_id, item.score]) || []);
questions.value = questions.value.map((item:any) => ({
...item,
score: scoreMap.has(item.question_id) ? scoreMap.get(item.question_id) : item.score
}));
totalScore.value = questions.value.reduce((sum:number, item:any) => sum + (item.score || 0), 0);
nextTick(() => { // ★ form 的 watch 跑完后再释放
isSyncingFromParent.value = false;
});
},{ immediate: true })
// 监听总分数变化,自动更新等级
watch(() => totalScore.value, (v:number) => {
form.value.change_level = v >= 20 ? 2 : 1;
});
// 变更类型 / 变更等级 / 变更时限,任一改变 → 更新模板
watch(
() => [
form.value?.change_type,
form.value?.change_level,
form.value?.duration_type,
],
() => {
if (isSyncingFromParent.value) return;
updateCurrentTemplate()
}
);
// 监听步骤变化,更新本地步骤
watch(() => props.steps, (v:any) => {
currentSteps.value = v;
},{ immediate: true })
// 筛选涉及专业的人员和部门
const filterMembersBySpecialties = (data: any, specialties: string[]) => {
const filterMembersBySpecialties = (steps: any, specialties: string[]) => {
const result: any[] = [];
data.steps.forEach((step: any) => {
steps.forEach((step: any) => {
step.members.forEach((member: any) => {
if (member.specialty && specialties.includes(member.specialty)) {
result.push({
@@ -200,27 +230,20 @@ const filterMembersBySpecialties = (data: any, specialties: string[]) => {
});
return result;
}
// 当前模板
const currentTemplate = computed(() => {
let template = null;
if(props.currentForm?.approval_flow?.steps && props.currentForm?.approval_flow?.steps.length){
template = props.currentForm?.approval_flow;
}else{
template = props.templateList.find((item: any) =>
item.level === form.value?.change_level &&
item.category === form.value?.change_type &&
item.duration === form.value?.duration_type
) || null;
}
footerRef.value?.clearApprovers();
changeStore.setStepsInfo(template?template.steps:[]);
return template?template:null;
});
// 更新当前模板步骤
const updateCurrentTemplate = () => {
let template = props.templateList.find((item: any) =>
item.level === form.value?.change_level &&
item.category === form.value?.change_type &&
item.duration === form.value?.duration_type
) || null;
currentSteps.value = template?.steps || [];
emit('update:steps', currentSteps.value);
}
// 联动专业会签人员
const signers = computed(() => {
if(currentTemplate.value){
const matchedMembers = filterMembersBySpecialties(currentTemplate.value, form.value.disciplines);
if(currentSteps.value){
const matchedMembers = filterMembersBySpecialties(currentSteps.value || [], form.value.disciplines);
return matchedMembers
}
return null
@@ -414,6 +437,11 @@ const confirmTab = (tab:string, val:boolean) => {
emit('confirmTab', tab, val);
}
// 更新审批流程
const updateSteps = (steps:any[]) => {
emit('update:steps', steps);
}
defineExpose({form, questions, applyAiResult, generateLevel: useAiLevel})
</script>
@@ -678,10 +706,12 @@ defineExpose({form, questions, applyAiResult, generateLevel: useAiLevel})
:currentForm="form"
:confirmed="props.confirmed"
:members="props.members"
:steps="currentSteps"
@save="saveDraft"
@submit="submitConfirm"
@confirmTab="confirmTab"
@regenerate="onRegenerate"
@update:steps="updateSteps"
/>
<!-- 侧边抽屉:AI 类型判定 / 等级判定表 -->
<NDrawer :show="sheet !== ''" @update:show="(v) => !v && (sheet = '')" placement="right" :width="sheet === 'level' ? '45rem' : '28rem'">
@@ -8,8 +8,8 @@ import Footer from './footer.vue';
const themeStore = useThemeStore();
const props = defineProps(['type','isAiOpen','title','currentId','formInfo', 'currentForm', 'confirmed','members'])
const emit = defineEmits(['save','submit','confirmTab']);
const props = defineProps(['type','isAiOpen','title','currentId','formInfo', 'currentForm', 'confirmed','members','steps'])
const emit = defineEmits(['save','submit','confirmTab','update:steps']);
watch(() => props.currentForm, (newVal:any) => {
// 过滤出需要的字段
@@ -67,10 +67,14 @@ const saveDraft = () => {
const submitConfirm = () => {
emit('submit', props.type);
}
// // 确认标签内容
// 确认标签内容
const confirmTab = (tab:string, val:boolean) => {
emit('confirmTab', tab, val);
}
// 更新审批流程
const updateSteps = (steps:any[]) => {
emit('update:steps', steps);
}
defineExpose({form, onRegenerate})
</script>
@@ -107,10 +111,12 @@ defineExpose({form, onRegenerate})
:isAiOpen="props.isAiOpen"
:confirmed="props.confirmed"
:members="props.members"
:steps="props.steps"
@save="saveDraft"
@submit="submitConfirm"
@confirmTab="confirmTab"
@regenerate="onRegenerate"
@update:steps="updateSteps"
/>
</template>
<style scoped lang="scss">
@@ -8,8 +8,8 @@ import { AiError, aiAcceptance } from "@/service/api/ai";
import Footer from './footer.vue';
const themeStore = useThemeStore();
const props = defineProps(['type','orgTree','isAiOpen','title','currentId','formInfo','currentForm','confirmed','members'])
const emit = defineEmits(['save','submit','confirmTab']);
const props = defineProps(['type','orgTree','isAiOpen','title','currentId','formInfo','currentForm','confirmed','members','steps'])
const emit = defineEmits(['save','submit','confirmTab','update:steps']);
watch(() => props.currentForm, (newVal:any) => {
const omit = (obj:any, fields: string[]) => Object.fromEntries(Object.entries(obj).filter(([k]) => !fields.includes(k)));
@@ -109,10 +109,14 @@ const saveDraft = () => {
const submitConfirm = () => {
emit('submit', props.type);
}
// // 确认标签内容
// 确认标签内容
const confirmTab = (tab:string, val:boolean) => {
emit('confirmTab', tab, val);
}
// 更新审批流程
const updateSteps = (steps:any[]) => {
emit('update:steps', steps);
}
const findNodeById = (tree:any,id:number) => {
for (const node of tree) {
@@ -294,10 +298,12 @@ defineExpose({list, onRegenerate})
:isAiOpen="props.isAiOpen"
:confirmed="props.confirmed"
:members="props.members"
:steps="props.steps"
@save="saveDraft"
@submit="submitConfirm"
@confirmTab="confirmTab"
@regenerate="onRegenerate"
@update:steps="updateSteps"
/>
</template>
<style scoped lang="scss">
+129 -52
View File
@@ -1,14 +1,9 @@
<!-- 通用底部操作 -->
<script setup lang="ts">
import { computed, inject, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { Icon } from '@iconify/vue'
import { useChangeStore } from '@/store/modules/change';
import { useAiAnalysisApi } from '@/service/api/change';
const changeStore = useChangeStore();
const { getStepsInfo } = storeToRefs(changeStore);
const props = defineProps([
'type',
'currentId',
@@ -17,8 +12,9 @@ const props = defineProps([
'currentForm',
'confirmed',
'members',
'steps'
])
const emit = defineEmits(['save','submit','confirmTab','regenerate']);
const emit = defineEmits(['save','submit','confirmTab','regenerate','update:steps']);
watch(() => props.confirmed, (newVal:boolean) => {
confirmed.value = newVal;
@@ -33,6 +29,16 @@ const regenerateLoading = ref<boolean>(false);
const submitLoading = ref<boolean>(false);
const confirmed = ref<boolean>(false);
// 专业
const SPECIALTIES = [
{ label: '生产', value: '生产' },
{ label: '技术', value: '技术' },
{ label: '仪表', value: '仪表' },
{ label: '电气', value: '电气' },
{ label: '设备', value: '设备' },
{ label: '安全', value: '安全' },
{ label: '质量', value: '质量' }
];
// 是否重新生成
const regenerateTypes = ref<Record<string, boolean>>({
'form':false,
@@ -43,23 +49,21 @@ const regenerateTypes = ref<Record<string, boolean>>({
'close':false,
});
// 存储每个步骤选中的审核人 user_id
const approvers = ref<Record<number, number[]>>({})
const stepList = ref<any[]>([]);
const flowList = ref<any[]>([]);
const currentSteps = ref<any>(null);
// 监听步骤变化,更新本地步骤
watch(() => props.steps, (v:any) => {
currentSteps.value = v;
},{ immediate: true })
// 审批链
const approvalFlowText = computed(() => {
if (!stepList.value || stepList.value.length === 0) return '暂无步骤'
if (!currentSteps.value || currentSteps.value.length === 0) return '暂无步骤'
// 按 step_order 排序
const sorted = [...stepList.value].sort((a, b) => a.step_order - b.step_order)
const sorted = [...currentSteps.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'){
@@ -67,13 +71,6 @@ const openApprovalDrawer = () => {
return window.$message?.warning('请先选择变更类型、变更等级、变更时限');
}
}
stepList.value = getStepsInfo.value;
if(Object.keys(approvers.value).length === 0){
const result = Object.fromEntries(
stepList.value.map((step: any) => [step.id, step.members.map((m: any) => m.user_id)])
);
approvers.value = result;
}
approvalDrawerShow.value = true;
}
@@ -93,7 +90,7 @@ const transformFlowInfo = (steps: any[]) => {
// 审批链
const approvalChain = computed(() => {
let template = getStepsInfo.value;
let template = props.steps;
if (!template || template.length === 0) return '暂无审批步骤'
flowList.value = transformFlowInfo(template);
const parts = flowList.value.map((step: any) => {
@@ -158,32 +155,71 @@ 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 = props.members.filter((m: any) => usersId.includes(m.user_id));
step.members = members || []; // 更新 step 的 members 属性
result.push(step);
}
}
return result;
}
// 完成设置
const finishApprover = async () => {
const matched = matchApprovers(stepList.value, approvers.value);
changeStore.setStepsInfo(matched);
emit('update:steps', currentSteps.value);
window.$message?.success("审批流程设置完成");
approvalDrawerShow.value = false;
}
defineExpose({clearApprovers, setRegenerateLoading})
/**
* 在组织树中根据 user_id 查找用户信息
* @param {Array} tree - 组织树数据
* @param {string|number} targetUserId - 要查找的 user_id
* @returns {Object|null} - 返回 { userLabel, parentLabel, parentValue } 或 null
*/
function findUserInTree(tree: any[], targetUserId: string | number) {
for (const org of tree) {
const children = org.children || [];
for (const user of children) {
if (String(user.user_id) === String(targetUserId)) {
return {
user_name: user.label,
org_name: org.label,
org_id: org.value
};
}
}
}
return null; // 未找到
}
// 设置专业会签步骤
const setStepCountersign = (target: any, v: boolean) => {
currentSteps.value.forEach((s:any) => {
s.countersign = s === target ? v : false;
if (!s.countersign) s.members.forEach((m:any) => { m.specialty = ""; m.is_sign = 0; });
});
}
// 选择专业
const selectSpecialty = (idx: number, mIdx: number, v: string | null) => {
currentSteps.value[idx].members[mIdx].specialty = v ?? null;
}
// 选择是否必须参与会签
const selectSign = (idx: number, mIdx: number, v: number) => {
currentSteps.value[idx].members[mIdx].is_sign = v;
}
// 当前打开人员选择器的步骤(步骤行「+」按钮切换)
const pickerStepId = ref<number | null>(null);
const pick = ref<string | null>(null);
const togglePicker = (s: any) => {
pickerStepId.value = pickerStepId.value === s.unique_id ? null : s.unique_id;
pick.value = null;
}
// 添加审批步骤人员
const addMember = (s: any, v: string | null) => {
if (v && !s.members.some((m:any) => m.user_name === v)){
let info = findUserInTree(props.members, v);
s.members.push({ user_id: Number(v), user_name: info?.user_name || '', org_id: info?.org_id || '', org_name: info?.org_name || '', specialty: "", is_sign: 0 });
pick.value = null;
pickerStepId.value = null;
}
}
// 删除审批步骤人员
const delMember = (s: any, p: number) => {
s.members = s.members.filter((m:any) => m.user_id !== p);
}
defineExpose({setRegenerateLoading})
</script>
@@ -225,7 +261,7 @@ defineExpose({clearApprovers, setRegenerateLoading})
<span class="min-w-0 truncate">
审核:{{ approvalChain }}
</span>
<NButton v-if="getStepsInfo && getStepsInfo.length" ghost size="small" type="primary" :disabled="btnDisabled" @click="openApprovalDrawer">
<NButton v-if="currentSteps && currentSteps.length" ghost size="small" type="primary" :disabled="btnDisabled" @click="openApprovalDrawer">
<Icon icon="octicon:sliders-24" class="size-14px" />
审批流程设置
</NButton>
@@ -239,22 +275,63 @@ defineExpose({clearApprovers, setRegenerateLoading})
</div>
</div>
<!-- 审批流程设置 -->
<NDrawer v-model:show="approvalDrawerShow" placement="right" width="28rem">
<NDrawer v-model:show="approvalDrawerShow" placement="right" width="37rem">
<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 stepList" :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 v-for="(s, idx) in currentSteps" :key="s.id" class="rounded-lg border border-slate-100 bg-slate-50/40 p-3">
<div class="flex flex-wrap items-center gap-2">
<NTag type="info" size="small">{{Number(idx)+1}}</NTag>
<p class="text-sm">{{s.name}}</p>
<NCheckbox :checked="s.countersign" size="small" @update:checked="(v) => setStepCountersign(s, v)">
<span class="text-xs text-slate-500">专业会签步骤</span>
</NCheckbox>
<NButton ghost type="primary" size="tiny" @click="togglePicker(s)" title="添加预设人员" class="ml-auto">
<Icon icon="ic:round-plus" class="size-16px" />
</NButton>
</div>
<!-- 预设人员:人员必选;仅会签步骤可选择专业(必选),且人员可勾选「必须进行专业会签」 -->
<div class="mt-2 flex gap-2 pl-8">
<span class="shrink-0 text-xs leading-7 text-slate-400">预设人员:</span>
<div class="min-w-0 flex-1 space-y-1.5">
<div v-for="(m, mIdx) in s.members" :key="m.id" class="flex min-h-7 flex-wrap items-center gap-2">
<span class="shrink-0 truncate text-xs font-medium text-slate-700">{{ m.user_name }}</span>
<template v-if="s.countersign">
<span class="text-xs text-slate-400"><span class="text-red-500">*</span>专业:</span>
<NSelect :value="m.specialty || null" clearable size="small" placeholder="请选择专业" class="w-28"
:options="SPECIALTIES"
@update:value="(v) => selectSpecialty(Number(idx), Number(mIdx), v)"
/>
<NCheckbox :checked="m.is_sign" size="small"
:checked-value="1"
:unchecked-value="0"
@update:checked="(v) => selectSign(Number(idx), Number(mIdx), v)"
>
<span class="text-xs text-slate-500">必须参与会签</span>
</NCheckbox>
</template>
<NButton text type="error" @click="delMember(s, m.user_id)" title="删除该人员">
<Icon icon="iconamoon:trash" class="size-16px" />
</NButton>
</div>
<p v-if="s.countersign && s.members.length" class="text-[11px] leading-5 text-slate-400">
必须参与会签:勾选的人员必须参与专业会签,未勾选的人员为可选会签(按需参加)
</p>
<NCascader v-if="pickerStepId === s.unique_id" :value="pick" :options="props.members"
placeholder="请选择人员" size="small" check-strategy="child" :show-path="false" expand-trigger="click"
class="w-56" :on-update:value="(v) => addMember(s, v)" />
<span v-if="s.members.length === 0" class="text-xs leading-7 text-slate-300">未预设</span>
</div>
</div>
<NSelect :value="approvers[item.id]" multiple :options="props.members" label-field="full_name" value-field="user_id"
@update:value="(v) => updateApprover(item.id, v)" />
</div>
<NButton v-if="stepList.length" type="primary" class="w-full" @click="finishApprover">完成设置</NButton>
</div>
<template #footer>
<NButton v-if="currentSteps.length" type="primary" class="w-full" @click="finishApprover">完成设置</NButton>
</template>
</NDrawerContent>
</NDrawer>
</template>
+8 -2
View File
@@ -8,8 +8,8 @@ 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','currentForm','confirmed','members'])
const emit = defineEmits(['save','submit','confirmTab']);
const props = defineProps(['type','isAiOpen','title','currentId','formInfo','changeType','currentForm','confirmed','members','steps'])
const emit = defineEmits(['save','submit','confirmTab','update:steps']);
watch(() => props.currentForm, (newVal:any) => {
// 1. 按 group_type 分组
@@ -141,6 +141,10 @@ const submitConfirm = () => {
const confirmTab = (tab:string, val:boolean) => {
emit('confirmTab', tab, val);
}
// 更新审批流程
const updateSteps = (steps:any[]) => {
emit('update:steps', steps);
}
defineExpose({list, onRegenerate})
</script>
@@ -245,10 +249,12 @@ defineExpose({list, onRegenerate})
:isAiOpen="props.isAiOpen"
:confirmed="props.confirmed"
:members="props.members"
:steps="props.steps"
@save="saveDraft"
@submit="submitConfirm"
@confirmTab="confirmTab"
@regenerate="onRegenerate"
@update:steps="updateSteps"
/>
</template>
<style scoped lang="scss">
@@ -13,16 +13,8 @@ import { matrixConfigApi } from "@/service/api/system";
const themeStore = useThemeStore();
const props = defineProps(['type','isAiOpen','title','currentId','formInfo', 'currentForm', 'confirmed','members'])
const emit = defineEmits(['save','submit','confirmTab']);
watch(() => props.currentForm, (newVal:any) => {
const omit = (obj:any, fields: string[]) => Object.fromEntries(Object.entries(obj).filter(([k]) => !fields.includes(k)));
const result = newVal?.records.map((item:any) => omit(item, [
'created_at', 'updated_at', 'change_id', 'source_row_id'
]));
riskRecords.value = result || [];
})
const props = defineProps(['type','isAiOpen','title','currentId','formInfo', 'currentForm', 'confirmed','members','steps']);
const emit = defineEmits(['save','submit','confirmTab','update:steps']);
const hazopRiskMatrixConfig = ref<any>(null);
const fmeaRiskMatrixConfig = ref<any>(null);
@@ -39,6 +31,14 @@ const riskRecords = ref<any>([]);
const pluginDrawer = ref<boolean>(false);
const currentPlugin = ref<string>('');
watch(() => props.currentForm, (newVal:any) => {
const omit = (obj:any, fields: string[]) => Object.fromEntries(Object.entries(obj).filter(([k]) => !fields.includes(k)));
const result = newVal?.records.map((item:any) => omit(item, [
'created_at', 'updated_at', 'change_id', 'source_row_id'
]));
riskRecords.value = result || [];
}, { immediate: true })
// 风险识别分析记录表
const TOOL_CARDS = [
{ key: "RISK", name: "风险预分析" },
@@ -156,6 +156,11 @@ const confirmTab = (tab:string, val:boolean) => {
emit('confirmTab', tab, val);
}
// 更新审批流程
const updateSteps = (steps:any[]) => {
emit('update:steps', steps);
}
// 获取JSA风险矩阵配置
const getRiskMatrixConfig = async () => {
const {data,error} = await matrixConfigApi();
@@ -455,9 +460,11 @@ onMounted(() => {
:currentForm="formInfo"
:confirmed="props.confirmed"
:members="props.members"
:steps="props.steps"
@save="saveDraft"
@submit="submitConfirm"
@confirmTab="confirmTab"
@update:steps="updateSteps"
/>
<!-- 插件抽屉 -->
<NDrawer v-model:show="pluginDrawer" width="100%" placement="right">