Files
moc/src/views/my/initiateChange/modules/footer.vue
T

337 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 通用底部操作 -->
<script setup lang="ts">
import { computed, inject, ref, watch } from 'vue';
import { Icon } from '@iconify/vue'
import { useAiAnalysisApi } from '@/service/api/change';
const props = defineProps([
'type',
'currentId',
'title',
'isAiOpen',
'currentForm',
'confirmed',
'members',
'steps'
])
const emit = defineEmits(['save','submit','confirmTab','regenerate','update:steps']);
watch(() => props.confirmed, (newVal:boolean) => {
confirmed.value = newVal;
})
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);
// 专业
const SPECIALTIES = [
{ label: '生产', value: '生产' },
{ label: '技术', value: '技术' },
{ label: '仪表', value: '仪表' },
{ label: '电气', value: '电气' },
{ label: '设备', value: '设备' },
{ label: '安全', value: '安全' },
{ label: '质量', value: '质量' }
];
// 是否重新生成
const regenerateTypes = ref<Record<string, boolean>>({
'form':false,
'risk':false,
'train':false,
'pssr':false,
'evaluation':false,
'close':false,
});
const flowList = ref<any[]>([]);
const currentSteps = ref<any>(null);
// 监听步骤变化,更新本地步骤
watch(() => props.steps, (v:any) => {
currentSteps.value = v;
},{ immediate: true })
// 审批链
const approvalFlowText = computed(() => {
if (!currentSteps.value || currentSteps.value.length === 0) return '暂无步骤'
// 按 step_order 排序
const sorted = [...currentSteps.value].sort((a, b) => a.step_order - b.step_order)
return sorted.map(item => item.name).join(' → ')
})
// 打开审批流程抽屉
const openApprovalDrawer = () => {
if(props.type==='form'){
if(!props.currentForm?.change_type || !props.currentForm?.change_level || !props.currentForm?.duration_type){
return window.$message?.warning('请先选择变更类型、变更等级、变更时限');
}
}
approvalDrawerShow.value = true;
}
// 数据转换
const transformFlowInfo = (steps: any[]) => {
const sortedSteps = [...steps || []].sort((a, b) => a.step_order - b.step_order)
const result = sortedSteps.map(step => {
return {
stepId: step.id,
name: step.name,
countersign: step.countersign,
selectedMember: step.members,
};
});
return result;
}
// 审批链
const approvalChain = computed(() => {
let template = props.steps;
if (!template || template.length === 0) return '暂无审批步骤'
flowList.value = transformFlowInfo(template);
const parts = flowList.value.map((step: any) => {
// 根据 countersign 决定显示名称
const displayName = 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 () => {
window.$message?.info('差接口');
}
// 重新生成:AI 调用逻辑由各标签页组件实现(按 type 分发),此处仅派发事件
const regenerate = async () => {
regenerateTypes.value[props.type] = true;
emit('regenerate', props.type);
}
// 供父组件控制「重新生成」按钮 loading
const setRegenerateLoading = (v: boolean) => {
regenerateLoading.value = v;
}
// 确认标签内容
const confirmTab = async () => {
let stepId = 0;
if(props.type==='form'){
stepId = 1;
}
if(props.type==='risk'){
stepId = 2;
}
if(props.type==='train'){
stepId = 3;
}
if(props.type==='pssr'){
stepId = 4;
}
if(props.type==='evaluation'){
stepId = 5;
}
const {error} = await useAiAnalysisApi(props.currentId, stepId, {action: !confirmed.value ? 'CONFIRM' : 'CANCEL'});
if(!error){
confirmed.value = !confirmed.value;
emit('confirmTab', props.type, confirmed.value);
}
}
// 保存
const saveDraft = async () => {
emit('save');
}
// 提交
const submitApply = () => {
emit('submit');
}
// 完成设置
const finishApprover = async () => {
emit('update:steps', currentSteps.value);
window.$message?.success("审批流程设置完成");
approvalDrawerShow.value = false;
}
/**
* 在组织树中根据 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>
<template>
<div>
<div class="flex flex-wrap items-center justify-between gap-2 border-t py-3 px-4">
<div class="flex flex-wrap gap-2">
<NButton v-if="isAiOpen && type!=='risk'" size="small" ghost type="warning" :disabled="btnDisabled || regenerateLoading" @click="regenerate">
<Icon icon="tdesign:refresh" class="size-14px mr-1" :class="regenerateLoading?'animate-spin':''" />
<template v-if="regenerateLoading">
<span>生成中...</span>
</template>
<template v-else>
<span v-if="type==='form'">{{regenerateTypes[type]?'重新生成变更申请表':'生成变更申请表'}}</span>
<span v-if="type==='train'">{{regenerateTypes[type]?'重新生成变更培训内容':'生成变更培训内容'}}</span>
<span v-if="type==='pssr'">{{regenerateTypes[type]?'重新生成PSSR检查内容':'生成PSSR检查内容'}}</span>
<span v-if="type==='evaluation'">{{regenerateTypes[type]?'重新生成验收评价':'生成验收评价'}}</span>
<span v-if="type==='close'">{{regenerateTypes[type]?'重新生成变更关闭确认表':'生成变更关闭确认表'}}</span>
</template>
</NButton>
<NButton size="small" ghost type="success" :disabled="btnDisabled || downloadLoading" @click="download">
<Icon v-if="downloadLoading" icon="tdesign:refresh" class="size-14px mr-1 animate-spin" />
<Icon v-else icon="material-symbols:download" class="size-16px" />
<span v-if="type==='form'">下载变更申请表</span>
<span v-if="type==='risk'">下载风险分析记录表</span>
<span v-if="type==='train'">下载变更培训内容</span>
<span v-if="type==='pssr'">下载PSSR检查内容</span>
<span v-if="type==='evaluation'">下载验收评价</span>
<span v-if="type==='close'">下载变更关闭确认表</span>
</NButton>
<NButton v-if="isAiOpen" size="small" 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="currentSteps && currentSteps.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="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 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>
</div>
</div>
<template #footer>
<NButton v-if="currentSteps.length" type="primary" class="w-full" @click="finishApprover">完成设置</NButton>
</template>
</NDrawerContent>
</NDrawer>
</template>