更新采购申请

This commit is contained in:
吴红兵
2026-03-01 21:55:33 +08:00
parent f2df01c38e
commit 992e9f5a3e
14 changed files with 837 additions and 948 deletions

View File

@@ -0,0 +1,520 @@
<template>
<el-dialog
v-model="visible"
:title="dialogTitle"
width="900px"
destroy-on-close
:close-on-click-modal="false"
@close="handleClose">
<el-tabs v-model="activeTab">
<!-- 采购需求文件 -->
<el-tab-pane label="采购需求文件" name="requirement">
<el-table :data="requirementFiles" stripe v-loading="requirementLoading">
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column prop="fileTitle" label="文件名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="fileType" label="文件类型" width="120">
<template #default="scope">
{{ getFileTypeLabel(scope.row.fileType) }}
</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center">
<template #default="scope">
<el-button type="primary" link icon="Download" @click="handleDownloadRequirement(scope.row)">
下载
</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!requirementLoading && requirementFiles.length === 0" description="暂无采购需求文件" />
</el-tab-pane>
<!-- 招标文件 -->
<el-tab-pane label="招标文件" name="doc">
<div class="doc-header">
<!-- 状态显示 -->
<el-tag :type="getStatusType(statusField)" class="mr-4">
{{ getStatusLabel(statusField) }}
</el-tag>
<!-- 上传按钮 - 仅招标代理且可上传时显示 -->
<el-upload
v-if="canUpload"
ref="uploadRef"
:action="uploadAction"
:headers="uploadHeaders"
:data="uploadData"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:before-upload="beforeUpload"
:on-change="handleFileChange"
:file-list="fileList"
:auto-upload="false"
:limit="1"
:show-file-list="false"
accept=".doc,.docx,.pdf">
<el-button type="primary" icon="Upload">
{{ isReturned ? '重新上传' : '上传文件' }}
</el-button>
</el-upload>
<!-- 提交按钮 - 有文件时显示 -->
<el-button
v-if="canUpload && fileList.length > 0"
type="success"
:loading="uploadSubmitting"
@click="handleUploadSubmit"
class="ml-2">
{{ isReturned ? '重新上传并提交' : '上传并提交' }}
</el-button>
</div>
<!-- 文件列表 -->
<el-table :data="docList" stripe v-loading="docLoading">
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column prop="fileName" label="文件名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="version" label="版本" width="80" align="center" />
<el-table-column prop="uploadByName" label="上传人" width="100" />
<el-table-column prop="uploadTime" label="上传时间" width="160" />
<el-table-column label="状态" width="120" align="center">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)" size="small">
{{ getStatusLabel(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center">
<template #default="scope">
<el-button type="primary" link icon="Download" @click="handleDownloadDoc(scope.row)">
下载
</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!docLoading && docList.length === 0" description="暂无招标文件" />
<!-- 状态提示 -->
<el-alert v-if="mode === 'agent' && !canUpload" type="warning" :closable="false" class="mt-4">
<template #title>
<span v-if="isReviewing">文件审核中请等待审核结果</span>
<span v-else-if="isConfirming">文件审核中请等待确认</span>
<span v-else-if="isCompleted">文件审核已完成</span>
<span v-else>当前状态不允许上传</span>
</template>
</el-alert>
</el-tab-pane>
<!-- 审核记录 -->
<el-tab-pane label="审核记录" name="audit">
<AuditRecordList :apply-id="applyId" ref="auditRecordListRef" />
</el-tab-pane>
</el-tabs>
<!-- 操作区域 -->
<template #footer>
<div class="dialog-footer">
<el-button v-if="canConfirm" type="success" @click="handleConfirm">确认无误</el-button>
<el-button v-if="canReturn" type="warning" @click="handleReturn">退回修改</el-button>
<el-button v-if="canComplete" type="primary" @click="handleComplete">确认流程结束</el-button>
<el-button @click="handleClose">关闭</el-button>
</div>
</template>
<!-- 退回原因弹窗 -->
<el-dialog v-model="returnDialogVisible" title="退回原因" width="400px" append-to-body>
<el-form>
<el-form-item label="退回原因">
<el-input v-model="returnRemark" type="textarea" :rows="3" placeholder="请输入退回原因" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="returnDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitReturn">确定</el-button>
</template>
</el-dialog>
</el-dialog>
</template>
<script setup lang="ts" name="DocProcessDialog">
import { ref, computed, defineAsyncComponent } from 'vue'
import { useMessage, useMessageBox } from '/@/hooks/message'
import { Session } from '/@/utils/storage'
import {
getRequirementFiles,
getDocList,
uploadDoc,
reuploadDoc,
confirmDoc,
returnDoc,
completeDoc,
getAvailableActions,
downloadDocById,
downloadFileById
} from '/@/api/purchase/docProcess'
import type { UploadInstance, UploadProps, UploadUserFile } from 'element-plus'
const AuditRecordList = defineAsyncComponent(() => import('./AuditRecordList.vue'))
const props = defineProps<{
mode: 'agent' | 'audit'
}>()
const emit = defineEmits(['refresh'])
const visible = ref(false)
const activeTab = ref('requirement')
const applyId = ref<string | number>('')
const rowData = ref<any>({})
const requirementFiles = ref<any[]>([])
const requirementLoading = ref(false)
const docList = ref<any[]>([])
const docLoading = ref(false)
const auditRecordListRef = ref()
const availableActions = ref<string[]>([])
// 上传相关
const uploadRef = ref<UploadInstance>()
const fileList = ref<UploadUserFile[]>([])
const uploadSubmitting = ref(false)
// 退回相关
const returnDialogVisible = ref(false)
const returnRemark = ref('')
// 弹窗标题
const dialogTitle = computed(() => {
return props.mode === 'agent' ? `处理项目 - ${rowData.value.purchaseNo || ''}` : '招标文件审核'
})
// 状态字段(两个模式使用不同字段名)
const statusField = computed(() => {
return props.mode === 'agent' ? rowData.value.status : rowData.value.docAuditStatus
})
// 是否可以上传
const canUpload = computed(() => {
if (props.mode !== 'agent') return false
const status = statusField.value
return status === 'PENDING_UPLOAD' || status === 'RETURNED'
})
// 是否可确认
const canConfirm = computed(() => availableActions.value.includes('confirm'))
// 是否可退回
const canReturn = computed(() => availableActions.value.includes('return'))
// 是否可完成
const canComplete = computed(() => availableActions.value.includes('complete'))
// 状态快捷判断
const isReturned = computed(() => statusField.value === 'RETURNED')
const isReviewing = computed(() => ['ASSET_REVIEWING', 'DEPT_REVIEWING', 'AUDIT_REVIEWING'].includes(statusField.value))
const isConfirming = computed(() => statusField.value === 'ASSET_CONFIRMING')
const isCompleted = computed(() => statusField.value === 'COMPLETED')
// 上传配置
const uploadAction = computed(() => {
const baseUrl = import.meta.env.VITE_API_URL || ''
return `${baseUrl}/purchase/purchasingfiles/upload`
})
const uploadHeaders = computed(() => {
const token = Session.getToken()
return {
Authorization: `Bearer ${token}`,
'TENANT-ID': Session.getTenant() || '1'
}
})
const uploadData = computed(() => ({
fileType: '130', // 招标文件类型
purchaseId: applyId.value || ''
}))
const open = async (row: any) => {
visible.value = true
activeTab.value = 'requirement'
rowData.value = { ...row }
requirementFiles.value = []
docList.value = []
returnRemark.value = ''
fileList.value = []
// 获取申请ID兼容 id 和 applyId 两种字段名)
applyId.value = row.applyId || row.id
// 加载可执行操作
try {
const actionsRes = await getAvailableActions(applyId.value)
availableActions.value = actionsRes.data || []
} catch (e) {
availableActions.value = []
}
// 加载采购需求文件
loadRequirementFiles()
// 加载招标文件
loadDocList()
}
const loadRequirementFiles = async () => {
if (!applyId.value) return
requirementLoading.value = true
try {
const res = await getRequirementFiles(applyId.value)
requirementFiles.value = res.data || []
} catch (e) {
requirementFiles.value = []
} finally {
requirementLoading.value = false
}
}
const loadDocList = async () => {
if (!applyId.value) return
docLoading.value = true
try {
const res = await getDocList(applyId.value)
docList.value = res.data || []
} catch (e) {
docList.value = []
} finally {
docLoading.value = false
}
}
const handleDownloadRequirement = async (row: any) => {
try {
const res = await downloadFileById(row.id)
const fileName = row.fileName || row.fileTitle || 'download'
const blob = new Blob([res])
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = fileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
} catch (e: any) {
useMessage().error(e?.msg || '下载失败')
}
}
const handleDownloadDoc = async (row: any) => {
try {
const res = await downloadDocById(row.id)
const fileName = row.fileName || 'download'
const blob = new Blob([res])
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = fileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
} catch (e: any) {
useMessage().error(e?.msg || '下载失败')
}
}
const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
const allowedTypes = ['application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/pdf']
const allowedExtensions = ['.doc', '.docx', '.pdf']
const fileExt = rawFile.name.substring(rawFile.name.lastIndexOf('.')).toLowerCase()
if (!allowedTypes.includes(rawFile.type) && !allowedExtensions.includes(fileExt)) {
useMessage().error('只能上传 .doc, .docx, .pdf 格式的文件')
return false
}
if (rawFile.size / 1024 / 1024 > 50) {
useMessage().error('文件大小不能超过 50MB')
return false
}
return true
}
const handleFileChange: UploadProps['onChange'] = (uploadFile, uploadFiles) => {
fileList.value = uploadFiles
}
const handleUploadSuccess: UploadProps['onSuccess'] = async (response: any, uploadFile: any) => {
if (response?.code === 0 || response?.code === 200) {
const fileData = response.data
try {
const submitData = {
applyId: applyId.value,
fileName: fileData.fileTitle || uploadFile.name,
filePath: fileData.remark || fileData.filePath
}
let submitRes
if (isReturned.value) {
submitRes = await reuploadDoc(submitData)
} else {
submitRes = await uploadDoc(submitData)
}
if (submitRes?.code === 0 || submitRes?.code === 200) {
useMessage().success('招标文件提交成功')
emit('refresh')
await loadDocList()
fileList.value = []
// 重新加载可执行操作
const actionsRes = await getAvailableActions(applyId.value)
availableActions.value = actionsRes.data || []
} else {
useMessage().error(submitRes?.msg || '提交失败')
}
} catch (e: any) {
useMessage().error(e?.msg || '提交失败')
} finally {
uploadSubmitting.value = false
}
} else {
useMessage().error(response?.msg || '上传失败')
uploadSubmitting.value = false
}
}
const handleUploadError: UploadProps['onError'] = (error: any) => {
useMessage().error('文件上传失败:' + (error?.message || '未知错误'))
uploadSubmitting.value = false
}
const handleUploadSubmit = async () => {
if (fileList.value.length === 0) {
useMessage().warning('请先选择文件')
return
}
uploadSubmitting.value = true
uploadRef.value?.submit()
}
const handleConfirm = async () => {
try {
await useMessageBox().confirm('确定要确认该招标文件无误吗?')
} catch {
return
}
try {
await confirmDoc({ applyId: applyId.value })
useMessage().success('确认成功')
emit('refresh')
loadDocList()
auditRecordListRef.value?.refresh()
// 重新加载可执行操作
const actionsRes = await getAvailableActions(applyId.value)
availableActions.value = actionsRes.data || []
} catch (e: any) {
useMessage().error(e?.msg || '确认失败')
}
}
const handleReturn = () => {
returnRemark.value = ''
returnDialogVisible.value = true
}
const submitReturn = async () => {
try {
await returnDoc({ applyId: applyId.value, remark: returnRemark.value })
useMessage().success('退回成功')
returnDialogVisible.value = false
emit('refresh')
loadDocList()
auditRecordListRef.value?.refresh()
// 重新加载可执行操作
const actionsRes = await getAvailableActions(applyId.value)
availableActions.value = actionsRes.data || []
} catch (e: any) {
useMessage().error(e?.msg || '退回失败')
}
}
const handleComplete = async () => {
try {
await useMessageBox().confirm('确定要确认流程结束吗?')
} catch {
return
}
try {
await completeDoc(applyId.value)
useMessage().success('流程已结束')
emit('refresh')
loadDocList()
auditRecordListRef.value?.refresh()
// 重新加载可执行操作
const actionsRes = await getAvailableActions(applyId.value)
availableActions.value = actionsRes.data || []
} catch (e: any) {
useMessage().error(e?.msg || '操作失败')
}
}
const handleClose = () => {
visible.value = false
}
const getStatusType = (status: string) => {
const typeMap: Record<string, string> = {
'PENDING_UPLOAD': 'info',
'ASSET_REVIEWING': 'warning',
'DEPT_REVIEWING': 'warning',
'AUDIT_REVIEWING': 'warning',
'ASSET_CONFIRMING': 'primary',
'COMPLETED': 'success',
'RETURNED': 'danger'
}
return typeMap[status] || 'info'
}
const getStatusLabel = (status: string) => {
const labelMap: Record<string, string> = {
'PENDING_UPLOAD': '待上传',
'ASSET_REVIEWING': '资产管理处审核中',
'DEPT_REVIEWING': '需求部门审核中',
'AUDIT_REVIEWING': '内审部门审核中',
'ASSET_CONFIRMING': '资产管理处确认中',
'COMPLETED': '已完成',
'RETURNED': '已退回'
}
return labelMap[status] || '-'
}
const getFileTypeLabel = (type: string) => {
const labelMap: Record<string, string> = {
'120': '采购需求表',
'130': '招标文件'
}
return labelMap[type] || type
}
defineExpose({ open })
</script>
<style scoped lang="scss">
.doc-header {
margin-bottom: 16px;
display: flex;
align-items: center;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.ml-2 {
margin-left: 8px;
}
.mr-4 {
margin-right: 16px;
}
.mt-4 {
margin-top: 16px;
}
</style>