资料关闭添加审批流程

This commit is contained in:
2026-10-09 17:01:46 +08:00
parent 7e0fb9e516
commit 98625c5c94
+196 -2
View File
@@ -1,12 +1,12 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ref, onMounted, computed } from 'vue';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { useRouterPush } from '@/hooks/common/router';
import { useRoute } from 'vue-router';
import { formatTimestamp, downloadFileFun } from '@/utils/common';
import { uploadFileApi, deleteFileApi, fileListApi } from "@/service/api/file";
import { systemConfigApi } from '@/service/api/system';
import { systemConfigApi, peopleListApi } from '@/service/api/system';
import { getChangeDetailApi } from '@/service/api/change';
import { myTasksCloseMaterialApi } from '@/service/api/myTasks';
import previewComponent from '@/components/previewFile/index.vue'
@@ -53,6 +53,28 @@ const previewFile = ref<any>(null);
// 预览文件弹框
const previewModal = ref<boolean>(false);
const memberList = ref<any[]>([]);
// 审批流程抽屉
const approvalDrawer = ref<boolean>(false);
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 SPECIALTIES = [
{ label: '生产', value: '生产' },
{ label: '技术', value: '技术' },
{ label: '仪表', value: '仪表' },
{ label: '电气', value: '电气' },
{ label: '设备', value: '设备' },
{ label: '安全', value: '安全' },
{ label: '质量', value: '质量' }
];
const list = ref<any>([
{id:1,status:null,name:'变更申请表及审批记录',files:[]},
{id:2,status:null,name:'风险分析记录(风险预分析 / HAZOP / FMEA / 风险检查表)',files:[]},
@@ -190,6 +212,10 @@ const submitCloseDoc = async (type:string) => {
return window.$message?.error('请选择验收结论');
}
const result = list.value.map(({ id, status }:{id:number,status:number} ) => ({ id, status }));
// 去掉id, step_order, user_name, status
const steps = flowList.value.map(({ members }:any) => ({
members: members.map(({ id, step_order, user_name, status, ...rest }:any) => rest)
}));
loading.value = true;
const {error} = await myTasksCloseMaterialApi(currentId.value,{
type:type,
@@ -199,6 +225,9 @@ const submitCloseDoc = async (type:string) => {
accept_date:formatTimestamp(acceptDate.value,'yyyy-MM-dd'),
cost:price.value,
close_docs:result,
approval_flow: {
steps: steps
}
});
if(!error){
if(type==='save'){
@@ -218,6 +247,8 @@ const getDetail = async () => {
const {data,error} = await getChangeDetailApi(currentId.value)
if(!error){
currentInfo.value = data;
// ******审批流程********
flowList.value = data?.approval_flow?.steps || [];
}
loading.value = false;
};
@@ -245,8 +276,110 @@ const getFileList = async () => {
fileList.value = data;
}
}
// 人员数据转换
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){
memberList.value = groupUsersByOrg(data);
}
}
/**
* 在组织树中根据 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) => {
flowList.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) => {
flowList.value[idx].members[mIdx].specialty = v ?? null;
}
// 选择是否必须参与会签
const selectSign = (idx: number, mIdx: number, v: number) => {
flowList.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.id ? null : s.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(memberList.value, 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);
}
// 完成设置
const finishApprover = async () => {
window.$message?.success("审批流程设置完成");
approvalDrawer.value = false;
}
onMounted(() => {
getAllMembers();
useDate.value = new Date().getTime();
if(route.query){
if(route.query.key){
@@ -553,11 +686,72 @@ onMounted(() => {
<div class="flex items-center justify-between border-t pt-3 mt-3">
<p class="text-xs text-slate-400">待归档 5 项:培训签到与考核记录、PSSR 检查记录(含现场照片)、验收报告与运行数据、更新后的 P&ID / 操作规程(受控最新版)、设备 / 联锁台账更新记录</p>
<div class="flex gap-2">
<NButton size="small" @click="approvalDrawer = true">审批流程设置</NButton>
<NButton ghost size="small" @click="back">取消</NButton>
<NButton ghost type="primary" size="small" @click="submitCloseDoc('save')">保存</NButton>
<NButton type="primary" size="small" @click="submitCloseDoc('close')">申请关闭</NButton>
</div>
</div>
<!-- 审批流程设置 -->
<NDrawer v-model:show="approvalDrawer" 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 v-for="(s, idx) in flowList" :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.id" :value="pick" :options="memberList"
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>
</div>
</div>
<template #footer>
<NButton v-if="flowList.length" type="primary" class="w-full" @click="finishApprover">完成设置</NButton>
</template>
</NDrawerContent>
</NDrawer>
<!-- 附件选择弹框 -->
<NModal
v-model:show="filesModal"