This commit is contained in:
吴红兵
2026-03-07 17:32:17 +08:00
parent 1cf529cead
commit 15b3efe51e
4 changed files with 593 additions and 0 deletions

View File

@@ -392,3 +392,27 @@ export function updateFiles(data: { purchaseId: string; fileIds: string[] }) {
data, data,
}); });
} }
export function tempStoreSupplement(applyId: string, fileIds: string[]) {
return request({
url: '/purchase/purchasingapply/supplement/temp-store',
method: 'post',
data: { applyId, fileIds },
});
}
export function submitSupplement(applyId: string) {
return request({
url: '/purchase/purchasingapply/supplement/submit',
method: 'post',
data: { id: applyId },
});
}
export function getSupplementFileType(purchaseType: string) {
return request({
url: '/purchase/purchasingapply/supplement/file-type',
method: 'get',
params: { purchaseType },
});
}

View File

@@ -0,0 +1,249 @@
<template>
<el-dialog v-model="visible" title="补充材料" width="900px" destroy-on-close append-to-body :close-on-click-modal="false">
<template #header>
<div class="dialog-header">
<span class="dialog-title">
<el-icon><Upload /></el-icon>
补充材料 - {{ applyData.purchaseNo || applyId }}
</span>
</div>
</template>
<el-descriptions :column="2" border class="apply-info">
<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="采购方式">{{ purchaseTypeLabelComputed || '-' }}</el-descriptions-item>
<el-descriptions-item label="采购内容" :span="2">{{ applyData.projectContent || '-' }}</el-descriptions-item>
</el-descriptions>
<el-divider content-position="left">采购相关文件补充上传</el-divider>
<el-alert
:type="fileTypeAlertType"
:closable="false"
style="margin-bottom: 16px"
>
<template #title>
<span v-if="requiredFileType">{{ requiredFileTypeName }}必传</span>
<span v-else>当前采购方式无需补充材料</span>
</template>
</el-alert>
<el-form v-if="requiredFileType" label-width="140px">
<el-form-item :label="requiredFileTypeName" required>
<upload-file
v-model="fileList"
:limit="1"
:file-type="['pdf']"
:data="{ fileType: requiredFileType, purchaseId: applyId }"
upload-file-url="/purchase/purchasingfiles/upload"
/>
<div class="file-tips">{{ fileTips }}</div>
</el-form-item>
</el-form>
<el-empty v-else description="当前采购方式无需补充材料" />
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="submitting" :disabled="!requiredFileType" @click="handleTempStore">暂存</el-button>
<el-button type="success" :loading="submitting" :disabled="!canSubmit" @click="handleSubmit">提交</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { Upload } from '@element-plus/icons-vue';
import { useMessage } from '/@/hooks/message';
import { getObj, tempStoreSupplement, submitSupplement, getSupplementFileType, getApplyFiles } from '/@/api/purchase/purchasingrequisition';
import { getDicts } from '/@/api/admin/dict';
interface FileItem {
id: string;
name: string;
url: string;
}
const visible = ref(false);
const submitting = ref(false);
const applyId = ref('');
const applyData = ref<any>({});
const requiredFileType = ref('');
const fileList = ref<FileItem[]>([]);
const purchaseTypeDeptList = ref<any[]>([]);
const PURCHASE_TYPE_DEPT_MAP: Record<string, string> = {
'1': '网上商城',
'2': '市场采购',
'3': '商务洽谈',
'5': '询价',
};
const purchaseTypeLabelComputed = computed(() => {
if (applyData.value.purchaseTypeLabel) {
return applyData.value.purchaseTypeLabel;
}
if (applyData.value.purchaseType) {
const found = purchaseTypeDeptList.value.find((item: any) => item.value === applyData.value.purchaseType);
if (found) {
return found.label;
}
return PURCHASE_TYPE_DEPT_MAP[applyData.value.purchaseType] || applyData.value.purchaseType;
}
return '';
});
const fileTypeMap: Record<string, { name: string; tips: string }> = {
'10': { name: '商务洽谈纪要', tips: '请上传商务洽谈相关纪要文件PDF格式' },
'20': { name: '市场采购纪要', tips: '请上传市场采购相关纪要文件PDF格式' },
'30': { name: '网上商城采购材料', tips: '请上传网上商城采购相关材料PDF格式' },
'150': { name: '部门采购询价表', tips: '请上传部门采购询价表PDF格式' },
};
const requiredFileTypeName = computed(() => {
if (!requiredFileType.value) return '';
return fileTypeMap[requiredFileType.value]?.name || '补充材料';
});
const fileTips = computed(() => {
if (!requiredFileType.value) return '';
return fileTypeMap[requiredFileType.value]?.tips || '请上传PDF格式文件';
});
const fileTypeAlertType = computed(() => {
return requiredFileType.value ? 'warning' : 'info';
});
const canSubmit = computed(() => {
return requiredFileType.value && fileList.value.length > 0;
});
const loadDictData = async () => {
try {
const res = await getDicts('PURCHASE_TYPE_DEPT');
if (res.data && Array.isArray(res.data)) {
purchaseTypeDeptList.value = res.data.map((item: any) => ({
label: item.label || item.dictLabel || item.name,
value: item.value || item.dictValue || item.code,
}));
}
} catch (e) {
console.error('加载字典数据失败', e);
}
};
const open = async (id: string) => {
applyId.value = id || '';
visible.value = true;
fileList.value = [];
requiredFileType.value = '';
applyData.value = {};
await loadDictData();
try {
const res = await getObj(id);
const data = res?.data || {};
applyData.value = data;
if (data.purchaseType) {
const fileTypeRes = await getSupplementFileType(data.purchaseType);
requiredFileType.value = fileTypeRes?.data || '';
}
if (requiredFileType.value) {
const filesRes = await getApplyFiles(id);
const files = filesRes?.data || [];
const matchingFiles = files.filter((f: any) => String(f.fileType) === requiredFileType.value);
if (matchingFiles.length > 0) {
fileList.value = matchingFiles.map((f: any) => ({
id: f.id,
name: f.fileTitle || f.fileName || '附件',
url: f.fileUrl,
}));
}
}
} catch (e) {
console.error('获取采购申请详情失败', e);
useMessage().error('获取采购申请详情失败');
}
};
const handleTempStore = async () => {
if (!applyId.value) {
useMessage().warning('无法获取采购申请ID');
return;
}
const fileIds = fileList.value.filter((f) => f.id).map((f) => f.id);
if (fileIds.length === 0) {
useMessage().warning('请先上传补充材料');
return;
}
submitting.value = true;
try {
await tempStoreSupplement(applyId.value, fileIds);
useMessage().success('暂存成功');
} catch (err: any) {
useMessage().error(err?.msg || '暂存失败');
} finally {
submitting.value = false;
}
};
const handleSubmit = async () => {
if (!applyId.value) {
useMessage().warning('无法获取采购申请ID');
return;
}
submitting.value = true;
try {
await submitSupplement(applyId.value);
useMessage().success('提交成功,流程已启动');
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err?.msg || '提交失败');
} finally {
submitting.value = false;
}
};
const emit = defineEmits<{
(e: 'refresh'): void;
}>();
defineExpose({
open,
});
</script>
<style scoped lang="scss">
.dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.dialog-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 500;
}
.apply-info {
margin-bottom: 16px;
}
.file-tips {
font-size: 12px;
color: #909399;
}
</style>

View File

@@ -292,6 +292,9 @@
<!-- 合同弹窗 --> <!-- 合同弹窗 -->
<ContractDialog ref="contractDialogRef" @refresh="getDataList" /> <ContractDialog ref="contractDialogRef" @refresh="getDataList" />
<!-- 补充材料弹窗 -->
<SupplementFilesDialog ref="supplementFilesDialogRef" @refresh="getDataList" />
<!-- 招标文件审核弹窗 --> <!-- 招标文件审核弹窗 -->
<!-- <DocAuditDialog ref="docAuditDialogRef" @refresh="getDataList" />--> <!-- <DocAuditDialog ref="docAuditDialogRef" @refresh="getDataList" />-->
@@ -405,6 +408,7 @@ const FlowCommentTimeline = defineAsyncComponent(() => import('/@/views/jsonflow
const FileArchiveDialog = defineAsyncComponent(() => import('./FileArchiveDialog.vue')); const FileArchiveDialog = defineAsyncComponent(() => import('./FileArchiveDialog.vue'));
const UpdateFilesDialog = defineAsyncComponent(() => import('./UpdateFilesDialog.vue')); const UpdateFilesDialog = defineAsyncComponent(() => import('./UpdateFilesDialog.vue'));
const ContractDialog = defineAsyncComponent(() => import('./contract/ContractDialog.vue')); const ContractDialog = defineAsyncComponent(() => import('./contract/ContractDialog.vue'));
const SupplementFilesDialog = defineAsyncComponent(() => import('./SupplementFilesDialog.vue'));
// const DocAuditDialog = defineAsyncComponent(() => import('./docAudit/DocAuditDialog.vue')); // const DocAuditDialog = defineAsyncComponent(() => import('./docAudit/DocAuditDialog.vue'));
// 字典数据和品目树数据 // 字典数据和品目树数据
@@ -436,6 +440,7 @@ const implementFormRef = ref();
const fileArchiveDialogRef = ref(); const fileArchiveDialogRef = ref();
const updateFilesDialogRef = ref(); const updateFilesDialogRef = ref();
const contractDialogRef = ref(); const contractDialogRef = ref();
const supplementFilesDialogRef = ref();
/** 采购代表弹窗 */ /** 采购代表弹窗 */
const representorDialogVisible = ref(false); const representorDialogVisible = ref(false);
@@ -712,6 +717,18 @@ const getActionMenuItems = (row: any) => {
icon: DocumentChecked, icon: DocumentChecked,
visible: () => isCompleted, visible: () => isCompleted,
}, },
{
command: 'supplementFiles',
label: '补充材料',
icon: Upload,
visible: () => isCompleted && row?.purchaseMode === '1' && row?.purchaseChannel === '1' && hasAuth('purchase_supplement'),
},
{
command: 'submitSupplementFiles',
label: '提交补充材料',
icon: Upload,
visible: () => isCompleted && row?.purchaseMode === '1' && row?.purchaseChannel === '1' && row?.supplementFlowStatus === '-1' && hasAuth('purchase_supplement'),
},
// { // {
// command: 'downloadFileApply', // command: 'downloadFileApply',
// label: '下载文件审批表', // label: '下载文件审批表',
@@ -773,6 +790,12 @@ const handleMoreCommand = (command: string, row: any) => {
case 'contract': case 'contract':
handleContract(row); handleContract(row);
break; break;
case 'supplementFiles':
handleSupplementFiles(row);
break;
case 'submitSupplementFiles':
handleSubmitSupplementFiles(row);
break;
} }
}; };
@@ -801,6 +824,26 @@ const handleContract = async (row: any) => {
} }
}; };
/** 补充材料 */
const handleSupplementFiles = (row: any) => {
const id = row?.id ?? row?.purchaseId;
if (!id) {
useMessage().warning('无法获取采购申请ID');
return;
}
supplementFilesDialogRef.value?.open(String(id));
};
/** 提交补充材料 */
const handleSubmitSupplementFiles = async (row: any) => {
const id = row?.id ?? row?.purchaseId;
if (!id) {
useMessage().warning('无法获取采购申请ID');
return;
}
supplementFilesDialogRef.value?.open(String(id));
};
/** 下载审批表 */ /** 下载审批表 */
const handleDownloadApply = (row: any) => { const handleDownloadApply = (row: any) => {
const id = row?.id ?? row?.purchaseId; const id = row?.id ?? row?.purchaseId;

View File

@@ -0,0 +1,277 @@
<template>
<div class="supplement-view-container">
<el-card shadow="never" class="info-card">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon><Document /></el-icon>
采购申请基本信息
</span>
</div>
</template>
<el-descriptions :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.budget ? Number(applyData.budget).toLocaleString() + ' 元' : '-' }}</el-descriptions-item>
<el-descriptions-item label="采购方式">{{ purchaseTypeLabel || '-' }}</el-descriptions-item>
<el-descriptions-item label="采购内容" :span="2">{{ applyData.projectContent || '-' }}</el-descriptions-item>
<el-descriptions-item label="申请人">{{ applyData.createByName || '-' }}</el-descriptions-item>
<el-descriptions-item label="申请部门">{{ applyData.deptName || '-' }}</el-descriptions-item>
</el-descriptions>
</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>
</div>
</template>
<el-alert
v-if="requiredFileTypeName"
type="info"
:closable="false"
style="margin-bottom: 16px"
>
<template #title>
<span>{{ requiredFileTypeName }}</span>
</template>
</el-alert>
<el-table :data="fileList" border stripe v-if="fileList.length > 0">
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="name" label="文件名称" show-overflow-tooltip />
<el-table-column label="操作" width="160" align="center">
<template #default="scope">
<el-button type="primary" link icon="View" @click="handlePreview(scope.row)">预览</el-button>
<el-button type="success" link icon="Download" @click="handleDownload(scope.row)">下载</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-else description="暂无补充材料文件" />
</el-card>
<el-dialog v-model="previewVisible" :title="previewTitle" width="80%" top="5vh" destroy-on-close append-to-body>
<div class="preview-container">
<iframe v-if="previewUrl" :src="previewUrl" class="preview-iframe" />
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { useRoute } from 'vue-router';
import { Document, FolderOpened, Download } from '@element-plus/icons-vue';
import { useMessage } from '/@/hooks/message';
import { getDicts } from '/@/api/admin/dict';
import { previewFileById, downloadFileById } from '/@/api/purchase/purchasingrequisition';
import { currElTabIsSave } from '/@/api/order/order-key-vue';
import request from '/@/utils/request';
interface FileInfo {
id: string;
name: string;
url: string;
}
const props = defineProps({
currJob: { type: Object, default: null },
currElTab: { type: Object, default: null },
});
const emit = defineEmits(['handleJob']);
const route = useRoute();
const loading = ref(false);
const applyData = ref<any>({});
const fileList = ref<FileInfo[]>([]);
const purchaseTypeLabel = ref('');
const requiredFileTypeName = ref('');
const previewVisible = ref(false);
const previewTitle = ref('');
const previewUrl = ref('');
const PURCHASE_TYPE_DEPT_MAP: Record<string, string> = {
'1': '网上商城',
'2': '市场采购',
'3': '商务洽谈',
'5': '询价',
};
const FILE_TYPE_MAP: Record<string, string> = {
'10': '商务洽谈纪要',
'20': '市场采购纪要',
'30': '网上商城采购材料',
'150': '部门采购询价表',
};
const loadDictData = async () => {
try {
const res = await getDicts('PURCHASE_TYPE_DEPT');
if (res.data && Array.isArray(res.data)) {
const list = res.data.map((item: any) => ({
label: item.label || item.dictLabel || item.name,
value: item.value || item.dictValue || item.code,
}));
if (applyData.value.purchaseType) {
const found = list.find((item: any) => item.value === applyData.value.purchaseType);
if (found) {
purchaseTypeLabel.value = found.label;
}
}
}
} catch (e) {
console.error('加载字典数据失败', e);
}
};
const getPurchaseTypeLabel = () => {
if (applyData.value.purchaseType) {
purchaseTypeLabel.value = PURCHASE_TYPE_DEPT_MAP[applyData.value.purchaseType] || applyData.value.purchaseType;
}
};
const getRequiredFileTypeName = () => {
const purchaseType = applyData.value.purchaseType;
let fileTypeCode = '';
if (purchaseType === '3') {
fileTypeCode = '10';
} else if (purchaseType === '2') {
fileTypeCode = '20';
} else if (purchaseType === '1') {
fileTypeCode = '30';
} else if (purchaseType === '5') {
fileTypeCode = '150';
}
if (fileTypeCode) {
requiredFileTypeName.value = FILE_TYPE_MAP[fileTypeCode] || '补充材料';
}
return fileTypeCode;
};
const loadData = async () => {
const flowInstId = route.query.flowInstId as string;
if (!flowInstId) {
useMessage().warning('缺少流程实例ID');
return;
}
loading.value = true;
try {
const res = await request({
url: '/purchase/purchasingapply/supplement/getByFlowInstId',
method: 'get',
params: { flowInstId },
});
applyData.value = res?.data || {};
await loadDictData();
getPurchaseTypeLabel();
const fileTypeCode = getRequiredFileTypeName();
if (applyData.value.id && fileTypeCode) {
const filesRes = await request({
url: '/purchase/purchasingfiles/listByType',
method: 'get',
params: { purchaseId: applyData.value.id, fileType: fileTypeCode },
});
const files = filesRes?.data || [];
fileList.value = files.map((f: any) => ({
id: f.id,
name: f.fileTitle || f.fileName || '附件',
url: f.fileUrl,
}));
}
} catch (e) {
console.error('获取数据失败', e);
useMessage().error('获取采购申请信息失败');
} finally {
loading.value = false;
}
};
const handlePreview = async (row: FileInfo) => {
if (!row.id) {
useMessage().warning('文件ID不存在');
return;
}
try {
const blob = await previewFileById(row.id);
previewUrl.value = window.URL.createObjectURL(blob);
previewTitle.value = row.name || '文件预览';
previewVisible.value = true;
} catch (e) {
console.error('预览失败', e);
useMessage().error('预览失败');
}
};
const handleDownload = async (row: FileInfo) => {
if (!row.id) {
useMessage().warning('文件ID不存在');
return;
}
try {
const blob = await downloadFileById(row.id);
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = row.name || '附件.pdf';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
} catch (e) {
console.error('下载失败', e);
useMessage().error('下载失败');
}
};
onMounted(() => {
loadData();
if (props.currJob && props.currElTab?.id) {
currElTabIsSave(props.currJob, props.currElTab.id, true, emit);
}
});
</script>
<style scoped lang="scss">
.supplement-view-container {
padding: 16px;
background-color: #f5f7fa;
min-height: 100vh;
}
.info-card,
.file-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: 500;
color: #303133;
}
.preview-container {
width: 100%;
height: 75vh;
}
.preview-iframe {
width: 100%;
height: 100%;
border: none;
}
</style>