This commit is contained in:
吴红兵
2026-03-08 15:18:12 +08:00
parent 5a53648ee7
commit ec690ecab5
2 changed files with 888 additions and 0 deletions

View File

@@ -0,0 +1,744 @@
<template>
<div class="bidfile-audit-container">
<!-- 基本信息 -->
<el-card shadow="never" class="info-card">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon><Document /></el-icon>
采购申请信息
</span>
<el-tag v-if="flowStatus === '0'" type="warning">运行中</el-tag>
<el-tag v-else-if="flowStatus === '1'" type="success">已完成</el-tag>
<el-tag v-else-if="flowStatus === '2'" type="info">已作废</el-tag>
</div>
</template>
<!-- 招标代理只能看到有限信息 -->
<el-descriptions v-if="isAgent" :column="2" border>
<el-descriptions-item label="采购编号">{{ applyData.purchaseNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="项目名称">{{ applyData.projectName || '-' }}</el-descriptions-item>
<el-descriptions-item label="招标代理">{{ applyData.agentName || '-' }}</el-descriptions-item>
</el-descriptions>
<!-- 其他角色可以看到完整信息 -->
<el-descriptions v-else :column="3" border>
<el-descriptions-item label="采购编号">{{ applyData.purchaseNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="项目名称">{{ applyData.projectName || '-' }}</el-descriptions-item>
<el-descriptions-item label="预算金额">{{ applyData.budget ? Number(applyData.budget).toLocaleString() + ' 元' : '-' }}</el-descriptions-item>
<el-descriptions-item label="采购方式">{{ purchaseTypeLabel || '-' }}</el-descriptions-item>
<el-descriptions-item label="招标代理">{{ applyData.agentName || '-' }}</el-descriptions-item>
<el-descriptions-item label="申请人">{{ applyData.createName || applyData.createBy || '-' }}</el-descriptions-item>
<el-descriptions-item label="申请部门" :span="3">{{ applyData.deptName || '-' }}</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 需求文件招标代理可见 -->
<el-card v-if="isAgent" shadow="never" class="file-card">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon><FolderOpened /></el-icon>
需求文件
</span>
</div>
</template>
<el-table v-loading="requirementLoading" :data="requirementFiles" stripe border>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="fileTitle" label="文件名" min-width="200" show-overflow-tooltip />
<el-table-column prop="createTime" label="上传时间" width="160" />
<el-table-column label="操作" width="100" align="center" fixed="right">
<template #default="{ row }">
<el-button type="primary" link icon="Download" @click="handleDownloadRequirement(row)">下载</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!requirementLoading && requirementFiles.length === 0" description="暂无需求文件" />
</el-card>
<!-- 招标文件信息 -->
<el-card shadow="never" class="file-card">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon><FolderOpened /></el-icon>
招标文件
</span>
<el-tag v-if="currentVersion" type="success">{{ currentVersion.version }}</el-tag>
</div>
</template>
<el-tabs v-model="activeTab">
<!-- 当前版本 -->
<el-tab-pane label="当前版本" name="current">
<div v-if="currentVersion" class="version-detail">
<el-descriptions :column="2" border>
<el-descriptions-item label="版本号">
<el-tag type="primary">{{ currentVersion.version }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="上传人">
{{ currentVersion.uploadUserName || '-' }}
<el-tag size="small" type="info" style="margin-left: 8px">{{ getRoleLabel(currentVersion.uploadRoleCode) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="文件名" :span="2">
<el-link type="primary" :href="currentVersion.fileUrl" target="_blank">
<el-icon><Link /></el-icon>
{{ currentVersion.fileName }}
</el-link>
</el-descriptions-item>
<el-descriptions-item label="文件修改意见" :span="2">
{{ currentVersion.comment || '无' }}
</el-descriptions-item>
<el-descriptions-item label="上传时间" :span="2">
{{ currentVersion.createTime || '-' }}
</el-descriptions-item>
</el-descriptions>
<div class="file-actions">
<el-button type="primary" icon="Download" @click="handleDownload(currentVersion)">下载文件</el-button>
</div>
</div>
<el-empty v-else description="暂无招标文件,请上传" />
</el-tab-pane>
<!-- 版本历史 -->
<el-tab-pane label="版本历史" name="history">
<el-table :data="versionHistory" stripe border v-if="versionHistory.length > 0">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="version" label="版本" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.isCurrent === '1' ? 'success' : 'info'">{{ row.version }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="fileName" label="文件名" min-width="200" show-overflow-tooltip />
<el-table-column prop="uploadUserName" label="上传人" width="100" />
<el-table-column prop="uploadRoleCode" label="角色" width="120">
<template #default="{ row }">
{{ getRoleLabel(row.uploadRoleCode) }}
</template>
</el-table-column>
<el-table-column prop="comment" label="文件修改意见" min-width="150" show-overflow-tooltip>
<template #default="{ row }">
{{ row.comment || '-' }}
</template>
</el-table-column>
<el-table-column prop="createTime" label="上传时间" width="160" />
<el-table-column label="操作" width="120" align="center" fixed="right">
<template #default="{ row }">
<el-button type="primary" link icon="Download" @click="handleDownload(row)">下载</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-else description="暂无版本历史" />
</el-tab-pane>
</el-tabs>
</el-card>
<!-- 上传招标文件流程中可编辑时显示 -->
<el-card v-if="showUploadSection" shadow="never" class="upload-card">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon><Upload /></el-icon>
上传招标文件
</span>
</div>
</template>
<el-form ref="uploadFormRef" :model="uploadForm" :rules="uploadRules" label-width="120px">
<el-form-item label="选择文件" prop="fileId">
<el-upload
ref="uploadRef"
:action="uploadUrl"
:headers="uploadHeaders"
:data="uploadData"
:before-upload="beforeUpload"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:on-remove="handleUploadRemove"
:file-list="fileList"
accept=".doc,.docx"
:disabled="isViewMode"
>
<el-button type="primary" :disabled="isViewMode">选择文件</el-button>
<template #tip>
<div class="el-upload__tip">仅支持 docdocx 格式单文件不超过 50MB上传后自动保存</div>
</template>
</el-upload>
</el-form-item>
<el-form-item v-if="uploadForm.fileId" label="文件修改意见">
<el-input
v-model="uploadForm.comment"
type="textarea"
:rows="3"
placeholder="请输入文件修改意见或备注(可选)"
:disabled="isViewMode"
maxlength="500"
show-word-limit
/>
</el-form-item>
<el-form-item v-if="uploadForm.fileId" label="文件修改意见">
<el-input
v-model="uploadForm.comment"
type="textarea"
:rows="3"
placeholder="请输入文件修改意见或备注(可选)"
:disabled="isViewMode"
maxlength="500"
show-word-limit
/>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup lang="ts" name="BidFileAudit">
import { ref, reactive, computed, onMounted, watch } from 'vue';
import { useRoute } from 'vue-router';
import { Document, FolderOpened, Upload, Link } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { Session } from '/@/utils/storage';
import { useUserInfo } from '/@/stores/userInfo';
import other from '/@/utils/other';
import {
getBidFileVersionHistory,
getBidFileCurrentVersion,
agentUploadBidFile,
assetUploadBidFile,
uploadBidFileNewVersion,
getAgentPurchaseDetail,
getFlowPurchaseDetail,
tempStoreBidFile,
submitBidFile,
} from '/@/api/purchase/bidfile';
import { getRequirementFiles } from '/@/api/purchase/purchasingfiles';
import { currElTabIsSave } from '/@/api/order/order-key-vue';
// ==================== Props & Emits ====================
const props = defineProps({
currJob: { type: Object, default: null },
currElTab: { type: Object, default: null },
});
const emit = defineEmits(['handleJob']);
// ==================== 用户角色判断 ====================
const userStore = useUserInfo();
const currentUserRoleCodes = computed(() => userStore.userInfos.roleCodes || []);
// ==================== 常量定义 ====================
const ROLE_LABEL_MAP: Record<string, string> = {
PURCHASE_AGENT: '招标代理',
PURCHASE_ASSET: '资产管理处',
PURCHASE_DEPT_APPLY: '部门负责人',
PURCHASE_FILE_AUDIT: '内审部门',
PURCHASE_CENTER: '采购中心',
};
const PURCHASE_TYPE_MAP: Record<string, string> = {
'1': '网上商城',
'2': '市场采购',
'3': '商务洽谈',
'5': '询价',
'6': '单一来源',
'7': '竞争性谈判',
'8': '竞争性磋商',
'9': '公开招标',
'10': '邀请招标',
};
// ==================== 响应式数据 ====================
const route = useRoute();
const loading = ref(false);
// 流程相关
const isFlowEmbed = computed(() => !!props.currJob);
const isViewMode = computed(() => {
if (isFlowEmbed.value && props.currJob) {
return !!props.currJob.hiJob || props.currElTab?.isFormEdit === '0';
}
return route.query.mode === 'view';
});
// 当前用户角色判断从用户store获取
const isAgent = computed(() => currentUserRoleCodes.value.includes('PURCHASE_AGENT'));
const isAsset = computed(() => currentUserRoleCodes.value.includes('PURCHASE_ASSET'));
const isDeptApply = computed(() => currentUserRoleCodes.value.includes('PURCHASE_DEPT_APPLY'));
const isFileAudit = computed(() => currentUserRoleCodes.value.includes('PURCHASE_FILE_AUDIT'));
// 是否显示上传区域
const showUploadSection = computed(() => {
if (isViewMode.value) return false;
if (isAgent.value ) return true;
if (isAsset.value || isDeptApply.value || isFileAudit.value) return true;
return false;
});
// 采购申请数据
const applyData = ref<any>({});
const purchaseTypeLabel = computed(() => {
const type = applyData.value.purchaseType;
return PURCHASE_TYPE_MAP[type] || type || '-';
});
// 流程状态
const flowStatus = computed(() => applyData.value.fileFlowStatus || '0');
// 招标文件
const activeTab = ref('current');
const currentVersion = ref<any>(null);
const versionHistory = ref<any[]>([]);
// 需求文件(招标代理可见)
const requirementFiles = ref<any[]>([]);
const requirementLoading = ref(false);
// 上传相关
const uploadFormRef = ref();
const uploadRef = ref();
const fileList = ref<any[]>([]);
const saveLoading = ref(false);
const tempStoreLoading = ref(false);
const submitLoading = ref(false);
const isTempStored = ref(false);
const uploadForm = reactive({
fileId: '',
fileName: '',
fileUrl: '',
comment: '',
});
const uploadRules = {
// 招标文件上传为可选,后续可补充上传
};
// ==================== 计算属性 ====================
const BID_FILE_TYPE = '130';
const uploadUrl = computed(() => import.meta.env.VITE_API_URL + '/purchase/purchasingfiles/upload');
const uploadHeaders = computed(() => ({
Authorization: 'Bearer ' + Session.getToken(),
'TENANT-ID': Session.getTenant(),
}));
const uploadData = computed(() => ({
purchaseId: effectivePurchaseId.value,
fileType: BID_FILE_TYPE,
}));
// 当前采购申请ID
const effectivePurchaseId = computed(() => {
if (props.currJob?.orderId) {
return String(props.currJob.orderId);
}
return route.query.id as string || route.query.purchaseId as string || '';
});
// 是否可以提交(非查看模式即可提交,不强制要求上传文件)
const canSubmitFile = computed(() => {
return !isViewMode.value;
});
// ==================== 方法定义 ====================
// 获取角色标签
const getRoleLabel = (roleCode: string) => {
return ROLE_LABEL_MAP[roleCode] || roleCode || '-';
};
// 加载采购申请详情(根据角色调用不同接口)
const loadApplyData = async () => {
if (!effectivePurchaseId.value) return;
try {
loading.value = true;
let res: any;
// 招标代理只能看到有限信息
if (isAgent.value) {
res = await getAgentPurchaseDetail(effectivePurchaseId.value);
} else {
// 其他角色可以看到完整信息
res = await getFlowPurchaseDetail(effectivePurchaseId.value);
}
if (res.code === 0 && res.data) {
applyData.value = res.data;
isTempStored.value = res.data.fileFlowStatus === '-1';
}
} catch (e: any) {
ElMessage.error(e?.msg || '加载采购申请失败');
} finally {
loading.value = false;
}
};
// 加载招标文件
const loadBidFiles = async () => {
if (!effectivePurchaseId.value) return;
try {
const [currentRes, historyRes] = await Promise.all([
getBidFileCurrentVersion(effectivePurchaseId.value),
getBidFileVersionHistory(effectivePurchaseId.value),
]);
if (currentRes.code === 0) {
currentVersion.value = currentRes.data;
}
if (historyRes.code === 0) {
versionHistory.value = historyRes.data || [];
}
} catch (e: any) {
ElMessage.error(e?.msg || '加载招标文件失败');
}
};
// 加载需求文件(招标代理可见)
const loadRequirementFiles = async () => {
if (!isAgent.value || !effectivePurchaseId.value) return;
try {
requirementLoading.value = true;
const res = await getRequirementFiles(effectivePurchaseId.value);
if (res.code === 0) {
requirementFiles.value = res.data || [];
}
} catch (e: any) {
ElMessage.error(e?.msg || '加载需求文件失败');
} finally {
requirementLoading.value = false;
}
};
// 下载需求文件使用downBlobFile携带token
const handleDownloadRequirement = (row: any) => {
if (row.id) {
const downloadUrl = '/purchase/purchasingfiles/downloadById?fileId=' + row.id;
other.downBlobFile(downloadUrl, {}, row.fileTitle || '需求文件');
}
};
// 文件上传前校验
const beforeUpload = (file: any) => {
const isLt50M = file.size / 1024 / 1024 < 50;
if (!isLt50M) {
ElMessage.error('文件大小不能超过 50MB');
return false;
}
return true;
};
// 上传成功后自动保存
const handleUploadSuccess = async (response: any, file: any) => {
if (response.code === 0) {
uploadForm.fileId = response.data.id || response.data.fileName;
uploadForm.fileUrl = response.data.url || response.data.remark;
uploadForm.fileName = file.name;
// 自动保存招标文件
const saved = await saveUploadedFile();
if (saved) {
ElMessage.success('招标文件上传并保存成功');
}
} else {
ElMessage.error(response.msg || '文件上传失败');
fileList.value = [];
}
};
// 上传失败
const handleUploadError = () => {
ElMessage.error('文件上传失败');
fileList.value = [];
};
// 移除文件
const handleUploadRemove = () => {
uploadForm.fileId = '';
uploadForm.fileUrl = '';
uploadForm.fileName = '';
};
// 下载招标文件使用downBlobFile携带token
const handleDownload = (row: any) => {
if (row.fileId) {
const downloadUrl = '/purchase/purchasingfiles/downloadById?fileId=' + row.fileId;
other.downBlobFile(downloadUrl, {}, row.fileName || '招标文件');
}
};
// 手动保存上传的招标文件
const handleSaveBidFile = async () => {
if (!uploadForm.fileId) {
ElMessage.warning('请先上传招标文件');
return;
}
saveLoading.value = true;
try {
const saved = await saveUploadedFile();
if (saved) {
ElMessage.success('招标文件保存成功,版本已更新');
}
} finally {
saveLoading.value = false;
}
};
// 保存上传的招标文件
const saveUploadedFile = async () => {
if (!uploadForm.fileId) {
ElMessage.warning('请先上传招标文件');
return false;
}
const runJobId = props.currJob?.id || '';
const params = {
purchaseId: effectivePurchaseId.value,
fileId: uploadForm.fileId,
fileName: uploadForm.fileName,
fileUrl: uploadForm.fileUrl,
comment: uploadForm.comment,
runJobId,
};
try {
let res: any;
if (isAgent.value) {
res = await agentUploadBidFile(params);
} else if (isAsset.value) {
res = await assetUploadBidFile(params);
} else {
res = await uploadBidFileNewVersion(params);
}
if (res.code === 0) {
await loadBidFiles();
// 标记为已保存
if (props.currJob && props.currElTab?.id) {
currElTabIsSave(props.currJob, props.currElTab.id, true, emit);
}
// 清空表单和上传组件,允许继续上传新文件
uploadForm.fileId = '';
uploadForm.fileUrl = '';
uploadForm.fileName = '';
uploadForm.comment = '';
fileList.value = [];
uploadRef.value?.clearFiles();
return true;
} else {
ElMessage.error(res.msg || '保存失败');
return false;
}
} catch (e: any) {
ElMessage.error(e?.msg || '保存招标文件失败');
return false;
}
};
// 暂存招标文件(模拟保存)
const handleTempStoreBidFile = async () => {
if (!uploadForm.fileId) {
ElMessage.warning('请先上传招标文件');
return;
}
tempStoreLoading.value = true;
try {
const params = {
purchaseId: effectivePurchaseId.value,
fileId: uploadForm.fileId,
fileName: uploadForm.fileName,
fileUrl: uploadForm.fileUrl,
comment: uploadForm.comment,
};
const res = await tempStoreBidFile(params);
if (res.code === 0) {
await loadBidFiles();
isTempStored.value = true;
ElMessage.success('暂存成功,可继续上传或提交');
// 清空表单和上传组件,允许继续上传新文件
uploadForm.fileId = '';
uploadForm.fileUrl = '';
uploadForm.fileName = '';
uploadForm.comment = '';
fileList.value = [];
uploadRef.value?.clearFiles();
} else {
ElMessage.error(res.msg || '暂存失败');
}
} catch (e: any) {
ElMessage.error(e?.msg || '暂存招标文件失败');
} finally {
tempStoreLoading.value = false;
}
};
// 提交招标文件(启动流程)
const handleSubmitBidFile = async () => {
submitLoading.value = true;
try {
const res = await submitBidFile(effectivePurchaseId.value);
if (res.code === 0) {
await loadApplyData();
await loadBidFiles();
isTempStored.value = false;
ElMessage.success('提交成功,流程已启动');
} else {
ElMessage.error(res.msg || '提交失败');
}
} catch (e: any) {
ElMessage.error(e?.msg || '提交招标文件失败');
} finally {
submitLoading.value = false;
}
};
// ==================== 流程集成 ====================
// 流程保存回调 - 审核时自动调用暂存接口
const handleFlowSave = async () => {
// 如果有待保存的文件,先保存文件
if (uploadForm.fileId) {
const saved = await saveUploadedFile();
if (!saved) return false;
}
// 自动调用暂存接口(可以没有文件)
try {
const params = {
purchaseId: effectivePurchaseId.value,
fileId: uploadForm.fileId || '',
fileName: uploadForm.fileName || '',
fileUrl: uploadForm.fileUrl || '',
comment: uploadForm.comment || '',
};
const res = await tempStoreBidFile(params);
if (res.code === 0) {
await loadBidFiles();
// 标记为已保存
if (props.currJob && props.currElTab?.id) {
currElTabIsSave(props.currJob, props.currElTab.id, true, emit);
}
return true;
} else {
ElMessage.error(res.msg || '暂存失败');
return false;
}
} catch (e: any) {
ElMessage.error(e?.msg || '暂存失败');
return false;
}
};
// 注册流程回调
const registerFlowCallbacks = () => {
if (!isFlowEmbed.value) return;
if (props.currJob?.resolveSaves) {
props.currJob.resolveSaves.push(handleFlowSave);
}
};
// ==================== 生命周期 ====================
onMounted(async () => {
await loadApplyData();
await loadBidFiles();
if (isAgent.value) {
await loadRequirementFiles();
}
registerFlowCallbacks();
});
watch(
() => props.currJob,
async (newVal) => {
if (newVal) {
await loadApplyData();
await loadBidFiles();
if (isAgent.value) {
await loadRequirementFiles();
}
registerFlowCallbacks();
}
},
{ immediate: false }
);
watch(
() => route.query.id,
async (newId) => {
if (newId && !isFlowEmbed.value) {
await loadApplyData();
await loadBidFiles();
if (isAgent.value) {
await loadRequirementFiles();
}
}
},
{ immediate: false }
);
</script>
<style scoped lang="scss">
.bidfile-audit-container {
padding: 16px;
min-height: 100%;
background: var(--el-fill-color-lighter);
}
.info-card,
.file-card,
.upload-card {
margin-bottom: 16px;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.card-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.version-detail {
padding: 16px 0;
}
.file-actions {
margin-top: 16px;
display: flex;
gap: 12px;
}
:deep(.el-descriptions__label) {
font-weight: 500;
}
:deep(.el-upload__tip) {
color: var(--el-text-color-secondary);
font-size: 12px;
margin-top: 8px;
}
.save-tip {
margin-left: 12px;
color: var(--el-color-warning);
font-size: 13px;
}
</style>