810 lines
42 KiB
Vue
810 lines
42 KiB
Vue
<script setup lang="ts">
|
||
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, peopleListApi, templateListApi } from '@/service/api/system';
|
||
import { getChangeDetailApi } from '@/service/api/change';
|
||
import { myTasksCloseMaterialApi } from '@/service/api/myTasks';
|
||
import previewComponent from '@/components/previewFile/index.vue'
|
||
|
||
const themeStore = useThemeStore();
|
||
const { routerPushByKey } = useRouterPush();
|
||
const route = useRoute();
|
||
|
||
const loading = ref<boolean>(false);
|
||
// 返回键
|
||
const backKey = ref<string>('');
|
||
// 当前变更详情
|
||
const currentInfo = ref<any>(null);
|
||
// 当前变更id
|
||
const currentId = ref<number>(0);
|
||
// 是否开启 AI 辅助功能
|
||
const isAiOn = ref<boolean>(false);
|
||
// 培训签到上传文件列表
|
||
const trainFiles = ref<any>([]);
|
||
// pssr上传文件列表
|
||
const pssrFiles = ref<any>([]);
|
||
// 验收上传文件列表
|
||
const acceptFiles = ref<any>([]);
|
||
// 实际投用日期
|
||
const useDate = ref<number | null>(null);
|
||
// 变更材料和实施费(万元)
|
||
const price = ref<number | null>(null);
|
||
// 验收状态
|
||
const resultStatus = ref<number>(0);
|
||
// 验收结论
|
||
const acceptDesc = ref<string>('');
|
||
// 验收时间
|
||
const acceptDate = ref<number | null>(null);
|
||
// 附件选择列表弹框
|
||
const filesModal = ref<boolean>(false);
|
||
// 选中的文件列表
|
||
const checkedFiles = ref<any>([]);
|
||
// 附件列表
|
||
const fileList = ref<any>([]);
|
||
// 当前从变更附件选择的row下标
|
||
const currentRowIndex = ref<number>(-1);
|
||
// 当前预览文件
|
||
const previewFile = ref<any>(null);
|
||
// 预览文件弹框
|
||
const previewModal = ref<boolean>(false);
|
||
// 成员列表
|
||
const memberList = ref<any[]>([]);
|
||
// 模板
|
||
const currentTemplate = 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>([]);
|
||
|
||
// 返回
|
||
const back = () => {
|
||
routerPushByKey(backKey.value as any);
|
||
};
|
||
|
||
// 上传文件
|
||
const onUploadFile = async (type:string = '',files:any,index:number = -1) => {
|
||
const file = files?.fileList[0]?.file;
|
||
if (!file) return;
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
// 3:培训签到;4:PSSR;5:验收
|
||
if(type==='3' || type==='4' || type==='5'){
|
||
formData.append('step', type);
|
||
}else{
|
||
formData.append('close_doc_id', list.value[index]?.id?.toString());
|
||
}
|
||
loading.value = true;
|
||
const {data,error} = await uploadFileApi(currentId.value, formData);
|
||
if(!error){
|
||
window.$message?.success('上传成功');
|
||
if(type==='3'){
|
||
trainFiles.value.push(data);
|
||
}else if(type==='4'){
|
||
pssrFiles.value.push(data);
|
||
}else if(type==='5'){
|
||
acceptFiles.value.push(data);
|
||
}else{
|
||
list.value[index].attachments.push(data);
|
||
list.value[index].status = 1;
|
||
}
|
||
}
|
||
loading.value = false;
|
||
}
|
||
|
||
// 下载文件
|
||
const downloadFile = (file:any,type:string) => {
|
||
if(file){
|
||
downloadFileFun(file?.file_url, file?.doc_name || file?.file_name);
|
||
}else{
|
||
window.$message?.info('还差接口,空白模板')
|
||
}
|
||
}
|
||
// 删除文件
|
||
const removeFileReal = async (type:string, fileIndex:number,index:number = -1) => {
|
||
let id = 0;
|
||
if(type==='3'){
|
||
id = trainFiles.value[fileIndex].id;
|
||
}else if(type==='4'){
|
||
id = pssrFiles.value[fileIndex].id;
|
||
}else if(type==='5'){
|
||
id = acceptFiles.value[fileIndex].id;
|
||
}else{
|
||
id = list.value[index]?.attachments[fileIndex]?.id;
|
||
}
|
||
loading.value = true;
|
||
const {error} = await deleteFileApi(currentId.value,id);
|
||
if(!error){
|
||
if(type==='3'){
|
||
trainFiles.value.splice(fileIndex, 1);
|
||
}else if(type==='4'){
|
||
pssrFiles.value.splice(fileIndex, 1);
|
||
}else if(type==='5'){
|
||
acceptFiles.value.splice(fileIndex, 1);
|
||
}else{
|
||
list.value[index]?.attachments.splice(fileIndex, 1);
|
||
if(list.value[index]?.attachments.length === 0){
|
||
list.value[index].status = 2;
|
||
}
|
||
}
|
||
window.$message?.success('删除成功');
|
||
}
|
||
loading.value = false;
|
||
}
|
||
// 打开预览
|
||
const preview = (file:any) => {
|
||
previewFile.value = file;
|
||
previewModal.value = true;
|
||
}
|
||
|
||
// 打开附件选择
|
||
const openFileList = (index:number) => {
|
||
currentRowIndex.value = index;
|
||
filesModal.value = true;
|
||
}
|
||
// 选择文件
|
||
const chooseFiles = () => {
|
||
// 过滤出选中的文件
|
||
const ids = new Set(checkedFiles.value);
|
||
const result = fileList.value.filter((item:any) => ids.has(item.id));
|
||
// 合并文件列表
|
||
let files = list.value[currentRowIndex.value]?.attachments.concat(result);
|
||
// 按id去重
|
||
const uniqueList = [
|
||
...new Map(files.map((item:any) => [item.id, item])).values(),
|
||
];
|
||
list.value[currentRowIndex.value].attachments = uniqueList;
|
||
if(uniqueList.length > 0){
|
||
list.value[currentRowIndex.value].status = 1;
|
||
}
|
||
filesModal.value = false;
|
||
}
|
||
|
||
// 保存/申请关闭
|
||
const submitCloseDoc = async (type:string) => {
|
||
if(!useDate.value){
|
||
return window.$message?.error('请选择实际投用日期');
|
||
}
|
||
if(!resultStatus.value){
|
||
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,
|
||
result_status:resultStatus.value,
|
||
note:acceptDesc.value,
|
||
use_time:formatTimestamp(useDate.value,'yyyy-MM-dd'),
|
||
accept_time:formatTimestamp(acceptDate.value,'yyyy-MM-dd'),
|
||
cost:price.value,
|
||
close_docs:result,
|
||
approval_flow: {
|
||
steps: steps
|
||
}
|
||
});
|
||
if(!error){
|
||
if(type==='save'){
|
||
window.$message?.success('保存成功');
|
||
}
|
||
if(type==='close'){
|
||
back();
|
||
window.$message?.success('申请关闭成功');
|
||
}
|
||
}
|
||
loading.value = false;
|
||
}
|
||
|
||
// 获取变更详情
|
||
const getDetail = async () => {
|
||
loading.value = true;
|
||
const {data,error} = await getChangeDetailApi(currentId.value)
|
||
if(!error){
|
||
currentInfo.value = data;
|
||
list.value = data.close_docs || [];
|
||
trainFiles.value = data?.train?.attachments || [];
|
||
pssrFiles.value = data?.pssr?.attachments || [];
|
||
acceptFiles.value = data?.accept_form?.attachments || [];
|
||
if(data?.close_approval_flow && data?.close_approval_flow?.steps?.length){
|
||
flowList.value = data?.close_approval_flow?.steps;
|
||
}else{
|
||
flowList.value = currentTemplate.value;
|
||
}
|
||
}
|
||
loading.value = false;
|
||
};
|
||
// 获取系统配置
|
||
const getConfig = async () => {
|
||
loading.value = true;
|
||
const {data,error} = await systemConfigApi();
|
||
if(!error){
|
||
// 解析 AI 辅助功能
|
||
const aiAssistGroup = data.find((group:{group_key:string}) => group.group_key === 'ai_assist');
|
||
if (aiAssistGroup) {
|
||
const target = aiAssistGroup?.items?.find((item:any) => item.item_key === 'enabled');
|
||
if (target) {
|
||
isAiOn.value = target.value==1?true:false;
|
||
}
|
||
}
|
||
getDetail();
|
||
}
|
||
loading.value = false;
|
||
}
|
||
// 获取文件列表
|
||
const getFileList = async () => {
|
||
const {data,error} = await fileListApi(currentId.value);
|
||
if(!error){
|
||
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;
|
||
}
|
||
|
||
// 获取所有模板
|
||
const getTemplateList = async () => {
|
||
loading.value = true;
|
||
const {data,error} = await templateListApi();
|
||
if(!error){
|
||
const result = data.filter((item:any) => item.type === 3);
|
||
// ******审批模板********
|
||
currentTemplate.value = result[0]?.steps || [];
|
||
}
|
||
loading.value = false;
|
||
}
|
||
|
||
onMounted(async () => {
|
||
useDate.value = new Date().getTime();
|
||
getAllMembers();
|
||
await getTemplateList();
|
||
if(route.query){
|
||
if(route.query.key){
|
||
backKey.value = route.query.key.toString();
|
||
}
|
||
if(route.query.id){
|
||
currentId.value = Number(route.query.id);
|
||
getFileList();
|
||
getConfig();
|
||
}
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<NSpace vertical :size="16">
|
||
<!-- 资料关闭详情-->
|
||
<NSpin :show="loading" size="small">
|
||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||
<div
|
||
class="flex flex-wrap items-center gap-3 border border-slate-200 px-3 py-2 mb-3"
|
||
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
|
||
>
|
||
<NButton ghost type="primary" @click="back">
|
||
<Icon icon="ep:back" class="size-16px mr-1" />返回
|
||
</NButton>
|
||
<div class="min-w-0 flex-1 truncate text-base font-bold text-slate-800">{{ currentInfo?.title }}</div>
|
||
<NTag size="small" type="info" class="font-mono text-xs">编号:{{ currentInfo?.change_no }}</NTag>
|
||
</div>
|
||
<div class="space-y-3 scroll">
|
||
<!-- 投用前确认 -->
|
||
<div class="border rounded-md">
|
||
<div class="flex items-center gap-x-3 px-4 py-2.5 border-b">
|
||
<p class="font-600 text-slate-800 text-sm">投用前确认(含变更培训、投用前检查)</p>
|
||
<span class="text-slate-400 text-xs">上传线下纸质表照片 / 扫描件即视为完成确认,上传后可查阅、下载;空白模板可下载</span>
|
||
</div>
|
||
<div class="p-3 space-y-3">
|
||
<div class="border rounded-lg py-3 px-4 flex items-center gap-3">
|
||
<Icon icon="bi:upload" class="size-18px" :style="{ color: trainFiles.length?themeStore.otherColor.success:themeStore.themeColor }" />
|
||
<div class="flex-1">
|
||
<div class="flex items-center gap-2">
|
||
<span class="text-slate-800 text-sm font-600">上传培训签到</span>
|
||
<p v-if="trainFiles.length" class="py-0.5 px-1.5 text-11px bg-emerald-100 rounded-md text-emerald-700">已上传 · 视为完成确认</p>
|
||
</div>
|
||
<p class="text-slate-400 text-xs mt-1 leading-4">线下培训签到表照片或扫描件;上传后视为培训完成确认,签到记录随单归档</p>
|
||
<div v-if="trainFiles.length" class="flex flex-wrap gap-2 mt-1">
|
||
<div
|
||
v-for="(file, fileIndex) in trainFiles"
|
||
:key="fileIndex"
|
||
class="text-11px flex items-center gap-2 border py-0.5 px-1.5 rounded-md text-[#17407F] border-[#ACC6E5] bg-[#EAF0F9]"
|
||
>
|
||
<Icon icon="akar-icons:file" class="size-12px" />
|
||
{{file.file_name}}
|
||
<NButton text type="primary" class="text-11px" @click="preview(file)">预览</NButton>
|
||
<NButton text type="primary" class="text-11px" @click="downloadFile(file,'train')">下载</NButton>
|
||
<NPopconfirm
|
||
positive-text="确定"
|
||
:positiveButtonProps="{ size: 'tiny' }"
|
||
:negativeButtonProps="{ size: 'tiny' }"
|
||
@positive-click="removeFileReal('3', Number(fileIndex))"
|
||
>
|
||
<template #trigger>
|
||
<NButton text type="error" class="text-11px">删除</NButton>
|
||
</template>
|
||
确定删除吗?
|
||
</NPopconfirm>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<NButton ghost type="primary" size="small" class="text-xs" @click="downloadFile(null,'train')">
|
||
<Icon icon="akar-icons:download" class="size-14px mr-1" />
|
||
下载培训签到表
|
||
</NButton>
|
||
<NUpload
|
||
class="w-auto"
|
||
:key="trainFiles.length?'train_'+trainFiles.length+1:'train'"
|
||
accept="image/*,.pdf,.docx,.xlsx,.pptx"
|
||
:default-upload="false"
|
||
:show-file-list="false"
|
||
@change="(files) => onUploadFile('3', files)"
|
||
>
|
||
<NButton type="primary" size="small" class="text-xs" :disabled="loading">上传</NButton>
|
||
</NUpload>
|
||
</div>
|
||
<div class="border rounded-lg py-3 px-4 flex items-center gap-3">
|
||
<Icon icon="bi:upload" class="size-18px" :style="{ color: pssrFiles.length?themeStore.otherColor.success:themeStore.themeColor }" />
|
||
<div class="flex-1">
|
||
<div class="flex items-center gap-2">
|
||
<span class="text-slate-800 text-sm font-600">上传 PSSR 检查</span>
|
||
<p v-if="pssrFiles.length" class="py-0.5 px-1.5 text-11px bg-emerald-100 rounded-md text-emerald-700">已上传 · 视为完成确认</p>
|
||
</div>
|
||
<p class="text-slate-400 text-xs mt-1 leading-4">PSSR 纸质确认表照片或扫描件;上传后视为 PSSR 全项确认完成,具备投用条件</p>
|
||
<div v-if="pssrFiles.length" class="flex flex-wrap gap-2 mt-1">
|
||
<div
|
||
v-for="(file, fileIndex) in pssrFiles"
|
||
:key="fileIndex"
|
||
class="text-11px flex items-center gap-2 border py-0.5 px-1.5 rounded-md text-[#17407F] border-[#ACC6E5] bg-[#EAF0F9]"
|
||
>
|
||
<Icon icon="akar-icons:file" class="size-12px" />
|
||
{{file.file_name}}
|
||
<NButton text type="primary" class="text-11px" @click="preview(file)">预览</NButton>
|
||
<NButton text type="primary" class="text-11px" @click="downloadFile(file,'pssr')">下载</NButton>
|
||
<NPopconfirm
|
||
positive-text="确定"
|
||
:positiveButtonProps="{ size: 'tiny' }"
|
||
:negativeButtonProps="{ size: 'tiny' }"
|
||
@positive-click="removeFileReal('4', Number(fileIndex))"
|
||
>
|
||
<template #trigger>
|
||
<NButton text type="error" class="text-11px">删除</NButton>
|
||
</template>
|
||
确定删除吗?
|
||
</NPopconfirm>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<NButton ghost type="primary" size="small" class="text-xs" @click="downloadFile(null,'pssr')">
|
||
<Icon icon="akar-icons:download" class="size-14px mr-1" />
|
||
下载PSSR检查表
|
||
</NButton>
|
||
<NUpload
|
||
class="w-auto"
|
||
:key="pssrFiles.length?'pssr_'+pssrFiles.length+1:'pssr'"
|
||
accept="image/*,.pdf,.docx,.xlsx,.pptx"
|
||
:default-upload="false"
|
||
:show-file-list="false"
|
||
@change="(files) => onUploadFile('4', files)"
|
||
>
|
||
<NButton type="primary" size="small" class="text-xs" :disabled="loading">上传</NButton>
|
||
</NUpload>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 变更投用时间 -->
|
||
<div class="border rounded-md">
|
||
<div class="flex items-center gap-x-3 px-4 py-2.5 border-b">
|
||
<p class="font-600 text-slate-800 text-sm">变更投用时间</p>
|
||
</div>
|
||
<div class="p-3">
|
||
<div class="flex items-center gap-2">
|
||
<p class="text-sm text-slate-600">实际投用时间</p>
|
||
<NDatePicker v-model:value="useDate" type="date" size="small" />
|
||
<p class="text-xs text-slate-400 mt-1px">默认为当天日期,可按实际投用日期调整</p>
|
||
</div>
|
||
<div class="mt-2.5 rounded-lg border px-3 py-2 text-[11px] leading-5 border-amber-200 bg-amber-50/60 text-amber-700"> 投用提示:投用前应完成 PSSR 检查。 未完成的变更不得投用。</div>
|
||
</div>
|
||
</div>
|
||
<!-- 变更验收 -->
|
||
<div class="border rounded-md">
|
||
<div class="flex items-center gap-x-3 px-4 py-2.5 border-b">
|
||
<p class="font-600 text-slate-800 text-sm">变更验收</p>
|
||
<span class="text-slate-400 text-xs">投用后组织验收:上传验收资料并选择验收结论;上传后可查阅、下载;验收表可下载</span>
|
||
</div>
|
||
<div class="p-3 space-y-2.5">
|
||
<div class="border rounded-lg py-3 px-4 flex items-center gap-3">
|
||
<Icon icon="bi:upload" class="size-18px" :style="{ color: acceptFiles.length?themeStore.otherColor.success:themeStore.themeColor }" />
|
||
<div class="flex-1">
|
||
<div class="flex items-center gap-2">
|
||
<span class="text-slate-800 text-sm font-600">上传验收资料</span>
|
||
<p v-if="pssrFiles.length" class="py-0.5 px-1.5 text-11px bg-emerald-100 rounded-md text-emerald-700">已上传 · 视为完成确认</p>
|
||
</div>
|
||
<p class="text-slate-400 text-xs mt-0.5 leading-4">验收报告、运行数据记录等验收证明材料;上传后纳入归档清单</p>
|
||
<div v-if="acceptFiles.length" class="flex flex-wrap gap-2 mt-1">
|
||
<div
|
||
v-for="(file, fileIndex) in acceptFiles"
|
||
:key="fileIndex"
|
||
class="text-11px flex items-center gap-2 border py-0.5 px-1.5 rounded-md text-[#17407F] border-[#ACC6E5] bg-[#EAF0F9]"
|
||
>
|
||
<Icon icon="akar-icons:file" class="size-12px" />
|
||
{{file.file_name}}
|
||
<NButton text type="primary" class="text-11px" @click="preview(file)">预览</NButton>
|
||
<NButton text type="primary" class="text-11px" @click="downloadFile(file,'accept')">下载</NButton>
|
||
<NPopconfirm
|
||
positive-text="确定"
|
||
:positiveButtonProps="{ size: 'tiny' }"
|
||
:negativeButtonProps="{ size: 'tiny' }"
|
||
@positive-click="removeFileReal('5', Number(fileIndex))"
|
||
>
|
||
<template #trigger>
|
||
<NButton text type="error" class="text-11px">删除</NButton>
|
||
</template>
|
||
确定删除吗?
|
||
</NPopconfirm>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<NButton ghost type="primary" size="small" class="text-xs" @click="downloadFile(null,'accept')">
|
||
<Icon icon="akar-icons:download" class="size-14px mr-1" />
|
||
下载验收表
|
||
</NButton>
|
||
<NUpload
|
||
class="w-auto"
|
||
:key="acceptFiles.length?'accept_'+acceptFiles.length+1:'accept'"
|
||
accept="image/*,.pdf,.docx,.xlsx,.pptx"
|
||
:default-upload="false"
|
||
:show-file-list="false"
|
||
@change="(files) => onUploadFile('5', files)"
|
||
>
|
||
<NButton type="primary" size="small" class="text-xs" :disabled="loading">上传</NButton>
|
||
</NUpload>
|
||
</div>
|
||
<div class="border rounded-lg py-3 px-4 flex flex-col gap-2">
|
||
<div class="flex items-center gap-2">
|
||
<p class="text-xs font-600" :style="{ color: themeStore.themeColor }">验收结论(互斥选择)</p>
|
||
<NButton size="small" :type="resultStatus === 3?'primary':'default'" class="text-xs" @click="resultStatus = 3">完全达到</NButton>
|
||
<NButton size="small" :type="resultStatus === 2?'primary':'default'" class="text-xs" @click="resultStatus = 2">基本达到</NButton>
|
||
<NButton size="small" :type="resultStatus === 1?'primary':'default'" class="text-xs" @click="resultStatus = 1">未达到</NButton>
|
||
</div>
|
||
<NInput v-model:value="acceptDesc" type="textarea" size="small" :autosize="{ minRows: 2 }" placeholder="请输入验收结论" />
|
||
</div>
|
||
<div class="border rounded-lg py-3 px-4 flex items-center gap-2">
|
||
<p class="text-xs font-600" :style="{ color: themeStore.themeColor }">变更材料和实施费(万元)</p>
|
||
<NInputNumber v-model:value="price" size="small" :min="0" :show-button="false" placeholder="如 12.6" />
|
||
<p class="text-xs text-slate-400 mt-1px">非必填 · 填报本次变更材料与实施费用合计,用于变更投入与效果统计</p>
|
||
</div>
|
||
<div class="border rounded-lg py-3 px-4 flex items-center gap-2">
|
||
<p class="text-xs font-600" :style="{ color: themeStore.themeColor }">验收时间</p>
|
||
<NDatePicker v-model:value="acceptDate" type="date" size="small" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 变更关闭资料归档确认 -->
|
||
<div class="border rounded-md">
|
||
<div class="flex items-center gap-x-3 px-4 py-2.5 border-b">
|
||
<p class="font-600 text-slate-800 text-sm">变更关闭资料归档确认</p>
|
||
<span class="text-slate-400 text-xs">是 = 已归档 · 否 = 待归档(本地上传或从变更附件选择,完成后自动转为「是」)· 不涉及 = 无需处理</span>
|
||
<NButton size="small" type="primary" ghost class="ml-auto text-xs">AI辅助判定</NButton>
|
||
</div>
|
||
<div class="p-3">
|
||
<NTable size="small">
|
||
<thead>
|
||
<tr>
|
||
<th>资料名称</th>
|
||
<th class="!text-center">是否涉及</th>
|
||
<th class="w-200px !text-center">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="(item, index) in list" :key="index">
|
||
<td>
|
||
<p>{{item.doc_name}}</p>
|
||
<div v-if="item.attachments.length" class="flex flex-wrap gap-2">
|
||
<div
|
||
v-for="(file, fileIndex) in item.attachments"
|
||
:key="fileIndex"
|
||
class="text-11px flex items-center gap-2 border py-0.5 px-1.5 rounded-md text-[#17407F] border-[#ACC6E5] bg-[#EAF0F9]"
|
||
>
|
||
<Icon icon="akar-icons:file" class="size-12px" />
|
||
{{file.file_name}}
|
||
<NButton text type="primary" class="text-11px" @click="preview(file)">预览</NButton>
|
||
<NButton text type="primary" class="text-11px" @click="downloadFile(file,'close')">下载</NButton>
|
||
<NPopconfirm
|
||
positive-text="确定"
|
||
:positiveButtonProps="{ size: 'tiny' }"
|
||
:negativeButtonProps="{ size: 'tiny' }"
|
||
@positive-click="removeFileReal(item.doc_name, Number(fileIndex), Number(index))"
|
||
>
|
||
<template #trigger>
|
||
<NButton text type="error" class="text-11px">删除</NButton>
|
||
</template>
|
||
确定删除吗?
|
||
</NPopconfirm>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<div class="flex justify-center gap-2">
|
||
<NButton size="small" :type="item.status === 1 ? 'primary' : 'default'" class="text-xs" @click="item.status = 1">涉及</NButton>
|
||
<NButton size="small" :type="item.status === 2 ? 'primary' : 'default'" class="text-xs" @click="item.status = 2">不涉及</NButton>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<div v-if="item.status === 1" class="flex justify-center gap-2">
|
||
<NButton size="small" class="text-xs" @click="openFileList(Number(index))">从变更附件选择</NButton>
|
||
<NUpload
|
||
class="w-auto"
|
||
:key="item.id"
|
||
accept="image/*,.pdf,.docx,.xlsx,.pptx"
|
||
:default-upload="false"
|
||
:show-file-list="false"
|
||
@change="(files) => onUploadFile(item.doc_name, files, Number(index))"
|
||
>
|
||
<NButton type="primary" size="small" ghost class="text-xs" :disabled="loading">
|
||
本地上传
|
||
</NButton>
|
||
</NUpload>
|
||
</div>
|
||
<div v-if="item.status === 2" class="flex justify-end gap-2">
|
||
<span class="text-xs text-slate-400">无需处理</span>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</NTable>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="flex items-center justify-end border-t pt-3 mt-3">
|
||
<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" :auto-focus="false">
|
||
<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"
|
||
preset="card"
|
||
title="从变更附件选择"
|
||
:auto-focus="false"
|
||
:style="{ width: '450px', height: 'auto' }"
|
||
:segmented="{ content: true, footer: true }"
|
||
>
|
||
<div v-if="fileList.length" class="max-h-60vh overflow-y-auto">
|
||
<NCheckboxGroup v-model:value="checkedFiles">
|
||
<div class="space-y-2">
|
||
<div v-for="item in fileList" :key="item.id">
|
||
<NCheckbox :value="item.id" :label="item.file_name" />
|
||
</div>
|
||
</div>
|
||
</NCheckboxGroup>
|
||
</div>
|
||
<div v-else>
|
||
<NEmpty description="暂无附件"></NEmpty>
|
||
</div>
|
||
<template v-if="fileList.length" #footer>
|
||
<div class="flex justify-end gap-[10px]">
|
||
<NButton size="small" @click="filesModal = false">取消</NButton>
|
||
<NButton size="small" type="primary" @click="chooseFiles">确定选择</NButton>
|
||
</div>
|
||
</template>
|
||
</NModal>
|
||
<!-- 附件在线预览弹窗 -->
|
||
<NModal
|
||
v-model:show="previewModal"
|
||
preset="card"
|
||
:auto-focus="false"
|
||
:style="{ width: '80%', height: 'auto' }"
|
||
:segmented="{ content: true, footer: true }"
|
||
>
|
||
<template #header>
|
||
<div class="flex items-center gap-2 text-base font-semibold text-slate-800">
|
||
<Icon icon="akar-icons:file" class="size-18px" /> {{ previewFile?.file_name }}
|
||
</div>
|
||
</template>
|
||
<template #default>
|
||
<div class="space-y-3">
|
||
<div class="flex flex-wrap gap-x-4 gap-y-1 text-xs text-slate-400">
|
||
<span>上传人:{{ previewFile?.uploader_name }}</span>
|
||
<span>上传时间:{{ previewFile?.created_at }}</span>
|
||
<span v-if="previewFile?.version">{{ `版本:${previewFile?.version}(受控)` }}</span>
|
||
</div>
|
||
<previewComponent v-if="previewFile" :previewFile="previewFile" />
|
||
</div>
|
||
</template>
|
||
</NModal>
|
||
</NCard>
|
||
</NSpin>
|
||
</NSpace>
|
||
</template>
|
||
|
||
<style scoped lang="scss">
|
||
.scroll {
|
||
height: calc(100vh - 263px);
|
||
overflow-y: auto;
|
||
}
|
||
</style>
|