v1流程下修复所有bug

pull/874/head
wh 2026-02-08 21:02:37 +08:00
parent c66abdf33e
commit 949725ffc6
4 changed files with 940 additions and 452 deletions

View File

@ -159,3 +159,8 @@ export const cancelAcceptance = (acceptanceId: number, reason: string) => {
export const getTodoList = () => { export const getTodoList = () => {
return request.get({ url: '/project/acceptance/todo' }) return request.get({ url: '/project/acceptance/todo' })
} }
// 获取验收状态列表
export const getStatusList = () => {
return request.get<{ code: string; name: string }[]>({ url: '/project/acceptance/status-list' })
}

View File

@ -1,33 +1,60 @@
<template> <template>
<ContentWrap> <div class="p-4 bg-gray-50 min-h-screen">
<div v-loading="loading"> <div v-loading="loading">
<!-- 头部信息 --> <!-- 头部 Hero Section -->
<!-- 头部信息 --> <div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 mb-6">
<div class="bg-white p-5 mb-6 rounded-lg shadow-sm border border-gray-100"> <div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-8">
<div class="flex justify-between items-start mb-8">
<div> <div>
<h2 class="text-2xl font-bold text-gray-800 flex items-center mb-3"> <div class="flex items-center gap-3 mb-2">
<h2 class="text-2xl font-bold text-gray-800 tracking-tight">
{{ projectInfo.projectName || '项目验收' }} {{ projectInfo.projectName || '项目验收' }}
<el-tag v-if="acceptance.acceptanceType === 'PRE'" type="info" effect="dark" class="ml-3 rounded-full px-3"></el-tag>
<el-tag v-else-if="acceptance.acceptanceType === 'FINAL'" type="primary" effect="dark" class="ml-3 rounded-full px-3">终验</el-tag>
</h2> </h2>
<div class="text-gray-500 text-sm flex items-center gap-4 bg-gray-50 px-4 py-2 rounded-md"> <el-tag
<span><span class="font-medium text-gray-700">项目编号</span>{{ projectInfo.projectCode }}</span> v-if="acceptance.acceptanceType === 'PRE'"
<el-divider direction="vertical" /> type="info"
<span><span class="font-medium text-gray-700">验收ID</span>{{ acceptance.id }}</span> effect="plain"
<el-divider direction="vertical" /> class="!rounded-full !px-3 font-medium border-gray-300"
<span><span class="font-medium text-gray-700">轮次</span> {{ acceptance.round }} </span> >
预验收
</el-tag>
<el-tag
v-else-if="acceptance.acceptanceType === 'FINAL'"
type="primary"
effect="plain"
class="!rounded-full !px-3 font-medium border-blue-200"
>
终验
</el-tag>
</div>
<div class="flex flex-wrap items-center gap-x-6 gap-y-2 text-sm text-gray-500">
<div class="flex items-center gap-1">
<Icon icon="ep:collection-tag" class="text-gray-400" />
<span>项目编号<span class="text-gray-700 font-medium">{{ projectInfo.projectCode }}</span></span>
</div>
<div class="hidden md:block w-px h-3.5 bg-gray-300"></div>
<div class="flex items-center gap-1">
<Icon icon="ep:hash" class="text-gray-400" />
<span>验收ID<span class="text-gray-700 font-medium">{{ acceptance.id }}</span></span>
</div>
<div class="hidden md:block w-px h-3.5 bg-gray-300"></div>
<div class="flex items-center gap-1">
<Icon icon="ep:refresh" class="text-gray-400" />
<span>轮次<span class="text-gray-700 font-medium">R{{ acceptance.round }}</span></span>
</div> </div>
</div> </div>
<div> </div>
<el-tag :type="getStatusType(acceptance.status)" size="large" effect="plain" class="text-base px-6 py-1.5 font-bold rounded-lg border-2">
<div class="flex items-center gap-3">
<el-tag :type="getStatusType(acceptance.status)" size="large" effect="dark" class="!px-4 !py-1.5 !text-base !font-semibold !rounded-lg shadow-sm">
{{ getStatusLabel(acceptance.status) }} {{ getStatusLabel(acceptance.status) }}
</el-tag> </el-tag>
</div> </div>
</div> </div>
<!-- 流程步骤 --> <!-- 流程步骤 -->
<el-steps :active="activeStep" finish-status="success" align-center class="mb-2"> <div class="px-4 py-2 bg-gray-50 rounded-lg border border-gray-100">
<el-steps :active="activeStep" finish-status="success" align-center class="custom-steps">
<el-step title="提交材料" description="待提交/完善材料" /> <el-step title="提交材料" description="待提交/完善材料" />
<el-step title="初步审核" description="对口人/组长审核" /> <el-step title="初步审核" description="对口人/组长审核" />
<el-step title="终验申请" description="确认进入终验" /> <el-step title="终验申请" description="确认进入终验" />
@ -35,96 +62,113 @@
<el-step title="归档完成" description="流程结束" /> <el-step title="归档完成" description="流程结束" />
</el-steps> </el-steps>
</div> </div>
</div>
<el-divider /> <!-- 主要内容区域 -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<el-tabs v-model="activeTab"> <!-- 左侧详细信息 (占据 2/3 宽度) -->
<div class="lg:col-span-2 space-y-6">
<!-- Tab切换 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden min-h-[500px]">
<el-tabs v-model="activeTab" class="custom-tabs">
<!-- 基本信息 --> <!-- 基本信息 -->
<el-tab-pane label="基本信息" name="info"> <el-tab-pane label="基本信息" name="info">
<el-descriptions :column="2" border> <div class="p-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-y-6 gap-x-12">
<div class="space-y-4">
<h3 class="text-base font-bold text-gray-900 flex items-center gap-2 pb-2 border-b border-gray-100">
<Icon icon="ep:user" /> 人员信息
</h3>
<div class="grid grid-cols-[80px_1fr] gap-y-3 text-sm">
<span class="text-gray-500 text-right">项目联络人:</span>
<span class="font-medium text-gray-800">{{ liaisonUserName || '-' }}</span>
<span class="text-gray-500 text-right">对口人:</span>
<span class="font-medium text-gray-800">{{ serviceUserName || '-' }}</span>
</div>
</div>
<el-descriptions-item label="项目联络人">{{ liaisonUserName || '-' }}</el-descriptions-item> <div class="space-y-4">
<el-descriptions-item label="对口人">{{ serviceUserName || '-' }}</el-descriptions-item> <h3 class="text-base font-bold text-gray-900 flex items-center gap-2 pb-2 border-b border-gray-100">
<el-descriptions-item label="开始时间">{{ formatDate(acceptance.startTime) }}</el-descriptions-item> <Icon icon="ep:timer" /> 时间及流程
<el-descriptions-item label="完成时间">{{ formatDate(acceptance.endTime) || '-' }}</el-descriptions-item> </h3>
<el-descriptions-item label="流程实例ID">{{ acceptance.processInstanceId || '-' }}</el-descriptions-item> <div class="grid grid-cols-[100px_1fr] gap-y-3 text-sm">
<el-descriptions-item label="创建时间">{{ formatDate(acceptance.createTime) }}</el-descriptions-item> <span class="text-gray-500 text-right">开始时间:</span>
</el-descriptions> <span class="font-medium text-gray-800">{{ formatDate(acceptance.startTime) }}</span>
<span class="text-gray-500 text-right">流程实例ID:</span>
<a class="text-blue-600 font-mono underline cursor-pointer hover:text-blue-800" :title="acceptance.processInstanceId">{{ acceptance.processInstanceId || '-' }}</a>
<span class="text-gray-500 text-right">创建时间:</span>
<span class="font-medium text-gray-800">{{ formatDate(acceptance.createTime) }}</span>
</div>
</div>
</div>
</div>
</el-tab-pane> </el-tab-pane>
<!-- 验收材料 --> <!-- 验收材料 -->
<el-tab-pane label="验收材料" name="materials"> <el-tab-pane label="验收材料" name="materials">
<div class="p-6">
<!-- 联络人视图显示当前阶段材料 -->
<template v-if="isLiaisonUser">
<div class="mb-6" v-if="canUploadMaterial"> <div class="mb-6" v-if="canUploadMaterial">
<el-alert <el-alert
title="待办任务:上传验收材料"
type="primary" type="primary"
:closable="false" :closable="false"
show-icon show-icon
description="请完成下方所有必填材料的上传,确认无误后点击底部的【提交材料】按钮进入审核流程。" class="!bg-blue-50 !border-blue-100 !text-blue-800"
/>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
<template v-for="(item, index) in materials" :key="index">
<el-card
shadow="hover"
class="relative transition-all duration-300 hover:shadow-lg hover:-translate-y-1 border-gray-200"
:body-style="{ padding: '16px' }"
> >
<div class="flex items-start"> <template #title>
<!-- 文件图标 --> <span class="font-bold text-base">待办任务上传验收材料</span>
<div class="w-12 h-12 flex items-center justify-center rounded-lg mr-4 shrink-0"
:class="getFileIconColor(item.fileName)">
<Icon :icon="getFileIcon(item.fileName)" size="28" color="#fff" />
</div>
<!-- 内容区域 -->
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between mb-1">
<span class="font-bold text-gray-800 truncate text-[15px]" :title="item.materialName">
{{ item.materialName }}
</span>
<el-tag v-if="item.isRequired" type="danger" effect="plain" size="small" class="ml-2 px-1"></el-tag>
<el-tag v-else type="info" effect="plain" size="small" class="ml-2 px-1">可选</el-tag>
</div>
<!-- 上传状态/文件名 -->
<div class="h-[24px] mb-3 flex items-center">
<template v-if="item.fileUrl">
<el-tooltip :content="item.fileName" placement="top" :show-after="500">
<span class="text-xs text-blue-600 truncate underline cursor-pointer hover:text-blue-800"
@click="handlePreview(item)">
{{ item.fileName }}
</span>
</el-tooltip>
</template> </template>
<span v-else class="text-xs text-gray-400 italic">暂未上传文件</span> <template #default>
<p class="mt-1 text-sm text-blue-600">请完成下方所有必填材料的上传确认无误后点击底部的提交材料按钮进入审核流程</p>
</template>
</el-alert>
</div> </div>
<!-- 操作按钮组 --> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="flex items-center justify-between mt-2 pt-3 border-t border-gray-100"> <template v-for="(item, index) in materials" :key="index">
<div class="flex gap-2"> <div class="group relative border border-gray-200 rounded-xl p-4 transition-all duration-300 hover:shadow-md hover:border-blue-200 hover:bg-blue-50/20 bg-white">
<el-button <div class="flex items-start gap-4">
v-if="item.fileUrl" <!-- 文件图标 -->
link <div class="w-12 h-12 flex items-center justify-center rounded-lg shrink-0 transition-transform group-hover:scale-105"
size="small" :class="[getFileIconColor(item.fileName), 'bg-opacity-10']">
type="primary" <Icon :icon="getFileIcon(item.fileName)" size="26" :class="item.fileUrl ? '' : 'grayscale opacity-50'" />
@click="handlePreview(item)"
>
<Icon icon="ep:view" class="mr-1"/> 预览
</el-button>
<el-button
v-if="item.fileUrl"
link
size="small"
type="success"
@click="handleDownload(item)"
>
<Icon icon="ep:download" class="mr-1"/> 下载
</el-button>
</div> </div>
<!-- 上传按钮 --> <div class="flex-1 min-w-0">
<div class="flex justify-between items-start">
<h4 class="font-semibold text-gray-800 truncate pr-2 text-[15px] group-hover:text-blue-700" :title="item.materialName">{{ item.materialName }}</h4>
<span v-if="item.isRequired" class="text-[10px] font-bold text-red-500 bg-red-50 px-1.5 py-0.5 rounded border border-red-100 shrink-0"></span>
</div>
<div class="mt-1 mb-3 h-5">
<template v-if="item.fileUrl">
<div class="flex items-center gap-1 text-xs text-gray-500 truncate">
<Icon icon="ep:document" class="text-gray-400" />
<span class="truncate underline cursor-pointer hover:text-blue-600" @click="handlePreview(item)">{{ item.fileName }}</span>
</div>
</template>
<span v-else class="text-xs text-gray-400 italic">暂未上传</span>
</div>
<!-- Actions -->
<div class="flex items-center justify-end gap-2 pt-2 border-t border-gray-50 mt-2 opacity-60 group-hover:opacity-100 transition-opacity">
<!-- 预览 -->
<el-tooltip content="预览" placement="top" v-if="item.fileUrl">
<button @click="handlePreview(item)" class="p-2 rounded-full hover:bg-blue-50 text-gray-500 hover:text-blue-600 transition-all border-none outline-none focus:outline-none">
<Icon icon="ep:view" size="20" />
</button>
</el-tooltip>
<!-- 下载 -->
<el-tooltip content="下载" placement="top" v-if="item.fileUrl">
<button @click="handleDownload(item)" class="p-2 rounded-full hover:bg-green-50 text-gray-500 hover:text-green-600 transition-all border-none outline-none focus:outline-none">
<Icon icon="ep:download" size="20" />
</button>
</el-tooltip>
<!-- 上传/重新上传 -->
<div v-if="canUploadMaterial"> <div v-if="canUploadMaterial">
<el-upload <el-upload
:action="uploadUrl" :action="uploadUrl"
@ -132,96 +176,296 @@
:show-file-list="false" :show-file-list="false"
:on-success="(res, file) => handleUploadSuccess(res, file, item)" :on-success="(res, file) => handleUploadSuccess(res, file, item)"
> >
<el-button <el-tooltip :content="item.fileUrl ? '重新上传' : '上传文件'" placement="top">
size="small" <button class="p-2 rounded-full hover:bg-blue-50 text-gray-500 hover:text-blue-600 transition-all border-none outline-none focus:outline-none">
:type="item.fileUrl ? 'primary' : 'primary'" <Icon :icon="item.fileUrl ? 'ep:refresh' : 'ep:upload'" size="20" />
:plain="!!item.fileUrl" </button>
class="!rounded-md" </el-tooltip>
>
<Icon icon="ep:upload" class="mr-1"/>
{{ item.fileUrl ? '重新上传' : '上传材料' }}
</el-button>
</el-upload> </el-upload>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- 状态角标 --> <!-- Corner Status Mark -->
<div class="absolute top-0 right-0"> <div v-if="item.fileUrl" class="absolute -top-[1px] -right-[1px] w-0 h-0 border-t-[32px] border-r-[32px] border-t-transparent border-r-green-500 rounded-tr-xl">
<div v-if="item.fileUrl" class="bg-green-100 text-green-600 text-[10px] px-2 py-0.5 rounded-bl-lg font-medium"></div> <Icon icon="ep:check" class="absolute -right-[26px] -top-[26px] text-white text-xs"/>
<div v-else class="bg-gray-100 text-gray-500 text-[10px] px-2 py-0.5 rounded-bl-lg">待上传</div> </div>
</div>
</template>
</div>
</template>
<!-- 其他角色视图分组显示预验收和终验材料 -->
<template v-else>
<!-- 预验收材料区域 -->
<div class="mb-8">
<h3 class="text-lg font-bold mb-4">
<span class="inline-block px-4 py-2 rounded-lg bg-blue-100 text-blue-800 w-40 text-center">预验收材料</span>
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<template v-for="(item, index) in preMaterials" :key="'pre-' + index">
<div class="group relative border border-gray-200 rounded-xl p-4 transition-all duration-300 hover:shadow-md hover:border-blue-200 hover:bg-blue-50/20 bg-white">
<div class="flex items-start gap-4">
<!-- 文件图标 -->
<div class="w-12 h-12 flex items-center justify-center rounded-lg shrink-0 transition-transform group-hover:scale-105"
:class="[getFileIconColor(item.fileName), 'bg-opacity-10']">
<Icon :icon="getFileIcon(item.fileName)" size="26" :class="item.fileUrl ? '' : 'grayscale opacity-50'" />
</div>
<div class="flex-1 min-w-0">
<div class="flex justify-between items-start">
<h4 class="font-semibold text-gray-800 truncate pr-2 text-[15px] group-hover:text-blue-700" :title="item.materialName">{{ item.materialName }}</h4>
<span v-if="item.isRequired" class="text-[10px] font-bold text-red-500 bg-red-50 px-1.5 py-0.5 rounded border border-red-100 shrink-0"></span>
</div>
<div class="mt-1 mb-3 h-5">
<template v-if="item.fileUrl">
<div class="flex items-center gap-1 text-xs text-gray-500 truncate">
<Icon icon="ep:document" class="text-gray-400" />
<span class="truncate underline cursor-pointer hover:text-blue-600" @click="handlePreview(item)">{{ item.fileName }}</span>
</div>
</template>
<span v-else class="text-xs text-gray-400 italic">暂未上传</span>
</div>
<!-- Actions -->
<div class="flex items-center justify-end gap-2 pt-2 border-t border-gray-50 mt-2 opacity-60 group-hover:opacity-100 transition-opacity">
<!-- 预览 -->
<el-tooltip content="预览" placement="top" v-if="item.fileUrl">
<button @click="handlePreview(item)" class="p-2 rounded-full hover:bg-blue-50 text-gray-500 hover:text-blue-600 transition-all border-none outline-none focus:outline-none">
<Icon icon="ep:view" size="20" />
</button>
</el-tooltip>
<!-- 下载 -->
<el-tooltip content="下载" placement="top" v-if="item.fileUrl">
<button @click="handleDownload(item)" class="p-2 rounded-full hover:bg-green-50 text-gray-500 hover:text-green-600 transition-all border-none outline-none focus:outline-none">
<Icon icon="ep:download" size="20" />
</button>
</el-tooltip>
<!-- 历史版本 -->
<el-tooltip :content="'历史版本 (' + item.versionCount + ')'" placement="top" v-if="item.versionCount > 1">
<button @click="showVersionHistory(item)" class="p-2 rounded-full hover:bg-orange-50 text-gray-500 hover:text-orange-600 transition-all relative border-none outline-none focus:outline-none">
<Icon icon="ep:clock" size="20" />
<!-- 小红点提示 -->
<span class="absolute top-0.5 right-0.5 w-1.5 h-1.5 bg-orange-500 rounded-full border border-white"></span>
</button>
</el-tooltip>
</div>
</div>
</div>
<!-- Corner Status Mark -->
<div v-if="item.fileUrl" class="absolute -top-[1px] -right-[1px] w-0 h-0 border-t-[32px] border-r-[32px] border-t-transparent border-r-green-500 rounded-tr-xl">
<Icon icon="ep:check" class="absolute -right-[26px] -top-[26px] text-white text-xs"/>
</div>
</div>
</template>
<div v-if="preMaterials.length === 0" class="col-span-2 text-center py-8 text-gray-400">
暂无预验收材料
</div>
</div>
</div>
<!-- 终验材料区域 -->
<div>
<h3 class="text-lg font-bold mb-4">
<span class="inline-block px-4 py-2 rounded-lg bg-purple-100 text-purple-800 w-40 text-center">终验材料</span>
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<template v-for="(item, index) in finalMaterials" :key="'final-' + index">
<div class="group relative border border-gray-200 rounded-xl p-4 transition-all duration-300 hover:shadow-md hover:border-blue-200 hover:bg-blue-50/20 bg-white">
<div class="flex items-start gap-4">
<!-- 文件图标 -->
<div class="w-12 h-12 flex items-center justify-center rounded-lg shrink-0 transition-transform group-hover:scale-105"
:class="[getFileIconColor(item.fileName), 'bg-opacity-10']">
<Icon :icon="getFileIcon(item.fileName)" size="26" :class="item.fileUrl ? '' : 'grayscale opacity-50'" />
</div>
<div class="flex-1 min-w-0">
<div class="flex justify-between items-start">
<h4 class="font-semibold text-gray-800 truncate pr-2 text-[15px] group-hover:text-blue-700" :title="item.materialName">{{ item.materialName }}</h4>
<span v-if="item.isRequired" class="text-[10px] font-bold text-red-500 bg-red-50 px-1.5 py-0.5 rounded border border-red-100 shrink-0"></span>
</div>
<div class="mt-1 mb-3 h-5">
<template v-if="item.fileUrl">
<div class="flex items-center gap-1 text-xs text-gray-500 truncate">
<Icon icon="ep:document" class="text-gray-400" />
<span class="truncate underline cursor-pointer hover:text-blue-600" @click="handlePreview(item)">{{ item.fileName }}</span>
</div>
</template>
<span v-else class="text-xs text-gray-400 italic">暂未上传</span>
</div>
<!-- Actions -->
<div class="flex items-center justify-end gap-2 pt-2 border-t border-gray-50 mt-2 opacity-60 group-hover:opacity-100 transition-opacity">
<!-- 预览 -->
<el-tooltip content="预览" placement="top" v-if="item.fileUrl">
<button @click="handlePreview(item)" class="p-2 rounded-full hover:bg-blue-50 text-gray-500 hover:text-blue-600 transition-all border-none outline-none focus:outline-none">
<Icon icon="ep:view" size="20" />
</button>
</el-tooltip>
<!-- 下载 -->
<el-tooltip content="下载" placement="top" v-if="item.fileUrl">
<button @click="handleDownload(item)" class="p-2 rounded-full hover:bg-green-50 text-gray-500 hover:text-green-600 transition-all border-none outline-none focus:outline-none">
<Icon icon="ep:download" size="20" />
</button>
</el-tooltip>
<!-- 历史版本 -->
<el-tooltip :content="'历史版本 (' + item.versionCount + ')'" placement="top" v-if="item.versionCount > 1">
<button @click="showVersionHistory(item)" class="p-2 rounded-full hover:bg-orange-50 text-gray-500 hover:text-orange-600 transition-all relative border-none outline-none focus:outline-none">
<Icon icon="ep:clock" size="20" />
<!-- 小红点提示 -->
<span class="absolute top-0.5 right-0.5 w-1.5 h-1.5 bg-orange-500 rounded-full border border-white"></span>
</button>
</el-tooltip>
</div>
</div>
</div>
<!-- Corner Status Mark -->
<div v-if="item.fileUrl" class="absolute -top-[1px] -right-[1px] w-0 h-0 border-t-[32px] border-r-[32px] border-t-transparent border-r-green-500 rounded-tr-xl">
<Icon icon="ep:check" class="absolute -right-[26px] -top-[26px] text-white text-xs"/>
</div>
</div>
</template>
<div v-if="finalMaterials.length === 0" class="col-span-2 text-center py-8 text-gray-400">
暂无终验材料
</div>
</div>
</div> </div>
</el-card>
</template> </template>
</div> </div>
</el-tab-pane> </el-tab-pane>
<!-- 审批记录 --> <!-- 审批记录 -->
<el-tab-pane label="审批记录" name="opinions"> <el-tab-pane label="审批记录" name="opinions">
<el-timeline> <div class="p-6">
<el-timeline class="pl-2">
<el-timeline-item <el-timeline-item
v-for="(opinion, index) in opinions" v-for="(opinion, index) in opinions"
:key="index" :key="index"
:type="getOpinionType(opinion.result)" :type="getOpinionType(opinion.result)"
:color="getOpinionColor(opinion.result)"
:timestamp="formatDate(opinion.createTime)" :timestamp="formatDate(opinion.createTime)"
placement="top" placement="top"
size="large"
> >
<el-card> <div class="bg-white border border-gray-200 rounded-lg p-4 shadow-sm hover:shadow-md transition-shadow relative">
<h4 class="mb-2"> <!-- Arrow -->
{{ opinion.reviewerRole || '审核人' }}{{ opinion.reviewerName || '未知' }} <div class="absolute w-3 h-3 bg-white border-l border-b border-gray-200 transform rotate-45 -left-[7px] top-4"></div>
<el-tag v-if="opinion.isLeader" type="warning" size="small" class="ml-1"></el-tag>
<el-tag :type="getOpinionType(opinion.result)" size="small" class="ml-2"> <div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<span class="font-semibold text-gray-900">{{ opinion.reviewerRole || '审核人' }}</span>
<span class="text-gray-500 text-sm">- {{ opinion.reviewerName || '未知' }}</span>
<el-tag v-if="opinion.isLeader" type="warning" size="small" effect="dark" class="rounded-full"></el-tag>
</div>
<el-tag :type="getOpinionType(opinion.result)" effect="plain" class="font-bold uppercase tracking-wider">
{{ getOpinionLabel(opinion.result) }} {{ getOpinionLabel(opinion.result) }}
</el-tag> </el-tag>
</h4> </div>
<p class="text-gray-600">{{ opinion.opinion || '无意见' }}</p>
<p class="text-gray-600 text-sm leading-relaxed mb-3 bg-gray-50 p-2 rounded">
{{ opinion.opinion || '无意见' }}
</p>
<div v-if="opinion.signatureUrl" class="border-t border-gray-100 pt-2 flex justify-end">
<el-image <el-image
v-if="opinion.signatureUrl"
:src="opinion.signatureUrl" :src="opinion.signatureUrl"
style="width: 150px; height: 60px; margin-top: 8px" class="h-10 w-auto opacity-80 hover:opacity-100"
fit="contain" fit="contain"
/> />
</el-card> </div>
</div>
</el-timeline-item> </el-timeline-item>
<el-empty v-if="opinions.length === 0" description="暂无审批记录" /> <el-empty v-if="opinions.length === 0" description="暂无审批记录" :image-size="100" />
</el-timeline> </el-timeline>
</div>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</div>
</div>
<!-- 操作按钮 --> <!-- 右侧操作区 (占据 1/3 宽度) -->
<div class="mt-6 flex justify-end gap-2" v-if="showActions"> <div class="lg:col-span-1 space-y-6">
<el-button @click="goBack"></el-button> <div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 sticky top-4">
<h3 class="text-lg font-bold text-gray-800 mb-4 flex items-center gap-2">
<Icon icon="ep:operation" class="text-blue-500" /> 快捷操作
</h3>
<div class="space-y-3 flex flex-col">
<el-button <el-button
v-if="canSubmitMaterial" v-if="canSubmitMaterial"
type="primary" type="primary"
size="large"
class="!w-full !justify-start !text-left !py-5"
@click="handleSubmitMaterial" @click="handleSubmitMaterial"
> >
提交材料 <Icon icon="ep:upload-filled" class="mr-2 text-xl" />
<div class="flex flex-col items-start leading-tight">
<span class="font-bold">提交材料</span>
<span class="text-xs opacity-80 font-normal">确认所有必填材料已上传</span>
</div>
</el-button> </el-button>
<el-button
v-if="canAudit"
type="success"
size="large"
class="!w-full !justify-start !text-left !py-5"
@click="openAuditDialog"
>
<Icon icon="ep:checked" class="mr-2 text-xl" />
<div class="flex flex-col items-start leading-tight">
<span class="font-bold">审核通过/驳回</span>
<span class="text-xs opacity-80 font-normal">对提交内容进行审批</span>
</div>
</el-button>
<el-button <el-button
v-if="canSubmitRectify" v-if="canSubmitRectify"
type="warning" type="warning"
size="large"
class="!w-full !justify-start !text-left !py-5"
@click="handleSubmitRectify" @click="handleSubmitRectify"
> >
提交整改 <Icon icon="ep:refresh-right" class="mr-2 text-xl" />
<div class="flex flex-col items-start leading-tight">
<span class="font-bold">提交整改</span>
<span class="text-xs opacity-80 font-normal">整改完成后再次提交</span>
</div>
</el-button> </el-button>
<el-button <el-button
v-if="canSubmitFinal" v-if="canSubmitFinal"
type="success" type="primary"
plain
size="large"
class="!w-full !justify-start !text-left !py-5"
@click="handleSubmitFinal" @click="handleSubmitFinal"
> >
提交终验申请 <Icon icon="ep:flag" class="mr-2 text-xl" />
<div class="flex flex-col items-start leading-tight">
<span class="font-bold">提交终验申请</span>
<span class="text-xs opacity-80 font-normal">初审通过进入终验</span>
</div>
</el-button> </el-button>
<el-button
v-if="canAudit" <el-divider class="!my-4" />
type="primary"
@click="openAuditDialog" <el-button @click="goBack" class="!w-full" plain>
> <Icon icon="ep:back" class="mr-1" /> 返回列表
<Icon icon="ep:check" class="mr-1" /> 审核
</el-button> </el-button>
</div> </div>
</div> </div>
</div>
</div>
</div>
<!-- 审核对话框 --> <!-- 审核对话框 -->
<el-dialog <el-dialog
@ -229,38 +473,111 @@
title="验收审核" title="验收审核"
width="500px" width="500px"
:close-on-click-modal="false" :close-on-click-modal="false"
class="rounded-xl overflow-hidden"
> >
<div class="p-2">
<el-form <el-form
ref="auditFormRef" ref="auditFormRef"
:model="auditForm" :model="auditForm"
:rules="auditFormRules" :rules="auditFormRules"
label-width="100px" label-width="100px"
label-position="top"
> >
<el-form-item label="审核结果" prop="result"> <el-form-item label="审核结果" prop="result">
<el-radio-group v-model="auditForm.result"> <div class="flex w-full gap-4">
<el-radio-button value="PASS"> <div
<Icon icon="ep:check" class="mr-1" /> 通过 class="flex-1 border rounded-lg p-4 cursor-pointer text-center transition-all hover:shadow-md"
</el-radio-button> :class="auditForm.result === 'PASS' ? 'border-green-500 bg-green-50 text-green-700' : 'border-gray-200 text-gray-500 hover:border-green-200'"
<el-radio-button value="REJECT"> @click="auditForm.result = 'PASS'"
<Icon icon="ep:close" class="mr-1" /> 驳回 >
</el-radio-button> <Icon icon="ep:circle-check-filled" class="text-3xl mb-2" :class="auditForm.result === 'PASS' ? 'text-green-500' : 'text-gray-300'" />
</el-radio-group> <div class="font-bold">通过</div>
</div>
<div
class="flex-1 border rounded-lg p-4 cursor-pointer text-center transition-all hover:shadow-md"
:class="auditForm.result === 'REJECT' ? 'border-red-500 bg-red-50 text-red-700' : 'border-gray-200 text-gray-500 hover:border-red-200'"
@click="auditForm.result = 'REJECT'"
>
<Icon icon="ep:circle-close-filled" class="text-3xl mb-2" :class="auditForm.result === 'REJECT' ? 'text-red-500' : 'text-gray-300'" />
<div class="font-bold">驳回</div>
</div>
</div>
</el-form-item> </el-form-item>
<el-form-item label="审核意见" prop="opinion"> <el-form-item label="审核意见" prop="opinion">
<el-input <el-input
v-model="auditForm.opinion" v-model="auditForm.opinion"
type="textarea" type="textarea"
:rows="4" :rows="4"
:placeholder="auditForm.result === 'REJECT' ? '请填写驳回理由...' : '请填写审核意见...'" :placeholder="auditForm.result === 'REJECT' ? '请务必填写驳回理由...' : '请填写审核意见(可选)...'"
class="!bg-gray-50"
/> />
</el-form-item> </el-form-item>
</el-form> </el-form>
</div>
<template #footer> <template #footer>
<div class="flex justify-end gap-2 pt-2">
<el-button @click="auditDialogVisible = false">取消</el-button> <el-button @click="auditDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleAudit"></el-button> <el-button type="primary" @click="handleAudit" class="px-6">确认提交</el-button>
</div>
</template> </template>
</el-dialog> </el-dialog>
</ContentWrap>
<!-- 历史版本对话框 -->
<el-dialog
v-model="versionHistoryVisible"
:title="'历史版本 - ' + currentMaterialName"
width="600px"
:close-on-click-modal="false"
class="rounded-xl overflow-hidden"
>
<div class="p-4 max-h-[60vh] overflow-y-auto">
<el-timeline>
<el-timeline-item
v-for="(version, index) in currentVersions"
:key="version.id"
:timestamp="formatDate(version.createTime)"
placement="top"
:type="index === 0 ? 'primary' : ''"
:hollow="index !== 0"
>
<div class="bg-gray-50 rounded-lg p-4 border border-gray-100 hover:shadow-sm transition-shadow">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<span class="font-bold text-gray-800">V{{ currentVersions.length - index }}</span>
<el-tag v-if="index === 0" size="small" type="success" effect="dark" class="rounded-full"></el-tag>
</div>
<div class="flex gap-2">
<el-button size="small" text bg type="primary" @click="handlePreview(version)">
<Icon icon="ep:view" class="mr-1" /> 预览
</el-button>
<el-button size="small" text bg type="success" @click="handleDownload(version)">
<Icon icon="ep:download" class="mr-1" /> 下载
</el-button>
</div>
</div>
<div class="text-sm text-gray-600 grid grid-cols-2 gap-2 mt-2">
<div class="flex items-center gap-1">
<Icon icon="ep:document" class="text-gray-400" />
<span class="truncate" :title="version.fileName">{{ version.fileName }}</span>
</div>
<div class="flex items-center gap-1">
<Icon icon="ep:data-line" class="text-gray-400" />
<span>{{ formatFileSize(version.fileSize) }}</span>
</div>
</div>
</div>
</el-timeline-item>
</el-timeline>
</div>
<template #footer>
<div class="flex justify-end pt-2">
<el-button @click="versionHistoryVisible = false">关闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -280,7 +597,8 @@ const router = useRouter()
const message = useMessage() const message = useMessage()
const loading = ref(true) const loading = ref(true)
const activeTab = ref('info') // tabtab
const activeTab = ref((route.query.tab as string) || 'info')
// //
const acceptance = ref<AcceptanceApi.AcceptanceVO>({} as AcceptanceApi.AcceptanceVO) const acceptance = ref<AcceptanceApi.AcceptanceVO>({} as AcceptanceApi.AcceptanceVO)
@ -289,11 +607,21 @@ const projectInfo = ref<any>({})
// //
const liaisonUserName = ref('') const liaisonUserName = ref('')
const serviceUserName = ref('') const serviceUserName = ref('')
// // 使
const materials = ref<any[]>([]) const materials = ref<any[]>([])
// 使
const preMaterials = ref<any[]>([])
// 使
const finalMaterials = ref<any[]>([])
// //
const opinions = ref<any[]>([]) const opinions = ref<any[]>([])
//
const isLiaisonUser = computed(() => {
const currentUserId = useUserStore().getUser.id
return projectInfo.value.liaisonUserId === currentUserId
})
// //
const uploadUrl = import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/infra/file/upload' const uploadUrl = import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/infra/file/upload'
@ -331,12 +659,18 @@ const getOpinionType = (result: string) => {
return 'info' return 'info'
} }
const getOpinionColor = (result: string) => {
if (result === 'PASS') return '#67C23A'
if (result === 'REJECT') return '#F56C6C'
return '#909399'
}
// //
// //
const canUploadMaterial = computed(() => { const canUploadMaterial = computed(() => {
const currentUserId = useUserStore().getUser.id const currentUserId = useUserStore().getUser.id
// // (05)(11)(30)(60)
return projectInfo.value.liaisonUserId === currentUserId && ['05', '11', '60'].includes(acceptance.value.status) return projectInfo.value.liaisonUserId === currentUserId && ['05', '11', '30', '60'].includes(acceptance.value.status)
}) })
const canSubmitMaterial = computed(() => { const canSubmitMaterial = computed(() => {
@ -358,7 +692,7 @@ const showActions = computed(() => {
return canSubmitMaterial.value || canSubmitRectify.value || canSubmitFinal.value || canAudit.value return canSubmitMaterial.value || canSubmitRectify.value || canSubmitFinal.value || canAudit.value
}) })
// (: 10, : 20) // (: 10, : 20, : 40)
const canAudit = computed(() => { const canAudit = computed(() => {
const currentUserId = useUserStore().getUser.id const currentUserId = useUserStore().getUser.id
const status = acceptance.value.status const status = acceptance.value.status
@ -370,6 +704,10 @@ const canAudit = computed(() => {
if (status === '20') { if (status === '20') {
return true return true
} }
// 40:
if (status === '40') {
return true
}
return false return false
}) })
@ -385,6 +723,27 @@ const auditFormRules = {
opinion: [{ required: true, message: '请填写审核意见', trigger: 'blur' }] opinion: [{ required: true, message: '请填写审核意见', trigger: 'blur' }]
} }
//
const versionHistoryVisible = ref(false)
const currentVersions = ref<any[]>([])
const currentMaterialName = ref('')
/** 显示历史版本 */
const showVersionHistory = (item: any) => {
currentMaterialName.value = item.materialName
currentVersions.value = item.versions || []
versionHistoryVisible.value = true
}
/** 格式化文件大小 */
const formatFileSize = (bytes: number) => {
if (!bytes) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
}
/** 打开审核对话框 */ /** 打开审核对话框 */
const openAuditDialog = () => { const openAuditDialog = () => {
auditForm.value = { result: '', opinion: '' } auditForm.value = { result: '', opinion: '' }
@ -398,11 +757,22 @@ const handleAudit = async () => {
if (!valid) return if (!valid) return
try { try {
await AcceptanceApi.auditPreAcceptance({ const auditData = {
acceptanceId: acceptance.value.id, acceptanceId: acceptance.value.id,
result: auditForm.value.result, result: auditForm.value.result,
opinion: auditForm.value.opinion opinion: auditForm.value.opinion
}) }
//
const status = acceptance.value.status
if (status === '40') {
//
await AcceptanceApi.auditFinalAdmin(auditData)
} else {
// 1020
await AcceptanceApi.auditPreAcceptance(auditData)
}
message.success('审核提交成功') message.success('审核提交成功')
auditDialogVisible.value = false auditDialogVisible.value = false
await getDetail() await getDetail()
@ -427,17 +797,18 @@ const getDetail = async () => {
} }
// //
// 1.
const defs = await AcceptanceMaterialDefApi.getListByType(acceptance.value.acceptanceType)
// 2. // 2.
const uploads = await AcceptanceMaterialApi.getListByAcceptanceId(id) as unknown as any[] const uploads = await AcceptanceMaterialApi.getListByAcceptanceId(id) as unknown as any[]
// 3. //
materials.value = defs.map((def: any) => { const mergeMaterials = (defs: any[]) => {
return defs.map((def: any) => {
// //
const allUploads = uploads.filter((u: any) => u.materialCode === def.materialCode) const allUploads = uploads.filter((u: any) => u.materialCode === def.materialCode)
// IDID // ID
const upload = allUploads.length > 0 ? allUploads.sort((a: any, b: any) => b.id - a.id)[0] : undefined const sortedUploads = allUploads.sort((a: any, b: any) => b.id - a.id)
//
const upload = sortedUploads.length > 0 ? sortedUploads[0] : undefined
return { return {
...def, ...def,
@ -447,9 +818,34 @@ const getDetail = async () => {
fileName: upload?.fileName, fileName: upload?.fileName,
fileSize: upload?.fileSize, fileSize: upload?.fileSize,
uploadId: upload?.id, uploadId: upload?.id,
acceptanceId: id acceptanceId: id,
//
versions: sortedUploads, //
versionCount: sortedUploads.length //
} }
}) })
}
//
const currentUserId = useUserStore().getUser.id
const isLiaison = projectInfo.value.liaisonUserId === currentUserId
if (isLiaison) {
//
// 1. 30
const materialType = acceptance.value.status === '30' ? 'FINAL' : acceptance.value.acceptanceType
const defs = await AcceptanceMaterialDefApi.getListByType(materialType)
// 3.
materials.value = mergeMaterials(defs)
} else {
//
// 1.
const preDefs = await AcceptanceMaterialDefApi.getListByType('PRE')
const finalDefs = await AcceptanceMaterialDefApi.getListByType('FINAL')
// 3.
preMaterials.value = mergeMaterials(preDefs)
finalMaterials.value = mergeMaterials(finalDefs)
}
// //
// //
@ -558,9 +954,16 @@ const handleSubmitRectify = async () => {
/** 提交终验申请 */ /** 提交终验申请 */
const handleSubmitFinal = async () => { const handleSubmitFinal = async () => {
try { try {
await message.confirm('确定提交终验申请吗?') //
const missing = materials.value.filter((m: any) => m.isRequired && !m.fileUrl)
if (missing.length > 0) {
message.warning(`请先上传终验必填材料:${missing.map((m: any) => m.materialName).join('、')}`)
return
}
await message.confirm('确定提交终验申请吗?提交后将进入终验审核流程。')
await AcceptanceApi.submitFinalAcceptance({ acceptanceId: acceptance.value.id }) await AcceptanceApi.submitFinalAcceptance({ acceptanceId: acceptance.value.id })
message.success('提交成功') message.success('终验申请提交成功')
await getDetail() await getDetail()
} catch {} } catch {}
} }
@ -616,14 +1019,36 @@ onMounted(() => {
/* 覆盖 el-steps 进行中状态的颜色为绿色 */ /* 覆盖 el-steps 进行中状态的颜色为绿色 */
:deep(.el-step__head.is-process) { :deep(.el-step__head.is-process) {
color: #67c23a; color: #3b82f6;
border-color: #67c23a; border-color: #3b82f6;
} }
:deep(.el-step__title.is-process) { :deep(.el-step__title.is-process) {
color: #67c23a; color: #3b82f6;
font-weight: bold; font-weight: bold;
} }
:deep(.el-step__description.is-process) { :deep(.el-step__description.is-process) {
color: #67c23a; color: #3b82f6;
} }
/* Custom Tabs Styling */
:deep(.el-tabs__header) {
margin-bottom: 0;
border-bottom: 1px solid #f3f4f6;
background-color: #f9fafb;
}
:deep(.el-tabs__item) {
height: 50px;
font-size: 15px;
font-weight: 500;
color: #6b7280;
}
:deep(.el-tabs__item.is-active) {
color: #2563eb;
background-color: #fff;
border-top: 2px solid #2563eb;
}
:deep(.el-tabs__nav-wrap::after) {
height: 0;
}
</style> </style>

View File

@ -1,4 +1,5 @@
<template> <template>
<div class="p-4">
<ContentWrap> <ContentWrap>
<!-- 搜索工作栏 --> <!-- 搜索工作栏 -->
<el-form <el-form
@ -6,7 +7,7 @@
:model="queryParams" :model="queryParams"
ref="queryFormRef" ref="queryFormRef"
:inline="true" :inline="true"
label-width="80px" label-width="68px"
> >
<el-form-item label="项目ID" prop="projectId"> <el-form-item label="项目ID" prop="projectId">
<el-input <el-input
@ -14,7 +15,7 @@
placeholder="请输入项目ID" placeholder="请输入项目ID"
clearable clearable
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
class="!w-180px" class="!w-200px"
/> />
</el-form-item> </el-form-item>
<el-form-item label="验收类型" prop="acceptanceType"> <el-form-item label="验收类型" prop="acceptanceType">
@ -22,7 +23,7 @@
v-model="queryParams.acceptanceType" v-model="queryParams.acceptanceType"
placeholder="请选择验收类型" placeholder="请选择验收类型"
clearable clearable
class="!w-180px" class="!w-200px"
> >
<el-option label="预验收" value="PRE" /> <el-option label="预验收" value="PRE" />
<el-option label="终验" value="FINAL" /> <el-option label="终验" value="FINAL" />
@ -33,14 +34,14 @@
v-model="queryParams.status" v-model="queryParams.status"
placeholder="请选择验收状态" placeholder="请选择验收状态"
clearable clearable
class="!w-180px" class="!w-200px"
> >
<el-option label="待审核" value="10" /> <el-option
<el-option label="审核中" value="20" /> v-for="item in statusList"
<el-option label="待会议" value="30" /> :key="item.code"
<el-option label="待整改" value="40" /> :label="item.name"
<el-option label="已通过" value="80" /> :value="item.code"
<el-option label="已驳回" value="90" /> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="轮次" prop="round"> <el-form-item label="轮次" prop="round">
@ -48,22 +49,17 @@
v-model="queryParams.round" v-model="queryParams.round"
:min="1" :min="1"
placeholder="轮次" placeholder="轮次"
controls-position="right"
class="!w-120px" class="!w-120px"
/> />
</el-form-item> </el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="datetimerange"
start-placeholder="开始日期"
end-placeholder="结束日期"
class="!w-240px"
/>
</el-form-item>
<el-form-item> <el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" />搜索</el-button> <el-button type="primary" plain @click="handleQuery">
<el-button @click="resetQuery"><Icon icon="ep:refresh" />重置</el-button> <Icon icon="ep:search" class="mr-1" />搜索
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-1" />重置
</el-button>
<el-button <el-button
type="success" type="success"
plain plain
@ -71,84 +67,100 @@
:loading="exportLoading" :loading="exportLoading"
v-hasPermi="['project:acceptance:export']" v-hasPermi="['project:acceptance:export']"
> >
<Icon icon="ep:download" />导出 <Icon icon="ep:download" class="mr-1" />导出
</el-button> </el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</ContentWrap> </ContentWrap>
<!-- 列表 --> <!-- 列表 -->
<ContentWrap> <ContentWrap class="mt-4">
<el-table v-loading="loading" :data="list"> <el-table
<el-table-column label="验收ID" align="center" prop="id" width="80" /> v-loading="loading"
<el-table-column label="项目ID" align="center" prop="projectId" width="80" /> :data="list"
<el-table-column label="验收类型" align="center" prop="acceptanceType" width="100"> :header-cell-style="{ background: '#f8fbfc', color: '#606266', height: '50px' }"
:row-style="{ height: '60px' }"
>
<el-table-column label="ID" align="center" prop="id" width="80" class-name="font-mono text-gray-500" />
<el-table-column label="轮次" align="center" prop="round" width="80">
<template #default="scope"> <template #default="scope">
<el-tag v-if="scope.row.acceptanceType === 'PRE'" type="info"></el-tag> <span class="font-bold text-gray-600 bg-gray-100 px-2 py-1 rounded">R{{ scope.row.round }}</span>
<el-tag v-else-if="scope.row.acceptanceType === 'FINAL'" type="primary">终验</el-tag> </template>
</el-table-column>
<el-table-column label="项目ID" align="center" prop="projectId" width="100" />
<el-table-column label="验收类型" align="center" prop="acceptanceType" width="120">
<template #default="scope">
<el-tag v-if="scope.row.acceptanceType === 'PRE'" type="info" effect="plain" class="!border-gray-300 !text-gray-600">
<span class="dot bg-gray-400 mr-1"></span>预验收
</el-tag>
<el-tag v-else-if="scope.row.acceptanceType === 'FINAL'" type="primary" effect="plain" class="!border-blue-200 !text-blue-600">
<span class="dot bg-blue-500 mr-1"></span>终验
</el-tag>
<el-tag v-else>{{ scope.row.acceptanceType }}</el-tag> <el-tag v-else>{{ scope.row.acceptanceType }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="状态" align="center" prop="status" width="100">
<el-table-column label="状态" align="center" prop="status" width="140">
<template #default="scope"> <template #default="scope">
<el-tag :type="getStatusType(scope.row.status)">{{ getStatusLabel(scope.row.status) }}</el-tag> <div class="flex items-center justify-center">
<el-tag
:type="getStatusType(scope.row.status)"
effect="light"
round
class="!border-0 !px-3 font-medium"
>
{{ getStatusLabel(scope.row.status) }}
</el-tag>
</div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="轮次" align="center" prop="round" width="80" />
<el-table-column label="流程实例ID" align="center" prop="processInstanceId" :show-overflow-tooltip="true" /> <el-table-column label="时间信息" min-width="250">
<el-table-column
label="启动时间"
align="center"
prop="startTime"
:formatter="dateFormatter"
width="180"
/>
<el-table-column
label="完成时间"
align="center"
prop="endTime"
:formatter="dateFormatter"
width="180"
/>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180"
/>
<el-table-column label="操作" align="center" width="200" fixed="right">
<template #default="scope"> <template #default="scope">
<el-button <div class="flex flex-col gap-1 text-xs text-gray-500">
link <div class="flex items-center">
type="primary" <span class="w-16 text-right mr-2">启动时间:</span>
@click="handleView(scope.row)" <span class="text-gray-700">{{ scope.row.startTime ? dateFormatter(scope.row, null, scope.row.startTime) : '-' }}</span>
> </div>
查看 <div v-if="scope.row.endTime" class="flex items-center">
<span class="w-16 text-right mr-2">完成时间:</span>
<span class="text-gray-700">{{ scope.row.endTime ? dateFormatter(scope.row, null, scope.row.endTime) : '-' }}</span>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="260" fixed="right">
<template #default="scope">
<el-button link type="primary" @click="handleView(scope.row)">
<Icon icon="ep:view" class="mr-1" /> 查看
</el-button> </el-button>
<el-button <el-button
v-if="canAudit(scope.row)"
link link
type="primary" type="primary"
@click="openAuditForm(scope.row)" @click="openAuditForm(scope.row)"
v-if="canAudit(scope.row)" v-hasRole="['acceptance-admin']"
> >
审核 <Icon icon="ep:check" class="mr-1" /> 审核
</el-button> </el-button>
<el-button <el-button
link link
type="warning" type="warning"
@click="handleForceArchive(scope.row)" @click="handleForceArchive(scope.row)"
v-hasPermi="['project:acceptance:force-archive']" v-hasPermi="['project:acceptance:force-archive']"
v-hasRole="['acceptance-admin']"
> >
强制归档 <Icon icon="ep:folder-checked" class="mr-1" /> 归档
</el-button> </el-button>
<el-button <el-button
link link
type="danger" type="danger"
@click="handleCancel(scope.row)" @click="handleCancel(scope.row)"
v-hasPermi="['project:acceptance:cancel']" v-hasRole="['acceptance-admin']"
> >
取消 <Icon icon="ep:circle-close" class="mr-1" /> 取消
</el-button> </el-button>
</template> </template>
</el-table-column> </el-table-column>
@ -164,8 +176,18 @@
<!-- 审核表单弹窗 --> <!-- 审核表单弹窗 -->
<AcceptanceAuditForm ref="auditFormRef" @success="getList" /> <AcceptanceAuditForm ref="auditFormRef" @success="getList" />
</div>
</template> </template>
<style scoped>
.dot {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
}
</style>
<script lang="ts" setup> <script lang="ts" setup>
import { dateFormatter } from '@/utils/formatTime' import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download' import download from '@/utils/download'
@ -193,22 +215,48 @@ const queryParams = reactive({
const queryFormRef = ref() // const queryFormRef = ref() //
const exportLoading = ref(false) // const exportLoading = ref(false) //
// //
const statusMap: Record<string, { label: string; type: string }> = { const statusList = ref<{ code: string; name: string }[]>([])
'10': { label: '待审核', type: 'info' }, const statusMap = ref<Record<string, string>>({})
'20': { label: '审核中', type: 'warning' },
'30': { label: '待会议', type: '' }, /** 获取状态列表 */
'40': { label: '待整改', type: 'danger' }, const getStatusList = async () => {
'80': { label: '已通过', type: 'success' }, try {
'90': { label: '已驳回', type: 'danger' } const data = await AcceptanceApi.getStatusList()
statusList.value = data
// statusMap
statusMap.value = {}
data.forEach((item: { code: string; name: string }) => {
statusMap.value[item.code] = item.name
})
} catch (error) {
console.error('获取状态列表失败', error)
}
} }
const getStatusLabel = (status: string) => { const getStatusLabel = (status: string) => {
return statusMap[status]?.label || status return statusMap.value[status] || status
}
// tag
const statusTypeMap: Record<string, string> = {
'00': 'info',
'05': 'warning',
'10': 'primary',
'11': 'danger',
'20': 'primary',
'30': 'info',
'40': 'primary',
'50': 'info',
'60': 'danger',
'61': 'warning',
'62': 'warning',
'98': 'info',
'99': 'success'
} }
const getStatusType = (status: string) => { const getStatusType = (status: string) => {
return statusMap[status]?.type || 'info' return statusTypeMap[status] || 'info'
} }
/** 判断是否可以审核 */ /** 判断是否可以审核 */
@ -297,7 +345,8 @@ const handleExport = async () => {
} }
/** 初始化 **/ /** 初始化 **/
onMounted(() => { onMounted(async () => {
await getStatusList()
getList() getList()
}) })
</script> </script>

View File

@ -81,22 +81,27 @@ const getList = async () => {
const auditFormRef = ref() const auditFormRef = ref()
const handleProcess = (row: AcceptanceApi.AcceptanceTodoVO) => { const handleProcess = (row: AcceptanceApi.AcceptanceTodoVO) => {
// //
// taskType BPMN taskDefinitionKey
const taskType = row.taskType const taskType = row.taskType
if (['LIAISON_REVIEW', 'LEADER_REVIEW', 'ADMIN_REVIEW', 'EXPERT_CHECK'].includes(taskType)) {
// //
if (['task_pre_liaison_review', 'task_pre_leader_review', 'task_expert_check', 'task_rectify_review', 'task_meeting_review'].includes(taskType)) {
auditFormRef.value.open({ auditFormRef.value.open({
id: row.acceptanceId, id: row.acceptanceId,
acceptanceType: row.acceptanceType, acceptanceType: row.acceptanceType,
status: row.status status: row.status
}, getAuditType(taskType)) }, getAuditType(taskType))
} else if (taskType === 'RECTIFY_SUBMIT' || taskType === 'task_pre_rectify' || taskType === 'task_final_rectify') { }
// tab //
else if (taskType === 'task_final_admin_review') {
router.push({ path: '/project/acceptance/detail/' + row.acceptanceId, query: { tab: 'info' } })
}
// tab
else if (['task_liaison_submit', 'task_pre_rectify', 'task_final_rectify', 'task_final_apply'].includes(taskType)) {
router.push({ path: '/project/acceptance/detail/' + row.acceptanceId, query: { tab: 'materials' } }) router.push({ path: '/project/acceptance/detail/' + row.acceptanceId, query: { tab: 'materials' } })
} else if (taskType === 'LIAISON_SUBMIT' || taskType === 'task_liaison_submit') { }
// tab
router.push({ path: '/project/acceptance/detail/' + row.acceptanceId, query: { tab: 'materials' } })
} else {
// //
else {
handleView(row) handleView(row)
} }
} }
@ -104,12 +109,16 @@ const handleProcess = (row: AcceptanceApi.AcceptanceTodoVO) => {
/** 获取审核类型 */ /** 获取审核类型 */
const getAuditType = (taskType: string) => { const getAuditType = (taskType: string) => {
switch (taskType) { switch (taskType) {
case 'LIAISON_REVIEW': case 'task_pre_liaison_review': //
case 'LEADER_REVIEW': case 'task_pre_leader_review': //
return 'pre-audit' return 'pre-audit'
case 'ADMIN_REVIEW': case 'task_final_admin_review': //
return 'final-admin' return 'final-admin'
case 'EXPERT_CHECK': case 'task_meeting_review': //
return 'meeting-review'
case 'task_rectify_review': //
return 'rectify-review'
case 'task_expert_check': //
return 'expert-check' return 'expert-check'
default: default:
return 'pre-audit' return 'pre-audit'