rebuild all

This commit is contained in:
吴红兵
2026-03-02 10:06:48 +08:00
parent c8aaeee298
commit 4167a2a94d
12 changed files with 836 additions and 4 deletions

View File

@@ -127,6 +127,119 @@
<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="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>
<!-- 操作区域 -->
@@ -244,9 +357,17 @@ import {
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'
@@ -297,6 +418,33 @@ const supplyUploadForm = ref({
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 || ''}` : '招标文件审核'
@@ -336,6 +484,12 @@ const isReviewing = computed(() => ['ASSET_REVIEWING', 'DEPT_REVIEWING', 'AUDIT_
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 || ''
@@ -365,6 +519,9 @@ const open = async (row: any) => {
fileList.value = []
uploadedFileData.value = null
// 重置开标通知表单
resetBidOpeningForm()
// 获取申请ID兼容 id 和 applyId 两种字段名)
applyId.value = row.applyId || row.id
@@ -380,6 +537,10 @@ const open = async (row: any) => {
loadRequirementFiles()
// 加载招标文件
loadDocList()
// 加载开标通知(如果是招标代理模式且状态为已完成)
if (props.mode === 'agent') {
loadBidOpeningNotice()
}
}
const loadRequirementFiles = async () => {
@@ -801,6 +962,154 @@ const submitFinalize = async () => {
}
}
// ==================== 开标通知相关方法 ====================
/**
* 重置开标通知表单
*/
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
}
@@ -841,6 +1150,11 @@ const getFileTypeLabel = (type: string) => {
return labelMap[type] || type
}
// 采购代表设置保存成功回调
const handleReviewerSaved = () => {
emit('refresh')
}
defineExpose({ open })
</script>
@@ -868,4 +1182,29 @@ defineExpose({ open })
.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>