Files
school-developer/src/views/purchase/purchasingrequisition/docProcess/DocProcessDialog.vue
2026-03-01 21:55:33 +08:00

520 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>