1218 lines
40 KiB
Vue
1218 lines
40 KiB
Vue
<template>
|
||
<el-dialog
|
||
v-model="visible"
|
||
:title="dialogTitle"
|
||
width="85%"
|
||
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="fileName" 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-tag v-if="isDraft" type="info" class="mr-4">草稿</el-tag>
|
||
<!-- 上传按钮 - 招标代理模式 -->
|
||
<template v-if="canSaveDraft || canSubmitDraft || canReupload">
|
||
<el-upload
|
||
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">
|
||
{{ canReupload ? '重新上传' : '上传文件' }}
|
||
</el-button>
|
||
</el-upload>
|
||
</template>
|
||
<!-- 保存草稿按钮 -->
|
||
<!-- <el-button-->
|
||
<!-- v-if="canSaveDraft && fileList.length > 0"-->
|
||
<!-- type="info"-->
|
||
<!-- :loading="uploadSubmitting"-->
|
||
<!-- @click="handleSaveDraft"-->
|
||
<!-- class="ml-2">-->
|
||
<!-- 保存草稿-->
|
||
<!-- </el-button>-->
|
||
<!-- 提交审核按钮 -->
|
||
<el-button
|
||
v-if="canSubmitDraft && fileList.length > 0"
|
||
type="success"
|
||
:loading="uploadSubmitting"
|
||
@click="handleSubmitDraft"
|
||
class="ml-2">
|
||
提交审核
|
||
</el-button>
|
||
<!-- 重新上传并提交按钮 -->
|
||
<el-button
|
||
v-if="canReupload && fileList.length > 0"
|
||
type="success"
|
||
:loading="uploadSubmitting"
|
||
@click="handleReuploadSubmit"
|
||
class="ml-2">
|
||
重新上传并提交
|
||
</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 prop="fileRemark" label="文件意见" min-width="150" show-overflow-tooltip />
|
||
<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' && !canSaveDraft && !canSubmitDraft && !canReupload" 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-tab-pane v-if="canSetReviewer" label="采购代表设置" name="reviewer">
|
||
<ReviewerSetting :apply-id="applyId" @saved="handleReviewerSaved" />
|
||
</el-tab-pane>
|
||
|
||
<el-tab-pane v-if="rowData.representorName" label="学校招标参与人">
|
||
<el-tag type="info">{{rowData.representorName}} ({{rowData.representorType}})</el-tag>
|
||
</el-tab-pane>
|
||
|
||
|
||
|
||
|
||
<!-- 开标通知 - 仅招标代理模式且状态为已完成时显示 -->
|
||
<el-tab-pane v-if="showBidOpeningNotice" label="开标通知" name="bidOpening">
|
||
<div class="bid-opening-form" v-loading="bidOpeningLoading">
|
||
<el-form :model="bidOpeningForm" :rules="bidOpeningRules" ref="bidOpeningFormRef" label-width="120px">
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="项目名称">
|
||
<el-input v-model="rowData.projectName" disabled />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="开标时间" prop="openTime">
|
||
<el-date-picker
|
||
v-model="bidOpeningForm.openTime"
|
||
type="datetime"
|
||
placeholder="请选择开标时间"
|
||
style="width: 100%"
|
||
value-format="YYYY-MM-DD HH:mm:ss" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="开标地点" prop="openLocation">
|
||
<el-input v-model="bidOpeningForm.openLocation" placeholder="请输入开标地点" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="公示网址">
|
||
<el-input v-model="bidOpeningForm.publicUrl" placeholder="请输入公示网址" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="代理联系人" prop="agentContactName">
|
||
<el-input v-model="bidOpeningForm.agentContactName" placeholder="请输入代理联系人姓名" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="联系电话" prop="agentContactPhone">
|
||
<el-input v-model="bidOpeningForm.agentContactPhone" placeholder="请输入代理联系人电话" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="招标确认函">
|
||
<el-upload
|
||
:action="uploadAction"
|
||
:headers="uploadHeaders"
|
||
:data="{ fileType: '131', purchaseId: applyId }"
|
||
:on-success="(res: any, file: any) => handleBidFileSuccess(res, file, 'bidConfirmationLetter')"
|
||
:before-upload="beforeUpload"
|
||
:show-file-list="false"
|
||
accept=".doc,.docx,.pdf,.jpg,.jpeg,.png">
|
||
<el-button type="primary" icon="Upload">上传文件</el-button>
|
||
</el-upload>
|
||
<div v-if="bidOpeningForm.bidConfirmationLetter" class="file-info">
|
||
<el-icon><Document /></el-icon>
|
||
<span>已上传</span>
|
||
<el-button type="primary" link @click="viewFile(bidOpeningForm.bidConfirmationLetter)">查看</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="授权书">
|
||
<el-upload
|
||
:action="uploadAction"
|
||
:headers="uploadHeaders"
|
||
:data="{ fileType: '132', purchaseId: applyId }"
|
||
:on-success="(res: any, file: any) => handleBidFileSuccess(res, file, 'authorizationLetter')"
|
||
:before-upload="beforeUpload"
|
||
:show-file-list="false"
|
||
accept=".doc,.docx,.pdf,.jpg,.jpeg,.png">
|
||
<el-button type="primary" icon="Upload">上传文件</el-button>
|
||
</el-upload>
|
||
<div v-if="bidOpeningForm.authorizationLetter" class="file-info">
|
||
<el-icon><Document /></el-icon>
|
||
<span>已上传</span>
|
||
<el-button type="primary" link @click="viewFile(bidOpeningForm.authorizationLetter)">查看</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="24">
|
||
<el-form-item label="备注">
|
||
<el-input v-model="bidOpeningForm.remarks" type="textarea" :rows="3" placeholder="请输入备注" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row>
|
||
<el-col :span="24" class="form-actions">
|
||
<el-button type="info" :loading="bidOpeningSaving" @click="handleSaveBidOpeningDraft">保存草稿</el-button>
|
||
<el-button type="success" :loading="bidOpeningPublishing" @click="handlePublishBidOpening">发布通知</el-button>
|
||
</el-col>
|
||
</el-row>
|
||
</el-form>
|
||
<!-- 已发布提示 -->
|
||
<el-alert v-if="bidOpeningNoticeStatus === 'PUBLISHED'" type="success" :closable="false" class="mt-4">
|
||
<template #title>
|
||
<span>开标通知已于 {{ bidOpeningForm.publishTime }} 发布</span>
|
||
</template>
|
||
</el-alert>
|
||
</div>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
|
||
<!-- 操作区域 -->
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<!-- 资产管理处操作按钮 -->
|
||
<el-button v-if="canSubmitToDept" type="primary" @click="handleSubmitToDept">提交至需求部门</el-button>
|
||
<el-button v-if="canSubmitToAudit" type="primary" @click="handleSubmitToAudit">提交至内审部门</el-button>
|
||
<el-button v-if="canSupplyUpload" type="info" @click="handleSupplyUpload">补充上传</el-button>
|
||
<el-button v-if="canFinalize" type="success" @click="handleFinalize">定稿</el-button>
|
||
<!-- 需求部门/内审部门操作按钮 -->
|
||
<el-button v-if="canSubmitToAsset" type="primary" @click="handleSubmitToAsset">提交至资产管理处</el-button>
|
||
<!-- 通用操作按钮 -->
|
||
<el-button v-if="canReturn" type="primary" @click="handleReturn">提交招标代理</el-button>
|
||
<el-button v-if="canComplete" type="success" @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 v-model="remarkDialogVisible" :title="remarkDialogTitle" width="400px" append-to-body>
|
||
<el-form>
|
||
<el-form-item label="批注意见">
|
||
<el-input v-model="remarkInput" type="textarea" :rows="3" placeholder="请输入批注意见(选填)" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="remarkDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="submitRemarkAction">确定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 定稿弹窗 -->
|
||
<el-dialog v-model="finalizeDialogVisible" title="定稿确认" width="400px" append-to-body>
|
||
<el-form>
|
||
<el-form-item label="批注意见">
|
||
<el-input v-model="finalizeRemark" type="textarea" :rows="3" placeholder="请输入批注意见(选填)" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="finalizeDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="submitFinalize">确定定稿</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 补充上传弹窗 -->
|
||
<el-dialog v-model="supplyUploadDialogVisible" title="补充上传" width="500px" append-to-body>
|
||
<el-form :model="supplyUploadForm" label-width="100px">
|
||
<el-form-item label="选择文件" required>
|
||
<el-upload
|
||
ref="supplyUploadRef"
|
||
:action="uploadAction"
|
||
:headers="uploadHeaders"
|
||
:data="uploadData"
|
||
:on-success="handleSupplyUploadSuccess"
|
||
:on-error="handleUploadError"
|
||
:before-upload="beforeUpload"
|
||
:on-change="handleSupplyFileChange"
|
||
:auto-upload="false"
|
||
:limit="1"
|
||
:file-list="supplyFileList"
|
||
:show-file-list="true"
|
||
accept=".doc,.docx,.pdf">
|
||
<el-button type="primary" icon="Upload">选择文件</el-button>
|
||
</el-upload>
|
||
</el-form-item>
|
||
<el-form-item label="文件名称">
|
||
<el-input v-model="supplyUploadForm.fileName" disabled placeholder="上传后自动显示" />
|
||
</el-form-item>
|
||
<el-form-item label="文件意见" required>
|
||
<el-input v-model="supplyUploadForm.fileRemark" type="textarea" :rows="3" placeholder="请输入文件意见(必填)" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="supplyUploadDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="supplyUploadSubmitting" @click="submitSupplyUpload">确定</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,
|
||
getRequirementFilesForAudit,
|
||
getDocList,
|
||
uploadDoc,
|
||
reuploadDoc,
|
||
returnDoc,
|
||
completeDoc,
|
||
getAvailableActions,
|
||
downloadDocById,
|
||
downloadFileById,
|
||
saveDraft,
|
||
submitDraft,
|
||
supplyUpload as supplyUploadApi,
|
||
submitToDept as submitToDeptApi,
|
||
submitToAudit as submitToAuditApi,
|
||
submitToAsset as submitToAssetApi,
|
||
finalizeDoc as finalizeDocApi
|
||
} from '/@/api/purchase/docProcess'
|
||
import {
|
||
getNoticeByApplyId,
|
||
saveNotice,
|
||
publishNotice,
|
||
canSendNotice
|
||
} from '/@/api/purchase/bidOpeningNotice'
|
||
import type { UploadInstance, UploadProps, UploadUserFile } from 'element-plus'
|
||
import { Document } from '@element-plus/icons-vue'
|
||
|
||
const AuditRecordList = defineAsyncComponent(() => import('./AuditRecordList.vue'))
|
||
const ReviewerSetting = defineAsyncComponent(() => import('./ReviewerSetting.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 uploadedFileData = ref<any>(null)
|
||
|
||
// 退回相关
|
||
const returnDialogVisible = ref(false)
|
||
const returnRemark = ref('')
|
||
|
||
// 备注输入相关(提交至资产处/内审部门)
|
||
const remarkDialogVisible = ref(false)
|
||
const remarkDialogTitle = ref('')
|
||
const remarkInput = ref('')
|
||
const remarkActionType = ref('')
|
||
|
||
// 定稿相关
|
||
const finalizeDialogVisible = ref(false)
|
||
const finalizeRemark = ref('')
|
||
|
||
// 补充上传相关
|
||
const supplyUploadRef = ref<UploadInstance>()
|
||
const supplyUploadDialogVisible = ref(false)
|
||
const supplyUploadSubmitting = ref(false)
|
||
const supplyFileList = ref<UploadUserFile[]>([])
|
||
const supplyUploadedFileData = ref<any>(null)
|
||
const supplyUploadForm = ref({
|
||
fileName: '',
|
||
filePath: '',
|
||
fileRemark: ''
|
||
})
|
||
|
||
// 开标通知相关
|
||
const bidOpeningFormRef = ref()
|
||
const bidOpeningLoading = ref(false)
|
||
const bidOpeningSaving = ref(false)
|
||
const bidOpeningPublishing = ref(false)
|
||
const bidOpeningNoticeStatus = ref('')
|
||
const bidOpeningForm = ref({
|
||
id: '',
|
||
applyId: '',
|
||
projectName: '',
|
||
openTime: '',
|
||
openLocation: '',
|
||
agentContactName: '',
|
||
agentContactPhone: '',
|
||
publicUrl: '',
|
||
bidConfirmationLetter: '',
|
||
authorizationLetter: '',
|
||
remarks: '',
|
||
publishTime: ''
|
||
})
|
||
const bidOpeningRules = {
|
||
openTime: [{ required: true, message: '请选择开标时间', trigger: 'change' }],
|
||
openLocation: [{ required: true, message: '请输入开标地点', trigger: 'blur' }],
|
||
agentContactName: [{ required: true, message: '请输入代理联系人姓名', trigger: 'blur' }],
|
||
agentContactPhone: [{ required: true, message: '请输入代理联系人电话', trigger: 'blur' }]
|
||
}
|
||
|
||
// 弹窗标题
|
||
const dialogTitle = computed(() => {
|
||
return props.mode === 'agent' ? `处理项目 - ${rowData.value.purchaseNo || ''}` : '招标文件审核'
|
||
})
|
||
|
||
// 状态字段(两个模式使用不同字段名)
|
||
const statusField = computed(() => {
|
||
return props.mode === 'agent' ? rowData.value.status : rowData.value.docAuditStatus
|
||
})
|
||
|
||
// 是否草稿状态
|
||
const isDraft = computed(() => statusField.value === 'DRAFT')
|
||
|
||
// 招标代理操作权限
|
||
const canSaveDraft = computed(() => availableActions.value.includes('saveDraft'))
|
||
const canSubmitDraft = computed(() => availableActions.value.includes('submitDraft'))
|
||
const canReupload = computed(() => availableActions.value.includes('reupload'))
|
||
|
||
// 审核操作权限
|
||
const canSubmitToDept = computed(() => availableActions.value.includes('submitToDept'))
|
||
const canSubmitToAudit = computed(() => availableActions.value.includes('submitToAudit'))
|
||
const canSupplyUpload = computed(() => availableActions.value.includes('supplyUpload'))
|
||
const canSubmitToAsset = computed(() => availableActions.value.includes('submitToAsset'))
|
||
|
||
// 是否可退回
|
||
const canReturn = computed(() => availableActions.value.includes('return'))
|
||
|
||
// 是否可完成
|
||
const canComplete = computed(() => availableActions.value.includes('complete'))
|
||
|
||
// 是否可定稿
|
||
const canFinalize = computed(() => availableActions.value.includes('finalize'))
|
||
|
||
// 状态快捷判断
|
||
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')
|
||
|
||
// 是否显示开标通知Tab(招标代理模式且状态为已完成)
|
||
const showBidOpeningNotice = computed(() => props.mode === 'agent' && isCompleted.value)
|
||
|
||
// 是否显示采购代表设置Tab(需求部门审核中且有提交权限)
|
||
const canSetReviewer = computed(() => statusField.value === 'DEPT_REVIEWING' && availableActions.value.includes('submitToAsset'))
|
||
|
||
// 上传配置
|
||
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 = []
|
||
uploadedFileData.value = null
|
||
|
||
// 重置开标通知表单
|
||
resetBidOpeningForm()
|
||
|
||
// 获取申请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()
|
||
// 加载开标通知(如果是招标代理模式且状态为已完成)
|
||
if (props.mode === 'agent') {
|
||
loadBidOpeningNotice()
|
||
}
|
||
}
|
||
|
||
const loadRequirementFiles = async () => {
|
||
if (!applyId.value) return
|
||
requirementLoading.value = true
|
||
try {
|
||
// 根据模式调用不同的接口
|
||
const res = props.mode === 'agent'
|
||
? await getRequirementFiles(applyId.value)
|
||
: await getRequirementFilesForAudit(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) {
|
||
uploadedFileData.value = {
|
||
fileName: response.data.fileTitle || uploadFile.name,
|
||
filePath: response.data.remark || response.data.filePath
|
||
}
|
||
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 startUpload = async () => {
|
||
if (fileList.value.length === 0) {
|
||
useMessage().warning('请先选择文件')
|
||
return false
|
||
}
|
||
uploadSubmitting.value = true
|
||
uploadRef.value?.submit()
|
||
// 等待上传完成
|
||
return new Promise((resolve) => {
|
||
const checkInterval = setInterval(() => {
|
||
if (!uploadSubmitting.value) {
|
||
clearInterval(checkInterval)
|
||
resolve(uploadedFileData.value !== null)
|
||
}
|
||
}, 100)
|
||
})
|
||
}
|
||
|
||
// 保存草稿
|
||
const handleSaveDraft = async () => {
|
||
const uploaded = await startUpload()
|
||
if (!uploaded || !uploadedFileData.value) {
|
||
useMessage().error('文件上传失败')
|
||
return
|
||
}
|
||
try {
|
||
const res = await saveDraft({
|
||
applyId: applyId.value,
|
||
fileName: uploadedFileData.value.fileName,
|
||
filePath: uploadedFileData.value.filePath,
|
||
isDraft: true
|
||
})
|
||
if (res?.code === 0 || res?.code === 200) {
|
||
useMessage().success('草稿保存成功')
|
||
emit('refresh')
|
||
await loadDocList()
|
||
fileList.value = []
|
||
uploadedFileData.value = null
|
||
const actionsRes = await getAvailableActions(applyId.value)
|
||
availableActions.value = actionsRes.data || []
|
||
} else {
|
||
useMessage().error(res?.msg || '保存失败')
|
||
}
|
||
} catch (e: any) {
|
||
useMessage().error(e?.msg || '保存失败')
|
||
}
|
||
}
|
||
|
||
// 提交草稿
|
||
const handleSubmitDraft = async () => {
|
||
const uploaded = await startUpload()
|
||
if (!uploaded || !uploadedFileData.value) {
|
||
useMessage().error('文件上传失败')
|
||
return
|
||
}
|
||
try {
|
||
const res = await submitDraft({
|
||
applyId: applyId.value,
|
||
fileName: uploadedFileData.value.fileName,
|
||
filePath: uploadedFileData.value.filePath,
|
||
isDraft: false
|
||
})
|
||
if (res?.code === 0 || res?.code === 200) {
|
||
useMessage().success('提交成功')
|
||
emit('refresh')
|
||
await loadDocList()
|
||
fileList.value = []
|
||
uploadedFileData.value = null
|
||
const actionsRes = await getAvailableActions(applyId.value)
|
||
availableActions.value = actionsRes.data || []
|
||
} else {
|
||
useMessage().error(res?.msg || '提交失败')
|
||
}
|
||
} catch (e: any) {
|
||
useMessage().error(e?.msg || '提交失败')
|
||
}
|
||
}
|
||
|
||
// 重新上传并提交
|
||
const handleReuploadSubmit = async () => {
|
||
const uploaded = await startUpload()
|
||
if (!uploaded || !uploadedFileData.value) {
|
||
useMessage().error('文件上传失败')
|
||
return
|
||
}
|
||
try {
|
||
const res = await reuploadDoc({
|
||
applyId: applyId.value,
|
||
fileName: uploadedFileData.value.fileName,
|
||
filePath: uploadedFileData.value.filePath
|
||
})
|
||
if (res?.code === 0 || res?.code === 200) {
|
||
useMessage().success('重新上传成功')
|
||
emit('refresh')
|
||
await loadDocList()
|
||
fileList.value = []
|
||
uploadedFileData.value = null
|
||
const actionsRes = await getAvailableActions(applyId.value)
|
||
availableActions.value = actionsRes.data || []
|
||
} else {
|
||
useMessage().error(res?.msg || '上传失败')
|
||
}
|
||
} catch (e: any) {
|
||
useMessage().error(e?.msg || '上传失败')
|
||
}
|
||
}
|
||
|
||
// 补充上传
|
||
const handleSupplyUpload = () => {
|
||
supplyUploadForm.value = {
|
||
fileName: '',
|
||
filePath: '',
|
||
fileRemark: ''
|
||
}
|
||
supplyFileList.value = []
|
||
supplyUploadedFileData.value = null
|
||
supplyUploadDialogVisible.value = true
|
||
}
|
||
|
||
// 补充上传文件选择变化
|
||
const handleSupplyFileChange: UploadProps['onChange'] = (uploadFile, uploadFiles) => {
|
||
supplyFileList.value = uploadFiles
|
||
}
|
||
|
||
// 补充上传文件上传成功回调
|
||
const handleSupplyUploadSuccess: UploadProps['onSuccess'] = (response: any, uploadFile: any) => {
|
||
if (response?.code === 0 || response?.code === 200) {
|
||
supplyUploadedFileData.value = {
|
||
fileName: response.data.fileTitle || uploadFile.name,
|
||
filePath: response.data.remark || response.data.filePath
|
||
}
|
||
supplyUploadForm.value.fileName = supplyUploadedFileData.value.fileName
|
||
supplyUploadForm.value.filePath = supplyUploadedFileData.value.filePath
|
||
supplyUploadSubmitting.value = false
|
||
} else {
|
||
useMessage().error(response?.msg || '上传失败')
|
||
supplyUploadSubmitting.value = false
|
||
}
|
||
}
|
||
|
||
// 补充上传文件上传
|
||
const startSupplyUpload = async () => {
|
||
if (supplyFileList.value.length === 0) {
|
||
useMessage().warning('请先选择文件')
|
||
return false
|
||
}
|
||
supplyUploadSubmitting.value = true
|
||
supplyUploadRef.value?.submit()
|
||
// 等待上传完成
|
||
return new Promise((resolve) => {
|
||
const checkInterval = setInterval(() => {
|
||
if (!supplyUploadSubmitting.value) {
|
||
clearInterval(checkInterval)
|
||
resolve(supplyUploadedFileData.value !== null)
|
||
}
|
||
}, 100)
|
||
})
|
||
}
|
||
|
||
const submitSupplyUpload = async () => {
|
||
// 先上传文件
|
||
const uploaded = await startSupplyUpload()
|
||
if (!uploaded || !supplyUploadedFileData.value) {
|
||
useMessage().error('文件上传失败')
|
||
return
|
||
}
|
||
if (!supplyUploadForm.value.fileRemark) {
|
||
useMessage().warning('请填写文件意见')
|
||
supplyUploadSubmitting.value = false
|
||
return
|
||
}
|
||
supplyUploadSubmitting.value = true
|
||
try {
|
||
const res = await supplyUploadApi({
|
||
applyId: applyId.value,
|
||
fileName: supplyUploadedFileData.value.fileName,
|
||
filePath: supplyUploadedFileData.value.filePath,
|
||
fileRemark: supplyUploadForm.value.fileRemark
|
||
})
|
||
if (res?.code === 0 || res?.code === 200) {
|
||
useMessage().success('补充上传成功')
|
||
supplyUploadDialogVisible.value = false
|
||
emit('refresh')
|
||
await loadDocList()
|
||
auditRecordListRef.value?.refresh()
|
||
const actionsRes = await getAvailableActions(applyId.value)
|
||
availableActions.value = actionsRes.data || []
|
||
} else {
|
||
useMessage().error(res?.msg || '上传失败')
|
||
}
|
||
} catch (e: any) {
|
||
useMessage().error(e?.msg || '上传失败')
|
||
} finally {
|
||
supplyUploadSubmitting.value = false
|
||
}
|
||
}
|
||
|
||
// 提交至需求部门
|
||
const handleSubmitToDept = () => {
|
||
remarkDialogTitle.value = '提交至需求部门'
|
||
remarkInput.value = ''
|
||
remarkActionType.value = 'dept'
|
||
remarkDialogVisible.value = true
|
||
}
|
||
|
||
// 提交至内审部门
|
||
const handleSubmitToAudit = () => {
|
||
remarkDialogTitle.value = '提交至内审部门'
|
||
remarkInput.value = ''
|
||
remarkActionType.value = 'audit'
|
||
remarkDialogVisible.value = true
|
||
}
|
||
|
||
// 提交至资产管理处
|
||
const handleSubmitToAsset = () => {
|
||
remarkDialogTitle.value = '提交至资产管理处'
|
||
remarkInput.value = ''
|
||
remarkActionType.value = 'asset'
|
||
remarkDialogVisible.value = true
|
||
}
|
||
|
||
// 提交备注操作
|
||
const submitRemarkAction = async () => {
|
||
remarkDialogVisible.value = false
|
||
try {
|
||
let res: any
|
||
const params = { applyId: applyId.value, remark: remarkInput.value }
|
||
if (remarkActionType.value === 'dept') {
|
||
res = await submitToDeptApi(params)
|
||
} else if (remarkActionType.value === 'audit') {
|
||
res = await submitToAuditApi(params)
|
||
} else if (remarkActionType.value === 'asset') {
|
||
res = await submitToAssetApi(params)
|
||
}
|
||
if (res?.code === 0 || res?.code === 200) {
|
||
useMessage().success('提交成功')
|
||
emit('refresh')
|
||
await loadDocList()
|
||
auditRecordListRef.value?.refresh()
|
||
const actionsRes = await getAvailableActions(applyId.value)
|
||
availableActions.value = actionsRes.data || []
|
||
} else {
|
||
useMessage().error(res?.msg || '提交失败')
|
||
}
|
||
} 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')
|
||
await 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')
|
||
await loadDocList()
|
||
auditRecordListRef.value?.refresh()
|
||
const actionsRes = await getAvailableActions(applyId.value)
|
||
availableActions.value = actionsRes.data || []
|
||
} catch (e: any) {
|
||
useMessage().error(e?.msg || '操作失败')
|
||
}
|
||
}
|
||
|
||
// 定稿
|
||
const handleFinalize = () => {
|
||
finalizeRemark.value = ''
|
||
finalizeDialogVisible.value = true
|
||
}
|
||
|
||
// 提交定稿
|
||
const submitFinalize = async () => {
|
||
try {
|
||
const res = await finalizeDocApi({ applyId: applyId.value, remark: finalizeRemark.value })
|
||
if (res?.code === 0 || res?.code === 200) {
|
||
useMessage().success('定稿成功')
|
||
finalizeDialogVisible.value = false
|
||
emit('refresh')
|
||
await loadDocList()
|
||
auditRecordListRef.value?.refresh()
|
||
const actionsRes = await getAvailableActions(applyId.value)
|
||
availableActions.value = actionsRes.data || []
|
||
} else {
|
||
useMessage().error(res?.msg || '定稿失败')
|
||
}
|
||
} catch (e: any) {
|
||
useMessage().error(e?.msg || '定稿失败')
|
||
}
|
||
}
|
||
|
||
// ==================== 开标通知相关方法 ====================
|
||
|
||
/**
|
||
* 重置开标通知表单
|
||
*/
|
||
const resetBidOpeningForm = () => {
|
||
bidOpeningForm.value = {
|
||
id: '',
|
||
applyId: '',
|
||
projectName: '',
|
||
openTime: '',
|
||
openLocation: '',
|
||
agentContactName: '',
|
||
agentContactPhone: '',
|
||
publicUrl: '',
|
||
bidConfirmationLetter: '',
|
||
authorizationLetter: '',
|
||
remarks: '',
|
||
publishTime: ''
|
||
}
|
||
bidOpeningNoticeStatus.value = ''
|
||
}
|
||
|
||
/**
|
||
* 加载开标通知
|
||
*/
|
||
const loadBidOpeningNotice = async () => {
|
||
if (!applyId.value) return
|
||
bidOpeningLoading.value = true
|
||
try {
|
||
const res = await getNoticeByApplyId(applyId.value)
|
||
if (res?.code === 0 || res?.code === 200) {
|
||
const data = res.data
|
||
if (data) {
|
||
bidOpeningForm.value = {
|
||
id: data.id || '',
|
||
applyId: data.applyId || applyId.value,
|
||
projectName: data.projectName || rowData.value.projectName || '',
|
||
openTime: data.openTime || '',
|
||
openLocation: data.openLocation || '',
|
||
agentContactName: data.agentContactName || '',
|
||
agentContactPhone: data.agentContactPhone || '',
|
||
publicUrl: data.publicUrl || '',
|
||
bidConfirmationLetter: data.bidConfirmationLetter || '',
|
||
authorizationLetter: data.authorizationLetter || '',
|
||
remarks: data.remarks || '',
|
||
publishTime: data.publishTime || ''
|
||
}
|
||
bidOpeningNoticeStatus.value = data.status || ''
|
||
} else {
|
||
bidOpeningForm.value.applyId = applyId.value
|
||
bidOpeningForm.value.projectName = rowData.value.projectName || ''
|
||
}
|
||
}
|
||
} catch (e) {
|
||
// 忽略错误,可能是第一次创建
|
||
} finally {
|
||
bidOpeningLoading.value = false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 开标通知文件上传成功回调
|
||
*/
|
||
const handleBidFileSuccess = (response: any, file: any, field: string) => {
|
||
if (response?.code === 0 || response?.code === 200) {
|
||
bidOpeningForm.value[field] = response.data.remark || response.data.filePath
|
||
useMessage().success('文件上传成功')
|
||
} else {
|
||
useMessage().error(response?.msg || '文件上传失败')
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 查看文件
|
||
*/
|
||
const viewFile = (url: string) => {
|
||
if (url) {
|
||
window.open(url, '_blank')
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 保存开标通知草稿
|
||
*/
|
||
const handleSaveBidOpeningDraft = async () => {
|
||
try {
|
||
await bidOpeningFormRef.value?.validate()
|
||
} catch {
|
||
useMessage().warning('请填写必填项')
|
||
return
|
||
}
|
||
|
||
bidOpeningSaving.value = true
|
||
try {
|
||
const res = await saveNotice({
|
||
...bidOpeningForm.value,
|
||
applyId: applyId.value
|
||
})
|
||
if (res?.code === 0 || res?.code === 200) {
|
||
useMessage().success('保存成功')
|
||
await loadBidOpeningNotice()
|
||
} else {
|
||
useMessage().error(res?.msg || '保存失败')
|
||
}
|
||
} catch (e: any) {
|
||
useMessage().error(e?.msg || '保存失败')
|
||
} finally {
|
||
bidOpeningSaving.value = false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 发布开标通知
|
||
*/
|
||
const handlePublishBidOpening = async () => {
|
||
try {
|
||
await bidOpeningFormRef.value?.validate()
|
||
} catch {
|
||
useMessage().warning('请填写必填项')
|
||
return
|
||
}
|
||
|
||
try {
|
||
await useMessageBox().confirm('确定要发布开标通知吗?发布后将无法修改。')
|
||
} catch {
|
||
return
|
||
}
|
||
|
||
bidOpeningPublishing.value = true
|
||
try {
|
||
const res = await publishNotice({
|
||
...bidOpeningForm.value,
|
||
applyId: applyId.value
|
||
})
|
||
if (res?.code === 0 || res?.code === 200) {
|
||
useMessage().success('发布成功')
|
||
await loadBidOpeningNotice()
|
||
} else {
|
||
useMessage().error(res?.msg || '发布失败')
|
||
}
|
||
} catch (e: any) {
|
||
useMessage().error(e?.msg || '发布失败')
|
||
} finally {
|
||
bidOpeningPublishing.value = false
|
||
}
|
||
}
|
||
|
||
const handleClose = () => {
|
||
visible.value = false
|
||
}
|
||
|
||
const getStatusType = (status: string) => {
|
||
const typeMap: Record<string, string> = {
|
||
'PENDING_UPLOAD': 'info',
|
||
'DRAFT': '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': '待上传',
|
||
'DRAFT': '草稿',
|
||
'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
|
||
}
|
||
|
||
// 采购代表设置保存成功回调
|
||
const handleReviewerSaved = () => {
|
||
emit('refresh')
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
.bid-opening-form {
|
||
padding: 20px;
|
||
|
||
.file-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-top: 8px;
|
||
color: #606266;
|
||
|
||
.el-icon {
|
||
font-size: 16px;
|
||
}
|
||
}
|
||
|
||
.form-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 12px;
|
||
margin-top: 20px;
|
||
padding-top: 20px;
|
||
border-top: 1px solid #ebeef5;
|
||
}
|
||
}
|
||
</style> |