更新采购申请

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,179 @@
/*
* 招标文件处理统一API
* 整合招标代理和审核部门的接口调用
*/
import request from '/@/utils/request';
/**
* 获取列表数据(根据模式调用不同接口)
* @param mode 模式agent-招标代理audit-审核部门
* @param params 分页参数
*/
export function getDocProcessList(mode: string, params?: any) {
const url = mode === 'agent'
? '/purchase/purchasingdoc/agent/list'
: '/purchase/purchasingdoc/audit/page';
return request({
url,
method: 'get',
params
});
}
/**
* 获取采购需求文件列表
* @param applyId 采购申请ID
*/
export function getRequirementFiles(applyId: number | string) {
return request({
url: `/purchase/purchasingdoc/agent/requirement/${applyId}`,
method: 'get'
});
}
/**
* 获取招标文件列表
* @param applyId 采购申请ID
*/
export function getDocList(applyId: number | string) {
return request({
url: `/purchase/purchasingdoc/list/${applyId}`,
method: 'get'
});
}
/**
* 上传招标文件(招标代理)
* @param data 文件数据
*/
export function uploadDoc(data: any) {
return request({
url: '/purchase/purchasingdoc/upload',
method: 'post',
data
});
}
/**
* 重新上传招标文件
* @param data 文件数据
*/
export function reuploadDoc(data: any) {
return request({
url: '/purchase/purchasingdoc/reupload',
method: 'post',
data
});
}
/**
* 获取招标文件下载地址
* @param id 招标文件ID
*/
export function getDocDownloadUrl(id: number | string) {
return `/purchase/purchasingdoc/download/${id}`;
}
/**
* 下载招标文件返回blob
* @param id 招标文件ID
*/
export function downloadDocById(id: number | string) {
return request({
url: `/purchase/purchasingdoc/download/${id}`,
method: 'get',
responseType: 'blob'
});
}
/**
* 根据文件ID下载采购附件返回blob
* @param fileId 文件ID
*/
export function downloadFileById(fileId: string | number) {
return request({
url: '/purchase/purchasingfiles/downloadById',
method: 'get',
params: { fileId },
responseType: 'blob'
});
}
/**
* 确认无误
* @param data 审核信息
*/
export function confirmDoc(data: any) {
return request({
url: '/purchase/purchasingdoc/confirm',
method: 'post',
data
});
}
/**
* 退回修改
* @param data 审核信息
*/
export function returnDoc(data: any) {
return request({
url: '/purchase/purchasingdoc/return',
method: 'post',
data
});
}
/**
* 确认流程结束
* @param applyId 采购申请ID
*/
export function completeDoc(applyId: number | string) {
return request({
url: '/purchase/purchasingdoc/complete',
method: 'post',
params: { applyId }
});
}
/**
* 获取审核记录
* @param applyId 采购申请ID
*/
export function getAuditRecords(applyId: number | string) {
return request({
url: `/purchase/purchasingdoc/audit-records/${applyId}`,
method: 'get'
});
}
/**
* 获取可执行操作
* @param applyId 采购申请ID
*/
export function getAvailableActions(applyId: number | string) {
return request({
url: `/purchase/purchasingdoc/actions/${applyId}`,
method: 'get'
});
}
/**
* 获取采购申请附件列表
* @param purchaseId 采购申请ID
*/
export function getApplyFiles(purchaseId: string | number) {
return request({
url: '/purchase/purchasingfiles/applyFiles',
method: 'post',
params: { purchaseId }
});
}
/**
* 获取文件上传地址
*/
export function getFileUploadUrl() {
const baseUrl = import.meta.env.VITE_API_URL || '';
return `${baseUrl}/purchase/purchasingfiles/upload`;
}

View File

@@ -18,7 +18,7 @@
import request from '/@/utils/request'; import request from '/@/utils/request';
/** /**
* 获取采购文件列表(含历史版本) * 获取招标文件列表(含历史版本)
* @param applyId 采购申请ID * @param applyId 采购申请ID
*/ */
export function getDocList(applyId: number | string) { export function getDocList(applyId: number | string) {
@@ -29,7 +29,7 @@ export function getDocList(applyId: number | string) {
} }
/** /**
* 上传采购文件(招标代理) * 上传招标文件(招标代理)
* @param data 文件信息 * @param data 文件信息
*/ */
export function uploadDoc(data: any) { export function uploadDoc(data: any) {
@@ -41,7 +41,7 @@ export function uploadDoc(data: any) {
} }
/** /**
* 重新上传采购文件 * 重新上传招标文件
* @param data 文件信息 * @param data 文件信息
*/ */
export function reuploadDoc(data: any) { export function reuploadDoc(data: any) {
@@ -53,13 +53,25 @@ export function reuploadDoc(data: any) {
} }
/** /**
* 获取采购文件下载地址 * 获取招标文件下载地址
* @param id 采购文件ID * @param id 招标文件ID
*/ */
export function getDocDownloadUrl(id: number | string) { export function getDocDownloadUrl(id: number | string) {
return `/purchase/purchasingdoc/download/${id}`; return `/purchase/purchasingdoc/download/${id}`;
} }
/**
* 下载招标文件返回blob
* @param id 招标文件ID
*/
export function downloadDocById(id: number | string) {
return request({
url: `/purchase/purchasingdoc/download/${id}`,
method: 'get',
responseType: 'blob'
});
}
/** /**
* 确认无误 * 确认无误
* @param data 审核信息 * @param data 审核信息
@@ -128,4 +140,16 @@ export function getAvailableActions(applyId: number | string) {
url: '/purchase/purchasingdoc/actions/' + applyId, url: '/purchase/purchasingdoc/actions/' + applyId,
method: 'get' method: 'get'
}); });
}
/**
* 审核人员分页查询招标文件列表(只查询有招标文件记录的申请)
* @param params 分页和筛选参数
*/
export function getDocAuditPage(params?: any) {
return request({
url: '/purchase/purchasingdoc/audit/page',
method: 'get',
params
});
} }

View File

@@ -176,9 +176,9 @@ export function getContracts(params?: any) {
} }
/** /**
* 实施采购:上传采购文件并关联到申请单(可同时保存采购代表人方式与人员) * 实施采购:上传招标文件并关联到申请单(可同时保存采购代表人方式与人员)
* @param id 采购申请ID * @param id 采购申请ID
* @param fileIds 已上传的采购文件ID列表fileType=130 * @param fileIds 已上传的招标文件ID列表fileType=130
* @param implementType 实施采购方式 1:自行组织采购 2:委托代理采购 * @param implementType 实施采购方式 1:自行组织采购 2:委托代理采购
* @param representorTeacherNo 需求部门初审-指定采购代表人(单人) * @param representorTeacherNo 需求部门初审-指定采购代表人(单人)
* @param representors 需求部门初审-部门多人逗号分隔 * @param representors 需求部门初审-部门多人逗号分隔
@@ -198,7 +198,7 @@ export function implementApply(
} }
/** /**
* 发起采购文件审批流程(需已实施采购并上传采购文件) * 发起招标文件审批流程(需已实施采购并上传招标文件)
* @param id 采购申请ID * @param id 采购申请ID
* @param representorTeacherNo 需求部门初审-指定采购代表人单人用户ID或工号 * @param representorTeacherNo 需求部门初审-指定采购代表人单人用户ID或工号
* @param representors 需求部门初审-部门多人由系统抽取多人用户ID或工号逗号分隔 * @param representors 需求部门初审-部门多人由系统抽取多人用户ID或工号逗号分隔
@@ -256,7 +256,7 @@ export function getApplyTemplateDownloadUrl(id: string | number) {
} }
/** /**
* 下载文件审批表:导出采购文件审批表 Word 文档fileapply.docx 模板) * 下载文件审批表:导出招标文件审批表 Word 文档fileapply.docx 模板)
* @param id 采购申请ID * @param id 采购申请ID
*/ */
export function getFileApplyTemplateDownloadUrl(id: string | number) { export function getFileApplyTemplateDownloadUrl(id: string | number) {
@@ -300,7 +300,7 @@ export function getAgentApplyDetail(applyId: number | string) {
} }
/** /**
* 招标代理上传采购文件 * 招标代理上传招标文件
* @param data 文件数据 * @param data 文件数据
*/ */
export function uploadAgentDoc(data: any) { export function uploadAgentDoc(data: any) {
@@ -312,7 +312,7 @@ export function uploadAgentDoc(data: any) {
} }
/** /**
* 招标代理重新上传采购文件 * 招标代理重新上传招标文件
* @param data 文件数据 * @param data 文件数据
*/ */
export function reuploadAgentDoc(data: any) { export function reuploadAgentDoc(data: any) {
@@ -324,7 +324,7 @@ export function reuploadAgentDoc(data: any) {
} }
/** /**
* 获取采购文件列表 * 获取招标文件列表
* @param applyId 采购申请ID * @param applyId 采购申请ID
*/ */
export function getDocList(applyId: number | string) { export function getDocList(applyId: number | string) {

View File

@@ -99,14 +99,7 @@ export const staticRoutes: Array<RouteRecordRaw> = [
isAuth: true, isAuth: true,
}, },
}, },
{
path: '/purchase/purchasingrequisition/add',
name: 'purchasingrequisition.add',
component: () => import('/@/views/purchase/purchasingrequisition/add.vue'),
meta: {
isAuth: false, // 不需要认证,纯页面展示
},
},
...staticRoutesFlow ...staticRoutesFlow
]; ];

View File

@@ -15,6 +15,7 @@ export const useUserInfo = defineStore('userInfo', {
photo: '', photo: '',
time: 0, time: 0,
roles: [], roles: [],
roleCodes: [],
authBtnList: [], authBtnList: [],
}, },
}), }),
@@ -134,6 +135,7 @@ export const useUserInfo = defineStore('userInfo', {
user: res.data.sysUser, user: res.data.sysUser,
time: new Date().getTime(), time: new Date().getTime(),
roles: res.data.roles, roles: res.data.roles,
roleCodes: res.data.roleCodes || [],
authBtnList: res.data.permissions, authBtnList: res.data.permissions,
}; };
this.userInfos = userInfo; this.userInfos = userInfo;

View File

@@ -8,6 +8,7 @@ declare interface UserInfosState<T = any> {
authBtnList: string[]; authBtnList: string[];
photo: string; photo: string;
roles: string[]; roles: string[];
roleCodes: string[];
time: number; time: number;
userName: string; userName: string;
[key: string]: T; [key: string]: T;

View File

@@ -440,7 +440,7 @@
</el-col> </el-col>
</el-row> </el-row>
<div v-if="viewImplementPurchaseFiles.length" class="mb16"> <div v-if="viewImplementPurchaseFiles.length" class="mb16">
<div class="view-label mb8">采购文件</div> <div class="view-label mb8">招标文件</div>
<el-table :data="viewImplementPurchaseFiles" border size="small" max-height="240"> <el-table :data="viewImplementPurchaseFiles" border size="small" max-height="240">
<el-table-column type="index" label="版本" width="70" align="center"> <el-table-column type="index" label="版本" width="70" align="center">
<template #default="{ $index }">V{{ $index + 1 }}</template> <template #default="{ $index }">V{{ $index + 1 }}</template>
@@ -632,7 +632,7 @@ const dataForm = reactive({
agentId: '', agentId: '',
agentName: '', agentName: '',
}); });
/** 查看时展示的采购文件列表(实施采购上传的 type=130 */ /** 查看时展示的招标文件列表(实施采购上传的 type=130 */
const viewImplementPurchaseFiles = ref<{ id: string; fileTitle?: string; createTime?: string; remark?: string }[]>([]); const viewImplementPurchaseFiles = ref<{ id: string; fileTitle?: string; createTime?: string; remark?: string }[]>([]);
const categoryTreeData = ref<any[]>([]); const categoryTreeData = ref<any[]>([]);
const categoryCodePath = ref<string[]>([]); // 级联选择器的路径数组 const categoryCodePath = ref<string[]>([]); // 级联选择器的路径数组
@@ -652,7 +652,7 @@ const loading = ref(false);
const helpDialogVisible = ref(false); const helpDialogVisible = ref(false);
// 文件类型映射(对应数据库 file_type 字段) // 文件类型映射(对应数据库 file_type 字段)
// 10:商务洽谈纪要 20:市场采购纪要 30:网上商城采购相关材料 40:可行性论证报告 50:会议记录 60:其他材料 70:单一来源专家论证表 90:进口产品专家论证表 100:政府采购意向表 110:履约验收单 120:采购需求表 130:采购文件 // 10:商务洽谈纪要 20:市场采购纪要 30:网上商城采购相关材料 40:可行性论证报告 50:会议记录 60:其他材料 70:单一来源专家论证表 90:进口产品专家论证表 100:政府采购意向表 110:履约验收单 120:采购需求表 130:招标文件
const FILE_TYPE_MAP: Record<string, string> = { const FILE_TYPE_MAP: Record<string, string> = {
businessNegotiationTable: '10', // 商务洽谈纪要 businessNegotiationTable: '10', // 商务洽谈纪要
marketPurchaseMinutes: '20', // 市场采购纪要 marketPurchaseMinutes: '20', // 市场采购纪要
@@ -1286,10 +1286,10 @@ async function loadDetail(applyId: string | number) {
}); });
} }
}); });
// 查看时展示实施采购的采购文件列表type=130 // 查看时展示实施采购的招标文件列表type=130
const purchaseFiles = fileList.filter((f: any) => String(f.fileType) === '130').map((f: any) => ({ const purchaseFiles = fileList.filter((f: any) => String(f.fileType) === '130').map((f: any) => ({
id: f.id, id: f.id,
fileTitle: f.fileTitle || f.file_title || '采购文件', fileTitle: f.fileTitle || f.file_title || '招标文件',
createTime: f.createTime || f.create_time, createTime: f.createTime || f.create_time,
remark: f.remark, remark: f.remark,
})); }));
@@ -1318,13 +1318,13 @@ function downloadImplementFile(file: { id?: string; remark?: string; fileTitle?:
// 优先使用文件ID下载 // 优先使用文件ID下载
if (file?.id) { if (file?.id) {
const url = `/purchase/purchasingfiles/downloadById?fileId=${encodeURIComponent(file.id)}`; const url = `/purchase/purchasingfiles/downloadById?fileId=${encodeURIComponent(file.id)}`;
other.downBlobFile(url, {}, file.fileTitle || '采购文件'); other.downBlobFile(url, {}, file.fileTitle || '招标文件');
return; return;
} }
// 兼容旧数据:使用文件路径下载 // 兼容旧数据:使用文件路径下载
if (file?.remark) { if (file?.remark) {
const url = `/purchase/purchasingfiles/download?fileName=${encodeURIComponent(file.remark)}&fileTitle=${encodeURIComponent(file.fileTitle || '采购文件')}`; const url = `/purchase/purchasingfiles/download?fileName=${encodeURIComponent(file.remark)}&fileTitle=${encodeURIComponent(file.fileTitle || '招标文件')}`;
other.downBlobFile(url, {}, file.fileTitle || '采购文件'); other.downBlobFile(url, {}, file.fileTitle || '招标文件');
return; return;
} }
useMessage().warning('无法获取文件信息'); useMessage().warning('无法获取文件信息');

View File

@@ -1,397 +0,0 @@
<template>
<el-dialog
v-model="visible"
:title="'处理项目 - ' + (projectInfo.purchaseNo || '')"
width="900px"
:close-on-click-modal="false"
destroy-on-close
class="agent-doc-dialog">
<el-tabs v-model="activeTab" type="border-card">
<!-- Tab 1: 项目基本信息 -->
<el-tab-pane label="项目信息" name="info">
<el-descriptions :column="2" border>
<el-descriptions-item label="采购编号">{{ projectInfo.purchaseNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="项目名称">{{ projectInfo.projectName || '-' }}</el-descriptions-item>
<el-descriptions-item label="文件状态">
<el-tag :type="getStatusType(projectInfo.status)">{{ getStatusLabel(projectInfo.status) }}</el-tag>
</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<!-- Tab 2: 采购需求文件 -->
<el-tab-pane label="采购需求文件" name="requirement">
<el-table :data="requirementFiles" v-loading="requirementLoading" stripe>
<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 label="操作" width="120" align="center">
<template #default="scope">
<el-button type="primary" link icon="Download" @click="handleDownload(scope.row)">下载</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!requirementLoading && requirementFiles.length === 0" description="暂无采购需求文件" />
</el-tab-pane>
<!-- Tab 3: 上传招标文件 -->
<el-tab-pane label="上传招标文件" name="upload">
<div class="upload-section">
<el-alert type="info" :closable="false" class="mb-4">
<template #title>
<span>当前状态<el-tag :type="getStatusType(projectInfo.status)" size="small">{{ getStatusLabel(projectInfo.status) }}</el-tag></span>
</template>
</el-alert>
<!-- 已上传的采购文件列表 -->
<div v-if="uploadedDocList.length > 0" class="uploaded-docs mb-4">
<div class="section-title">已上传的采购文件</div>
<el-table :data="uploadedDocList" stripe size="small">
<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" align="center" />
<el-table-column prop="uploadTime" label="上传时间" width="160" align="center" />
<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>
</div>
<!-- 文件上传区域 -->
<div v-if="canUpload" class="upload-area">
<div class="section-title">{{ projectInfo.status === 'RETURNED' ? '重新上传招标文件' : '上传招标文件' }}</div>
<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"
accept=".doc,.docx,.pdf"
class="upload-demo">
<el-button type="primary">选择文件</el-button>
<template #tip>
<div class="el-upload__tip">支持 .doc, .docx, .pdf 格式文件大小不超过 50MB</div>
</template>
</el-upload>
<div class="action-buttons mt-4">
<el-button
type="success"
:loading="uploadSubmitting"
:disabled="fileList.length === 0"
@click="handleUploadSubmit">
{{ projectInfo.status === 'RETURNED' ? '重新上传并提交' : '上传并提交' }}
</el-button>
</div>
</div>
<el-alert v-if="!canUpload" type="warning" :closable="false" class="mt-4">
<template #title>
<span v-if="projectInfo.status === 'ASSET_REVIEWING'">文件审核中请等待审核结果</span>
<span v-else-if="projectInfo.status === 'DEPT_REVIEWING'">文件审核中请等待审核结果</span>
<span v-else-if="projectInfo.status === 'AUDIT_REVIEWING'">文件审核中请等待审核结果</span>
<span v-else-if="projectInfo.status === 'ASSET_CONFIRMING'">文件审核中请等待确认</span>
<span v-else-if="projectInfo.status === 'COMPLETED'">文件审核已完成</span>
<span v-else>当前状态不允许上传</span>
</template>
</el-alert>
</div>
</el-tab-pane>
</el-tabs>
<template #footer>
<el-button @click="handleClose">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="AgentDocDialog">
import { ref, computed } from 'vue'
import { useMessage } from '/@/hooks/message'
import { Session } from '/@/utils/storage'
import { getAgentRequirementFiles, getAgentApplyDetail, getDocList, downloadFileById, uploadAgentDoc, reuploadAgentDoc } from '/@/api/purchase/purchasingrequisition'
import type { UploadInstance, UploadProps, UploadUserFile } from 'element-plus'
const emit = defineEmits(['refresh'])
const visible = ref(false)
const activeTab = ref('info')
const projectInfo = ref<any>({})
const requirementFiles = ref<any[]>([])
const requirementLoading = ref(false)
const uploadSubmitting = ref(false)
const uploadRef = ref<UploadInstance>()
const fileList = ref<UploadUserFile[]>([])
const uploadedDocList = ref<any[]>([])
const docListLoading = ref(false)
// 上传配置 - 使用采购文件上传接口
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: projectInfo.value.applyId || ''
}))
// 是否可以上传
const canUpload = computed(() => {
const status = projectInfo.value.status
return status === 'PENDING_UPLOAD' || status === 'RETURNED'
})
const open = async (row: any) => {
projectInfo.value = { ...row }
visible.value = true
activeTab.value = 'info'
fileList.value = []
uploadedDocList.value = []
await loadProjectDetail()
await loadRequirementFiles()
await loadUploadedDocList()
}
const loadProjectDetail = async () => {
try {
const res = await getAgentApplyDetail(projectInfo.value.applyId)
if (res?.data) {
projectInfo.value = { ...projectInfo.value, ...res.data }
}
} catch (e: any) {
console.error('加载项目详情失败', e)
}
}
const loadRequirementFiles = async () => {
requirementLoading.value = true
try {
const res = await getAgentRequirementFiles(projectInfo.value.applyId)
requirementFiles.value = res?.data || []
} catch (e: any) {
requirementFiles.value = []
} finally {
requirementLoading.value = false
}
}
const loadUploadedDocList = async () => {
docListLoading.value = true
try {
const res = await getDocList(projectInfo.value.applyId)
uploadedDocList.value = res?.data || []
} catch (e: any) {
uploadedDocList.value = []
} finally {
docListLoading.value = false
}
}
const handleDownload = 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 baseUrl = import.meta.env.VITE_API_URL || ''
const token = Session.getToken()
const tenantId = Session.getTenant() || '1'
const url = `${baseUrl}/purchase/purchasingdoc/download/${row.id}?Authorization=Bearer ${token}&TENANT-ID=${tenantId}`
window.open(url, '_blank')
} 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: projectInfo.value.applyId,
fileName: fileData.fileTitle || uploadFile.name,
filePath: fileData.remark || fileData.filePath
}
let submitRes
if (projectInfo.value.status === 'RETURNED') {
submitRes = await reuploadAgentDoc(submitData)
} else {
submitRes = await uploadAgentDoc(submitData)
}
if (submitRes?.code === 0 || submitRes?.code === 200) {
useMessage().success('招标文件提交成功')
emit('refresh')
await loadProjectDetail()
await loadUploadedDocList()
fileList.value = []
} 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 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] || '-'
}
defineExpose({ open })
</script>
<style scoped lang="scss">
.agent-doc-dialog {
:deep(.el-dialog__body) {
padding: 16px 20px;
}
}
.upload-section {
padding: 16px 0;
}
.mb-4 {
margin-bottom: 16px;
}
.mt-4 {
margin-top: 16px;
}
.section-title {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-left: 8px;
border-left: 3px solid #409eff;
}
.uploaded-docs {
background-color: #f5f7fa;
padding: 12px;
border-radius: 4px;
}
.upload-area {
background-color: #fff;
padding: 12px;
border: 1px dashed #dcdfe6;
border-radius: 4px;
}
.action-buttons {
display: flex;
justify-content: flex-start;
gap: 12px;
}
</style>

View File

@@ -1,166 +0,0 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- 搜索表单卡片 -->
<el-card v-show="showSearch" class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
筛选条件
</span>
</div>
</template>
<el-form :model="state.queryForm" ref="searchFormRef" :inline="true" @keyup.enter="getDataList" class="search-form">
<el-form-item label="采购编号" prop="purchaseNo">
<el-input
v-model="state.queryForm.purchaseNo"
placeholder="请输入采购编号"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="项目名称" prop="projectName">
<el-input
v-model="state.queryForm.projectName"
placeholder="请输入项目名称"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="getDataList">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 内容卡片 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><DocumentCopy /></el-icon>
招标代理工作台
</span>
<div class="header-actions">
<right-toolbar v-model:showSearch="showSearch" class="ml10" @queryTable="getDataList" />
</div>
</div>
</template>
<!-- 表格 -->
<el-table
ref="tableRef"
:data="state.dataList"
v-loading="state.loading"
stripe
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table">
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
</el-table-column>
<el-table-column prop="purchaseNo" label="采购编号" min-width="140" show-overflow-tooltip />
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="status" label="文件状态" width="140" align="center">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)">
{{ getStatusLabel(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="120">
<template #default="scope">
<el-button type="primary" link icon="View" @click="handleView(scope.row)">
处理
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<pagination
v-if="state.pagination && state.pagination.total && state.pagination.total > 0"
:total="state.pagination.total"
:current="state.pagination.current"
:size="state.pagination.size"
@sizeChange="sizeChangeHandle"
@currentChange="currentChangeHandle"
/>
</el-card>
</div>
<!-- 处理弹窗 -->
<AgentDocDialog ref="agentDocDialogRef" @refresh="getDataList" />
</div>
</template>
<script setup lang="ts" name="PurchasingAgentDoc">
import { ref, reactive, defineAsyncComponent, onMounted } from 'vue'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { useMessage } from "/@/hooks/message";
import { getAgentPendingList } from "/@/api/purchase/purchasingrequisition";
import { Search, DocumentCopy, List } from '@element-plus/icons-vue'
// 引入组件
const AgentDocDialog = defineAsyncComponent(() => import('./AgentDocDialog.vue'));
const agentDocDialogRef = ref()
const searchFormRef = ref()
const showSearch = ref(true)
const state: BasicTableProps = reactive<BasicTableProps>({
pageList: getAgentPendingList,
queryForm: {
purchaseNo: '',
projectName: '',
},
createdIsNeed: true
});
const { getDataList, tableStyle, sizeChangeHandle, currentChangeHandle } = useTable(state);
const handleReset = () => {
searchFormRef.value?.resetFields();
getDataList();
};
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 handleView = (row: any) => {
agentDocDialogRef.value?.open(row);
};
onMounted(() => {
// 页面加载时的初始化逻辑
});
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>

View File

@@ -1,323 +0,0 @@
<template>
<el-dialog
v-model="visible"
title="采购文件审核"
width="900px"
destroy-on-close
@close="handleClose">
<el-tabs v-model="activeTab">
<!-- 项目信息 -->
<el-tab-pane label="项目信息" name="info">
<el-descriptions :column="2" border>
<el-descriptions-item label="采购编号">{{ applyInfo.purchaseNo }}</el-descriptions-item>
<el-descriptions-item label="项目名称">{{ applyInfo.projectName }}</el-descriptions-item>
<el-descriptions-item label="需求部门">{{ applyInfo.deptName }}</el-descriptions-item>
<el-descriptions-item label="预算金额">{{ applyInfo.budget ? Number(applyInfo.budget).toLocaleString() + '元' : '-' }}</el-descriptions-item>
<el-descriptions-item label="招标代理">{{ applyInfo.agentName || '-' }}</el-descriptions-item>
<el-descriptions-item label="审核状态">
<el-tag :type="getStatusType(applyInfo.docAuditStatus)">
{{ getStatusLabel(applyInfo.docAuditStatus) }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<!-- 采购需求文件 -->
<el-tab-pane label="采购需求文件" name="requirement">
<el-table :data="requirementFiles" stripe v-loading="requirementLoading">
<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-tab-pane>
<!-- 采购文件 -->
<el-tab-pane label="采购文件" name="doc">
<div class="doc-header">
<el-button v-if="canUpload" type="primary" icon="Upload" @click="handleUpload">
{{ applyInfo.docAuditStatus === 'RETURNED' ? '重新上传' : '上传文件' }}
</el-button>
</div>
<el-table :data="docList" stripe v-loading="docLoading">
<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="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-tab-pane>
<!-- 审核记录 -->
<el-tab-pane label="审核记录" name="audit">
<AuditRecordList :apply-id="applyInfo.id" 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">
import { ref, computed, defineAsyncComponent } from 'vue'
import { useMessage, useMessageBox } from '/@/hooks/message'
import { getObj, getApplyFiles } from '/@/api/purchase/purchasingrequisition'
import { getDocList, uploadDoc, reuploadDoc, confirmDoc, returnDoc, completeDoc, getAvailableActions, getDocDownloadUrl } from '/@/api/purchase/purchasingdoc'
import other from '/@/utils/other'
const AuditRecordList = defineAsyncComponent(() => import('./AuditRecordList.vue'));
const emit = defineEmits(['refresh']);
const visible = ref(false)
const activeTab = ref('info')
const applyInfo = 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 returnDialogVisible = ref(false)
const returnRemark = ref('')
const canUpload = computed(() => availableActions.value.includes('upload'))
const canConfirm = computed(() => availableActions.value.includes('confirm'))
const canReturn = computed(() => availableActions.value.includes('return'))
const canComplete = computed(() => availableActions.value.includes('complete'))
const open = async (row: any) => {
visible.value = true
activeTab.value = 'info'
applyInfo.value = {}
requirementFiles.value = []
docList.value = []
returnRemark.value = ''
// 加载详情
try {
const res = await getObj(row.id)
applyInfo.value = res.data || res
} catch (e: any) {
useMessage().error(e?.msg || '加载项目信息失败')
return
}
// 加载可执行操作
try {
const actionsRes = await getAvailableActions(row.id)
availableActions.value = actionsRes.data || []
} catch (e) {
availableActions.value = []
}
// 加载采购需求文件
loadRequirementFiles()
// 加载采购文件
loadDocList()
}
const loadRequirementFiles = async () => {
if (!applyInfo.value.id) return
requirementLoading.value = true
try {
const res = await getApplyFiles(applyInfo.value.id)
const files = res.data || res || []
// 过滤采购需求文件fileType=120
requirementFiles.value = files.filter((f: any) => f.fileType === '120')
} catch (e) {
requirementFiles.value = []
} finally {
requirementLoading.value = false
}
}
const loadDocList = async () => {
if (!applyInfo.value.id) return
docLoading.value = true
try {
const res = await getDocList(applyInfo.value.id)
docList.value = res.data || []
} catch (e) {
docList.value = []
} finally {
docLoading.value = false
}
}
const handleUpload = () => {
// 触发文件上传
const input = document.createElement('input')
input.type = 'file'
input.accept = '*/*'
input.onchange = async (e: any) => {
const file = e.target.files[0]
if (!file) return
// TODO: 实现文件上传到OSS然后调用上传接口
useMessage().info('文件上传功能需要配合OSS实现')
}
input.click()
}
const handleDownloadRequirement = (row: any) => {
if (row.remark) {
const url = `/purchase/purchasingfiles/download?fileName=${encodeURIComponent(row.remark)}&fileTitle=${encodeURIComponent(row.fileTitle)}`
other.downBlobFile(url, {}, row.fileTitle)
}
}
const handleDownloadDoc = (row: any) => {
const url = getDocDownloadUrl(row.id)
other.downBlobFile(url, {}, row.fileName)
}
const handleConfirm = async () => {
try {
await useMessageBox().confirm('确定要确认该采购文件无误吗?')
} catch {
return
}
try {
await confirmDoc({ applyId: applyInfo.value.id })
useMessage().success('确认成功')
emit('refresh')
loadDocList()
auditRecordListRef.value?.refresh()
// 重新加载可执行操作
const actionsRes = await getAvailableActions(applyInfo.value.id)
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: applyInfo.value.id, remark: returnRemark.value })
useMessage().success('退回成功')
returnDialogVisible.value = false
emit('refresh')
loadDocList()
auditRecordListRef.value?.refresh()
// 重新加载可执行操作
const actionsRes = await getAvailableActions(applyInfo.value.id)
availableActions.value = actionsRes.data || []
} catch (e: any) {
useMessage().error(e?.msg || '退回失败')
}
}
const handleComplete = async () => {
try {
await useMessageBox().confirm('确定要确认流程结束吗?')
} catch {
return
}
try {
await completeDoc(applyInfo.value.id)
useMessage().success('流程已结束')
emit('refresh')
loadDocList()
auditRecordListRef.value?.refresh()
// 重新加载可执行操作
const actionsRes = await getAvailableActions(applyInfo.value.id)
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;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
}
</style>

View File

@@ -21,7 +21,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import { getAuditRecords } from '/@/api/purchase/purchasingdoc' import { getAuditRecords } from '/@/api/purchase/docProcess'
const props = defineProps<{ const props = defineProps<{
applyId: number | string applyId: number | string

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>

View File

@@ -26,7 +26,8 @@
clearable clearable
style="width: 200px" /> style="width: 200px" />
</el-form-item> </el-form-item>
<el-form-item label="审核状态" prop="docAuditStatus"> <!-- 审核状态筛选 - 仅审核模式显示 -->
<el-form-item v-if="mode === 'audit'" label="审核状态" prop="docAuditStatus">
<el-select <el-select
v-model="state.queryForm.docAuditStatus" v-model="state.queryForm.docAuditStatus"
placeholder="请选择审核状态" placeholder="请选择审核状态"
@@ -53,8 +54,10 @@
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span class="card-title"> <span class="card-title">
<el-icon class="title-icon"><DocumentChecked /></el-icon> <el-icon class="title-icon">
采购文件审核 <component :is="titleIcon" />
</el-icon>
{{ pageTitle }}
</span> </span>
<div class="header-actions"> <div class="header-actions">
<right-toolbar v-model:showSearch="showSearch" class="ml10" @queryTable="getDataList" /> <right-toolbar v-model:showSearch="showSearch" class="ml10" @queryTable="getDataList" />
@@ -78,28 +81,33 @@
</el-table-column> </el-table-column>
<el-table-column prop="purchaseNo" label="采购编号" min-width="140" show-overflow-tooltip /> <el-table-column prop="purchaseNo" label="采购编号" min-width="140" show-overflow-tooltip />
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip /> <el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="deptName" label="需求部门" min-width="150" show-overflow-tooltip /> <!-- 需求部门 - 仅审核模式显示 -->
<el-table-column prop="budget" label="预算金额(元)" width="120" align="right"> <el-table-column v-if="mode === 'audit'" prop="deptName" label="需求部门" min-width="150" show-overflow-tooltip />
<!-- 预算金额 - 仅审核模式显示 -->
<el-table-column v-if="mode === 'audit'" prop="budget" label="预算金额(元)" width="120" align="right">
<template #default="scope"> <template #default="scope">
{{ scope.row.budget ? Number(scope.row.budget).toLocaleString() : '-' }} {{ scope.row.budget ? Number(scope.row.budget).toLocaleString() : '-' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="docAuditStatus" label="审核状态" width="140" align="center"> <!-- 文件状态 -->
<el-table-column :prop="statusProp" label="文件状态" width="140" align="center">
<template #default="scope"> <template #default="scope">
<el-tag :type="getStatusType(scope.row.docAuditStatus)"> <el-tag :type="getStatusType(getRowStatus(scope.row))">
{{ getStatusLabel(scope.row.docAuditStatus) }} {{ getStatusLabel(getRowStatus(scope.row)) }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="currentDocVersion" label="当前版本" width="100" align="center"> <!-- 当前版本 - 仅审核模式显示 -->
<el-table-column v-if="mode === 'audit'" prop="currentDocVersion" label="当前版本" width="100" align="center">
<template #default="scope"> <template #default="scope">
{{ scope.row.currentDocVersion || '-' }} {{ scope.row.currentDocVersion || '-' }}
</template> </template>
</el-table-column> </el-table-column>
<!-- 操作 -->
<el-table-column label="操作" align="center" fixed="right" width="120"> <el-table-column label="操作" align="center" fixed="right" width="120">
<template #default="scope"> <template #default="scope">
<el-button type="primary" link icon="View" @click="handleAudit(scope.row)"> <el-button type="primary" link icon="View" @click="handleProcess(scope.row)">
审核 {{ actionLabel }}
</el-button> </el-button>
</template> </template>
</el-table-column> </el-table-column>
@@ -117,32 +125,73 @@
</el-card> </el-card>
</div> </div>
<!-- 审核弹窗 --> <!-- 处理弹窗 -->
<DocAuditDialog ref="docAuditDialogRef" @refresh="getDataList" /> {{mode}}
<DocProcessDialog ref="docProcessDialogRef" :mode="mode" @refresh="getDataList" />
</div> </div>
</template> </template>
<script setup lang="ts" name="PurchasingDocAudit"> <script setup lang="ts" name="PurchasingDocProcess">
import { ref, reactive, defineAsyncComponent, onMounted } from 'vue' import { ref, reactive, computed, defineAsyncComponent, onMounted } from 'vue'
import { BasicTableProps, useTable } from "/@/hooks/table"; import { BasicTableProps, useTable } from "/@/hooks/table";
import { useMessage } from "/@/hooks/message"; import { getDocProcessList } from "/@/api/purchase/docProcess";
import { getPage } from "/@/api/purchase/purchasingrequisition"; import { Search, DocumentCopy, DocumentChecked, List } from '@element-plus/icons-vue'
import { Search, DocumentChecked, List } from '@element-plus/icons-vue' import { useUserInfo } from '/@/stores/userInfo';
// //
const DocAuditDialog = defineAsyncComponent(() => import('./DocAuditDialog.vue')); const DocProcessDialog = defineAsyncComponent(() => import('./DocProcessDialog.vue'));
const docAuditDialogRef = ref() const userInfoStore = useUserInfo()
const docProcessDialogRef = ref()
const searchFormRef = ref() const searchFormRef = ref()
const showSearch = ref(true) const showSearch = ref(true)
const state: BasicTableProps = reactive<BasicTableProps>({ //
pageList: getPage, const mode = computed(() => {
queryForm: { const roleCodes = userInfoStore.userInfos.roleCodes || [];
// PURCHASE_AGENT
if (roleCodes.includes('PURCHASE_AGENT')) {
return 'agent';
}
//
return 'audit';
})
//
const pageTitle = computed(() => {
return mode.value === 'agent' ? '招标代理工作台' : '招标文件审核'
})
//
const titleIcon = computed(() => {
return mode.value === 'agent' ? DocumentCopy : DocumentChecked
})
//
const actionLabel = computed(() => {
return mode.value === 'agent' ? '处理' : '审核'
})
//
const statusProp = computed(() => {
return mode.value === 'agent' ? 'status' : 'docAuditStatus'
})
//
const getQueryForm = () => {
const base: any = {
purchaseNo: '', purchaseNo: '',
projectName: '', projectName: '',
docAuditStatus: '', }
}, if (mode.value === 'audit') {
base.docAuditStatus = ''
}
return base
}
const state: BasicTableProps = reactive<BasicTableProps>({
pageList: (params?: any) => getDocProcessList(mode.value, params),
queryForm: getQueryForm(),
createdIsNeed: true createdIsNeed: true
}); });
@@ -153,6 +202,11 @@ const handleReset = () => {
getDataList(); getDataList();
}; };
//
const getRowStatus = (row: any) => {
return mode.value === 'agent' ? row.status : row.docAuditStatus
}
const getStatusType = (status: string) => { const getStatusType = (status: string) => {
const typeMap: Record<string, string> = { const typeMap: Record<string, string> = {
'PENDING_UPLOAD': 'info', 'PENDING_UPLOAD': 'info',
@@ -179,12 +233,14 @@ const getStatusLabel = (status: string) => {
return labelMap[status] || '-'; return labelMap[status] || '-';
}; };
const handleAudit = (row: any) => { const handleProcess = (row: any) => {
docAuditDialogRef.value?.open(row); docProcessDialogRef.value?.open(row);
}; };
//
onMounted(() => { onMounted(() => {
// //
state.queryForm = getQueryForm()
}); });
</script> </script>

View File

@@ -292,8 +292,8 @@
<!-- 实施采购iframe 嵌入 implement.vue供列表与流程页面使用 --> <!-- 实施采购iframe 嵌入 implement.vue供列表与流程页面使用 -->
<ImplementForm ref="implementFormRef" @refresh="getDataList" /> <ImplementForm ref="implementFormRef" @refresh="getDataList" />
<!-- 采购文件审核弹窗 --> <!-- 招标文件审核弹窗 -->
<DocAuditDialog ref="docAuditDialogRef" @refresh="getDataList" /> <!-- <DocAuditDialog ref="docAuditDialogRef" @refresh="getDataList" />-->
<!-- 采购代表弹窗 --> <!-- 采购代表弹窗 -->
<el-dialog <el-dialog
@@ -365,7 +365,7 @@ const ImplementForm = defineAsyncComponent(() => import('./implementForm.vue'));
const ActionDropdown = defineAsyncComponent(() => import('/@/components/tools/action-dropdown.vue')); const ActionDropdown = defineAsyncComponent(() => import('/@/components/tools/action-dropdown.vue'));
const PurchasingAcceptModal = defineAsyncComponent(() => import('./accept/PurchasingAcceptModal.vue')); const PurchasingAcceptModal = defineAsyncComponent(() => import('./accept/PurchasingAcceptModal.vue'));
const FlowCommentTimeline = defineAsyncComponent(() => import('/@/views/jsonflow/comment/timeline.vue')); const FlowCommentTimeline = defineAsyncComponent(() => import('/@/views/jsonflow/comment/timeline.vue'));
const DocAuditDialog = defineAsyncComponent(() => import('./docAudit/DocAuditDialog.vue')); // const DocAuditDialog = defineAsyncComponent(() => import('./docAudit/DocAuditDialog.vue'));
// 字典数据和品目树数据 // 字典数据和品目树数据
const dictData = ref({ const dictData = ref({
@@ -537,7 +537,7 @@ const handleImplement = (row: any) => {
implementFormRef.value?.openDialog(row); implementFormRef.value?.openDialog(row);
}; };
/** 打开采购文件审核 */ /** 打开招标文件审核 */
const handleDocAudit = (row: any) => { const handleDocAudit = (row: any) => {
docAuditDialogRef.value?.open(row); docAuditDialogRef.value?.open(row);
}; };
@@ -639,7 +639,7 @@ const getActionMenuItems = (row: any) => {
// }, // },
// { // {
// command: 'docAudit', // command: 'docAudit',
// label: '采购文件审核', // label: '招标文件审核',
// icon: DocumentChecked, // icon: DocumentChecked,
// visible: () => row?.implementType === '2' && row?.agentId, // visible: () => row?.implementType === '2' && row?.agentId,
// }, // },