更新采购申请前端

This commit is contained in:
吴红兵
2026-02-08 16:29:04 +08:00
parent 123c28b345
commit 5dc9b39cab
7 changed files with 502 additions and 2506 deletions

View File

@@ -21,7 +21,22 @@
</el-col>
<el-col :span="8" class="mb20" v-if="form.hasContract === '1'">
<el-form-item label="合同" prop="contractId">
<el-input v-model="form.contractId" placeholder="请选择合同(待对接合同接口)" clearable />
<el-select
v-model="form.contractId"
placeholder="请选择合同"
clearable
filterable
style="width: 100%"
:loading="contractLoading"
@visible-change="onContractSelectVisibleChange"
>
<el-option
v-for="item in contractOptions"
:key="item.id"
:label="item.contractName || item.contractNo || item.id"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8" class="mb20">
@@ -62,13 +77,16 @@
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue'
import { ref, reactive, watch, onMounted } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { getContracts } from '/@/api/finance/purchasingrequisition'
const props = defineProps<{
modelValue: Record<string, any>
projectName?: string
deptName?: string
/** 采购申请ID用于拉取合同列表 */
purchaseId?: string | number
/** 每次打开弹窗时变化,用于强制重置内部 form */
resetKey?: number
}>()
@@ -78,6 +96,9 @@ const emit = defineEmits(['update:modelValue'])
const formRef = ref<FormInstance>()
const purchaserList = ref<any[]>([])
const assetAdminList = ref<any[]>([])
const contractOptions = ref<any[]>([])
const contractLoading = ref(false)
const contractLoaded = ref(false)
const form = reactive({
hasContract: '0',
@@ -107,11 +128,52 @@ const syncFormFromModel = (val: Record<string, any> | undefined) => {
}
}
const loadContractOptions = async () => {
if (contractLoaded.value || contractLoading.value) return
contractLoading.value = true
try {
const res = await getContracts(props.purchaseId ? { id: props.purchaseId } : {})
const list = res?.data
contractOptions.value = Array.isArray(list) ? list : []
contractLoaded.value = true
} catch (_) {
contractOptions.value = []
} finally {
contractLoading.value = false
}
}
const onContractSelectVisibleChange = (visible: boolean) => {
if (visible && form.hasContract === '1' && contractOptions.value.length === 0) {
loadContractOptions()
}
}
watch(() => props.modelValue, syncFormFromModel, { deep: true, immediate: true })
// resetKey 变化时强制用 modelValue 覆盖内部 form
watch(() => props.resetKey, () => syncFormFromModel(props.modelValue))
// resetKey 变化时强制用 modelValue 覆盖内部 form,并重置合同列表以便重新拉取
watch(() => props.resetKey, () => {
syncFormFromModel(props.modelValue)
contractLoaded.value = false
contractOptions.value = []
})
watch(form, () => emit('update:modelValue', { ...form }), { deep: true })
watch(() => form.hasContract, (val) => {
if (val === '1') {
contractLoaded.value = false
loadContractOptions()
} else {
contractOptions.value = []
contractLoaded.value = false
}
})
onMounted(() => {
if (form.hasContract === '1') {
loadContractOptions()
}
})
const onPurchaserChange = (list: any[]) => {
if (list?.length) {
const u = list[0]

View File

@@ -33,6 +33,7 @@
:reset-key="openToken"
ref="commonFormRef"
v-model="commonForm"
:purchase-id="purchaseId"
:project-name="applyInfo?.projectName"
:dept-name="applyInfo?.deptName"
/>

View File

@@ -6,26 +6,21 @@
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
新增采购申请
{{ pageTitle }}
</span>
</div>
</template>
<div v-loading="loading" style="padding-bottom: 20px;">
<!-- 步骤条 -->
<!-- <el-steps :active="currentStep" finish-status="success" style="margin-bottom: 30px;">
<el-step title="基本信息" />
<el-step :title="stepTwoTitle" />
</el-steps> -->
<el-form
ref="formRef"
:model="dataForm"
:rules="dataRules"
label-width="120px"
:disabled="isViewMode"
class="compact-form">
<!-- 第一步基本信息 -->
<div v-show="currentStep === 0">
<!-- 第一步基本信息查看模式下一并展示新增/编辑按步骤切换 -->
<div v-show="isViewMode || currentStep === 0">
<el-row :gutter="20">
<el-col :span="12" class="mb16">
<el-form-item label="采购项目名称" prop="projectName">
@@ -136,8 +131,8 @@
</el-row>
</div>
<!-- 第二步采购详情 -->
<div v-show="currentStep === 1">
<!-- 第二步采购详情查看模式下一并展示新增/编辑按步骤切换 -->
<div v-show="isViewMode || currentStep === 1">
<!-- 分支一部门自行采购 -->
<div class="mb20" v-if="isDeptPurchase">
<div class="step-title mb16">部门自行采购</div>
@@ -182,7 +177,7 @@
</el-button>
<upload-file
v-model="dataForm.businessNegotiationTable"
:limit="5"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:data="{ fileType: FILE_TYPE_MAP.businessNegotiationTable }"
upload-file-url="/purchase/purchasingfiles/upload" />
@@ -204,7 +199,7 @@
</el-button>
<upload-file
v-model="dataForm.marketPurchaseMinutes"
:limit="5"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:data="{ fileType: FILE_TYPE_MAP.marketPurchaseMinutes }"
upload-file-url="/purchase/purchasingfiles/upload" />
@@ -240,7 +235,7 @@
</el-button>
<upload-file
v-model="dataForm.inquiryTemplate"
:limit="5"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:data="{ fileType: FILE_TYPE_MAP.inquiryTemplate }"
upload-file-url="/purchase/purchasingfiles/upload" />
@@ -295,14 +290,14 @@
</el-button>
<upload-file
v-model="dataForm.serviceDirectSelect"
:limit="5"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:data="{ fileType: FILE_TYPE_MAP.serviceDirectSelect }"
upload-file-url="/purchase/purchasingfiles/upload" />
</el-form-item>
</template>
<el-form-item
v-if="dataForm.hasSupplier === 'no'"
v-if="dataForm.hasSupplier === '0'"
label="服务商城项目需求模板(邀请比选)"
prop="serviceInviteSelect"
class="mb16">
@@ -316,7 +311,7 @@
</el-button>
<upload-file
v-model="dataForm.serviceInviteSelect"
:limit="5"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:data="{ fileType: FILE_TYPE_MAP.serviceInviteSelect }"
upload-file-url="/purchase/purchasingfiles/upload" />
@@ -339,7 +334,7 @@
</el-button>
<upload-file
v-model="dataForm.purchaseRequirementTemplate"
:limit="5"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:data="{ fileType: FILE_TYPE_MAP.purchaseRequirementTemplate }"
upload-file-url="/purchase/purchasingfiles/upload" />
@@ -348,20 +343,20 @@
<!-- 特殊规则5<=金额<40万服务类目自动使用邀请比选模版 -->
<template v-if="showAutoInviteSelect">
<el-form-item label="是否有推荐供应商" prop="hasRecommendedSupplier" class="mb16">
<el-radio-group v-model="dataForm.hasRecommendedSupplier">
<el-form-item label="是否有推荐供应商" prop="hasSupplier" class="mb16">
<el-radio-group v-model="dataForm.hasSupplier">
<el-radio label="1"></el-radio>
<el-radio label="0"></el-radio>
</el-radio-group>
</el-form-item>
<!-- 有推荐供应商显示推荐供应商输入框和邀请比选模板 -->
<template v-if="dataForm.hasRecommendedSupplier === '1'">
<template v-if="dataForm.hasSupplier === '1'">
<el-form-item
label="推荐供应商"
prop="recommendedSuppliers"
prop="suppliers"
class="mb16">
<el-input
v-model="dataForm.recommendedSuppliers"
v-model="dataForm.suppliers"
placeholder="请输入三家供应商名称,用逗号分隔"
clearable />
<div class="template-note mt5">
@@ -382,7 +377,7 @@
</el-button>
<upload-file
v-model="dataForm.serviceInviteSelect"
:limit="5"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:data="{ fileType: FILE_TYPE_MAP.serviceInviteSelect }"
upload-file-url="/purchase/purchasingfiles/upload" />
@@ -390,7 +385,7 @@
</template>
<!-- 无推荐供应商显示公开比选模板 -->
<el-form-item
v-if="dataForm.hasRecommendedSupplier === 'no'"
v-if="dataForm.hasSupplier === '0'"
label="服务商城项目需求模板(公开比选)"
prop="servicePublicSelectAuto"
class="mb16">
@@ -404,7 +399,7 @@
</el-button>
<upload-file
v-model="dataForm.servicePublicSelectAuto"
:limit="5"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:data="{ fileType: FILE_TYPE_MAP.servicePublicSelectAuto }"
upload-file-url="/purchase/purchasingfiles/upload" />
@@ -415,7 +410,7 @@
<el-form-item label="其他材料" prop="otherMaterials" class="mb16">
<upload-file
v-model="dataForm.otherMaterials"
:limit="5"
:limit="1"
:file-type="['zip']"
:data="{ fileType: FILE_TYPE_MAP.otherMaterials }"
upload-file-url="/purchase/purchasingfiles/upload" />
@@ -442,7 +437,7 @@
<el-col :span="12" class="mb16">
<el-form-item label="采购方式" prop="purchaseTypeUnion">
<el-select
v-model="dataForm.purchaseTypeUnion"
v-model="dataForm.purchaseType"
placeholder="请选择采购方式"
clearable
:disabled="isAutoSelectPurchaseTypeUnion"
@@ -460,9 +455,9 @@
<el-row :gutter="20">
<!-- 业务分管处室 -->
<el-col :span="12" class="mb16">
<el-form-item label="业务分管处室" prop="deptClassifyId">
<el-form-item label="业务分管处室" prop="deptClassifyUserId">
<el-select
v-model="dataForm.deptClassifyId"
v-model="dataForm.deptClassifyUserId"
placeholder="请选择业务分管处室"
clearable
filterable
@@ -472,7 +467,7 @@
v-for="item in businessDeptList"
:key="item.id"
:label="item.deptName"
:value="item.id" />
:value="item.username" />
</el-select>
</el-form-item>
</el-col>
@@ -514,7 +509,7 @@
</el-button>
<upload-file
v-model="dataForm.feasibilityReport"
:limit="5"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:data="{ fileType: FILE_TYPE_MAP.feasibilityReport }"
upload-file-url="/purchase/purchasingfiles/upload" />
@@ -530,7 +525,7 @@
prop="meetingMinutes">
<upload-file
v-model="dataForm.meetingMinutes"
:limit="5"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:data="{ fileType: FILE_TYPE_MAP.meetingMinutes }"
upload-file-url="/purchase/purchasingfiles/upload" />
@@ -546,7 +541,7 @@
<el-form-item label="会议纪要" prop="meetingMinutesUrgent">
<upload-file
v-model="dataForm.meetingMinutesUrgent"
:limit="5"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:data="{ fileType: FILE_TYPE_MAP.meetingMinutesUrgent }"
upload-file-url="/purchase/purchasingfiles/upload" />
@@ -572,7 +567,7 @@
</el-button>
<upload-file
v-model="dataForm.singleSourceProof"
:limit="5"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:data="{ fileType: FILE_TYPE_MAP.singleSourceProof }"
upload-file-url="/purchase/purchasingfiles/upload" />
@@ -582,7 +577,7 @@
<el-form-item label="会议纪要" prop="meetingMinutesSingle">
<upload-file
v-model="dataForm.meetingMinutesSingle"
:limit="5"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:data="{ fileType: FILE_TYPE_MAP.meetingMinutesSingle }"
upload-file-url="/purchase/purchasingfiles/upload" />
@@ -608,7 +603,7 @@
</el-button>
<upload-file
v-model="dataForm.importApplication"
:limit="5"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:data="{ fileType: FILE_TYPE_MAP.importApplication }"
upload-file-url="/purchase/purchasingfiles/upload" />
@@ -618,7 +613,7 @@
<el-form-item label="会议纪要" prop="meetingMinutesImport">
<upload-file
v-model="dataForm.meetingMinutesImport"
:limit="5"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:data="{ fileType: FILE_TYPE_MAP.meetingMinutesImport }"
upload-file-url="/purchase/purchasingfiles/upload" />
@@ -631,29 +626,29 @@
<template v-if="showAutoInviteSelectSchool">
<el-row :gutter="20">
<el-col :span="12" class="mb16">
<el-form-item label="是否有推荐供应商" prop="hasRecommendedSupplierSchool">
<el-radio-group v-model="dataForm.hasRecommendedSupplierSchool">
<el-radio label="yes"></el-radio>
<el-radio label="no"></el-radio>
<el-form-item label="是否有推荐供应商" prop="hasSupplier">
<el-radio-group v-model="dataForm.hasSupplier">
<el-radio label="1"></el-radio>
<el-radio label="0"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<!-- 有推荐供应商显示推荐供应商输入框 -->
<el-col
v-if="dataForm.hasRecommendedSupplierSchool === 'yes'"
v-if="dataForm.hasSupplier === '1'"
:span="12"
class="mb16">
<el-form-item
label="推荐供应商"
prop="recommendedSuppliersSchool">
prop="suppliers">
<el-input
v-model="dataForm.recommendedSuppliersSchool"
v-model="dataForm.suppliers"
type="textarea"
:rows="2"
placeholder="请输入三家供应商名称,用逗号或分号分隔"
placeholder="请输入至少三家供应商名称,用逗号或分号分隔"
clearable />
<div class="template-note mt5">
<el-text type="info" size="small">请输入三家供应商名称用逗号或分号分隔</el-text>
<el-text type="info" size="small">请输入至少三家供应商名称用逗号或分号分隔</el-text>
</div>
</el-form-item>
</el-col>
@@ -665,7 +660,7 @@
<!-- 特殊规则5<=金额<40万服务类目isMallService=1isProjectService=1自动使用邀请比选模版 -->
<template v-if="showAutoInviteSelectSchool">
<!-- 有推荐供应商显示邀请比选模板 -->
<template v-if="dataForm.hasRecommendedSupplierSchool === 'yes'">
<template v-if="dataForm.hasSupplier === '1'">
<div class="mb10">
<el-button
type="primary"
@@ -678,13 +673,13 @@
</div>
<upload-file
v-model="dataForm.serviceInviteSelectSchool"
:limit="5"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:data="{ fileType: FILE_TYPE_MAP.serviceInviteSelectSchool }"
upload-file-url="/purchase/purchasingfiles/upload" />
</template>
<!-- 无推荐供应商显示公开比选模板 -->
<template v-else-if="dataForm.hasRecommendedSupplierSchool === 'no'">
<template v-else-if="dataForm.hasSupplier === '0'">
<div class="mb10">
<el-button
type="primary"
@@ -697,7 +692,7 @@
</div>
<upload-file
v-model="dataForm.servicePublicSelectSchoolAuto"
:limit="5"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:data="{ fileType: FILE_TYPE_MAP.servicePublicSelectSchoolAuto }"
upload-file-url="/purchase/purchasingfiles/upload" />
@@ -718,7 +713,7 @@
</div>
<upload-file
v-model="dataForm.servicePublicSelectSchool"
:limit="5"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:data="{ fileType: FILE_TYPE_MAP.servicePublicSelectSchool }"
upload-file-url="/purchase/purchasingfiles/upload" />
@@ -738,7 +733,7 @@
</div>
<upload-file
v-model="dataForm.purchaseRequirement"
:limit="5"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:data="{ fileType: FILE_TYPE_MAP.purchaseRequirement }"
upload-file-url="/purchase/purchasingfiles/upload" />
@@ -756,7 +751,7 @@
prop="governmentPurchaseIntent">
<upload-file
v-model="dataForm.governmentPurchaseIntent"
:limit="5"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:data="{ fileType: FILE_TYPE_MAP.governmentPurchaseIntent }"
upload-file-url="/purchase/purchasingfiles/upload" />
@@ -768,7 +763,7 @@
<el-form-item label="其他材料" prop="otherMaterials">
<upload-file
v-model="dataForm.otherMaterials"
:limit="5"
:limit="1"
:file-type="['zip']"
:data="{ fileType: FILE_TYPE_MAP.otherMaterials }"
upload-file-url="/purchase/purchasingfiles/upload" />
@@ -791,29 +786,36 @@
<!-- 操作按钮 -->
<div class="form-footer">
<el-button @click="handleCancel">取消</el-button>
<el-button v-if="currentStep > 0" @click="prevStep">上一步</el-button>
<el-button
v-if="currentStep < 1"
type="primary"
@click="nextStep"
:disabled="loading">
下一步
</el-button>
<el-button
v-if="currentStep === 1"
type="warning"
@click="handleTempStore"
:disabled="loading">
暂存
</el-button>
<el-button
v-if="currentStep === 1"
type="primary"
@click="handleSubmit"
:disabled="loading">
提交
</el-button>
<template v-if="isViewMode">
<el-button @click="handleCancel">返回</el-button>
</template>
<template v-else>
<el-button @click="handleCancel">取消</el-button>
<el-button v-if="currentStep > 0" @click="prevStep">上一步</el-button>
<el-button
v-if="currentStep < 1"
type="primary"
@click="nextStep"
:disabled="loading">
下一步
</el-button>
<template v-if="currentStep === 1">
<el-button
v-if="!isEditMode"
type="warning"
@click="handleTempStore"
:disabled="loading">
暂存
</el-button>
<el-button
v-if="currentStep === 1"
type="primary"
@click="handleSubmit"
:disabled="loading">
{{ isEditMode ? '保存' : '提交' }}
</el-button>
</template>
</template>
</div>
</div>
</el-card>
@@ -823,8 +825,8 @@
<script setup lang="ts" name="PurchasingRequisitionAdd">
import { reactive, ref, onMounted, computed, watch, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import { addObj, tempStore } from '/@/api/finance/purchasingrequisition';
import { useRouter, useRoute } from 'vue-router'
import { addObj, tempStore, getObj, editObj, getApplyFiles } from '/@/api/finance/purchasingrequisition';
import { getTree } from '/@/api/finance/purchasingcategory';
import { getDicts } from '/@/api/admin/dict';
import { useMessage } from '/@/hooks/message';
@@ -836,6 +838,16 @@ import { getPage as getSchoolLeaderPage } from '/@/api/finance/purchasingschooll
// 路由
const router = useRouter();
const route = useRoute();
// 模式add | edit | view来自 URL 参数,用于 form 弹窗内 iframe 打开)
const isEditMode = computed(() => String(route.query.mode) === 'edit');
const isViewMode = computed(() => String(route.query.mode) === 'view');
const pageTitle = computed(() => {
if (isViewMode.value) return '查看采购申请';
if (isEditMode.value) return '编辑采购申请';
return '新增采购申请';
});
// 定义变量内容
const formRef = ref();
@@ -862,13 +874,12 @@ const dataForm = reactive({
onlineMallMaterials: '',
inquiryTemplate: '', // 询价模板
entrustCenterType: '',
hasSupplier: '',
hasSupplier: '0',
suppliers: '', // 供应商名称(逗号或分号分隔)
serviceDirectSelect: '',
servicePublicSelect: '',
purchaseRequirementTemplate: '',
hasRecommendedSupplier: '',
recommendedSuppliers: '',
serviceInviteSelect: '',
servicePublicSelectAuto: '',
// 学校统一采购字段
@@ -883,12 +894,11 @@ const dataForm = reactive({
governmentPurchaseIntent: '',
servicePublicSelectSchool: '',
// 学校统一采购特殊规则字段5万<=金额<40万
hasRecommendedSupplierSchool: '',
recommendedSuppliersSchool: '',
serviceInviteSelectSchool: '',
servicePublicSelectSchoolAuto: '',
// 业务分管处室和分管校领导
deptClassifyId: '',
deptClassifyUserId: '',
deptClassifyName: '',
schoolLeaderUserId: '',
schoolLeaderName: '',
@@ -920,7 +930,7 @@ const PURCHASE_TYPE_IDS = {
};
// 文件类型映射(对应数据库 file_type 字段)
// 10:商务洽谈纪要 20:市场采购纪要 30:网上商城采购相关材料 40:可行性论证报告 50:会议记录 60:其他材料 70:单一来源专家论证表 80:进口产品申请表 90:进口产品专家论证表 100:政府采购意向表 110:履约验收单 120:采购需求表 130:采购文件
// 10:商务洽谈纪要 20:市场采购纪要 30:网上商城采购相关材料 40:可行性论证报告 50:会议记录 60:其他材料 70:单一来源专家论证表 90:进口产品专家论证表 100:政府采购意向表 110:履约验收单 120:采购需求表 130:采购文件
const FILE_TYPE_MAP: Record<string, string> = {
businessNegotiationTable: '10', // 商务洽谈纪要
marketPurchaseMinutes: '20', // 市场采购纪要
@@ -933,7 +943,7 @@ const FILE_TYPE_MAP: Record<string, string> = {
meetingMinutesImport: '50', // 会议记录
otherMaterials: '60', // 其他材料
singleSourceProof: '70', // 单一来源专家论证表
importApplication: '80', // 进口产品申请表
importApplication: '90', // 进口产品申请表
governmentPurchaseIntent: '100', // 政府采购意向表
// 需求文件相关 - 所有需求模板都应该是120采购需求表
serviceDirectSelect: '120', // 服务商城项目需求模板(直选)- 采购需求表
@@ -947,6 +957,13 @@ const FILE_TYPE_MAP: Record<string, string> = {
servicePublicSelectSchool: '120', // 服务商城项目需求模板(公开比选-学校)- 采购需求表
};
// fileType -> 表单字段名数组(顺序与回填分配一致,同类型多字段时按此顺序分配)
const FILE_TYPE_TO_FIELDS: Record<string, string[]> = {};
Object.entries(FILE_TYPE_MAP).forEach(([field, type]) => {
if (!FILE_TYPE_TO_FIELDS[type]) FILE_TYPE_TO_FIELDS[type] = [];
FILE_TYPE_TO_FIELDS[type].push(field);
});
// 辅助函数:判断当前采购方式是否为指定类型(通过 id 或 value 匹配)
const isPurchaseType = (purchaseTypeId: string) => {
if (!dataForm.purchaseType) return false;
@@ -1082,21 +1099,21 @@ const isServiceCategory = computed(() => {
// 备用判断:通过 category 对象判断
const category = getCategoryInfo();
if (!category) return false;
return category.type === 'C' || category.projectType === 'C';
return category.type === 'C' ;
});
// 判断是否为货物类
const isGoodsCategory = computed(() => {
const category = getCategoryInfo();
if (!category) return false;
return category.type === 'A' || category.projectType === 'A';
return category.type === 'A';
});
// 判断是否为特殊服务类目isMallService=1、isProjectService=1
const isSpecialServiceCategory = computed(() => {
const category = getCategoryInfo();
if (!category) return false;
return Number(category.isMallService) === 1 || Number(category.isProjectService) === 1;
return Number(category.isMallService) === 1 ;
});
// 委托采购中心方式自动判断:
@@ -1133,7 +1150,7 @@ watch(
// 切换时清理不相关字段,避免脏数据
if (nextType === 'other') {
dataForm.hasSupplier = '';
dataForm.hasSupplier = '0';
dataForm.suppliers = '';
dataForm.serviceDirectSelect = '';
dataForm.serviceInviteSelect = '';
@@ -1180,9 +1197,9 @@ const showAutoPublicSelect = computed(() => {
// 获取需求文件的 prop 名称(用于表单验证)
const getRequirementFileProp = () => {
if (showAutoInviteSelectSchool.value) {
if (dataForm.hasRecommendedSupplierSchool === 'yes') {
if (dataForm.hasSupplier === '1') {
return 'serviceInviteSelectSchool';
} else if (dataForm.hasRecommendedSupplierSchool === 'no') {
} else if (dataForm.hasSupplier === '0') {
return 'servicePublicSelectSchoolAuto';
}
} else if (showAutoPublicSelect.value) {
@@ -1215,15 +1232,15 @@ watch([() => dataForm.categoryCode, () => dataForm.budget], () => {
}
}
// 学校统一采购:自动设置网上商城采购方式
// 学校统一采购:自动设置网上商城采购方式(提交字段统一为 purchaseType
if (isAutoSelectPurchaseTypeUnion.value && !isDeptPurchase.value) {
// 查找学校统一采购方式字典中包含"网上商城"的选项
const onlineMallOption = purchaseTypeUnionList.value.find(item => {
const label = item.label || item.dictLabel || item.name || '';
return label.includes('网上商城') || label.includes('商城');
});
if (onlineMallOption && dataForm.purchaseTypeUnion !== onlineMallOption.value) {
dataForm.purchaseTypeUnion = onlineMallOption.value;
if (onlineMallOption && dataForm.purchaseType !== onlineMallOption.value) {
dataForm.purchaseType = onlineMallOption.value;
}
}
}, { immediate: true });
@@ -1562,11 +1579,11 @@ const handleBusinessDeptChange = (value: string) => {
if (value) {
const selected = businessDeptList.value.find(item => item.id === value);
if (selected) {
dataForm.deptClassifyId = selected.id;
dataForm.deptClassifyUserId = selected.id;
dataForm.deptClassifyName = selected.deptName || '';
}
} else {
dataForm.deptClassifyId = '';
dataForm.deptClassifyUserId = '';
dataForm.deptClassifyName = '';
}
};
@@ -1585,14 +1602,19 @@ const handleSchoolLeaderChange = (value: string) => {
}
};
// 处理文件ID字符串数组
// 从上传返回的URL或ID中提取文件ID拼成数组格式["id1", "id2"]
// 上传接口返回的id会保存在URL的id参数中或者直接是32位十六进制字符串
const getFileIdsArray = (fileIds: string | string[]): string[] => {
// 处理文件ID字符串或对象数组转ID数组
// 支持:逗号分隔的字符串、字符串数组、{ id, name? }[](编辑回填时的格式)
const getFileIdsArray = (fileIds: string | string[] | { id?: string; name?: string }[]): string[] => {
if (!fileIds) return [];
if (Array.isArray(fileIds)) return fileIds;
const items = fileIds.split(',').filter(item => item.trim());
if (Array.isArray(fileIds)) {
return fileIds.map((item: any) => {
if (item && typeof item === 'object' && item.id) return String(item.id).trim();
if (typeof item === 'string') return item.trim();
return '';
}).filter(Boolean);
}
const items = String(fileIds).split(',').filter((item: string) => item.trim());
const ids: string[] = [];
items.forEach(item => {
@@ -1688,8 +1710,13 @@ const handleSubmit = async () => {
console.log('提交数据:', submitData);
await addObj(submitData);
useMessage().success('提交成功');
if (dataForm.id) {
await editObj(submitData);
useMessage().success('保存成功');
} else {
await addObj(submitData);
useMessage().success('提交成功');
}
// 如果是在 iframe 中,向父窗口发送消息
if (window.parent !== window) {
@@ -1913,6 +1940,103 @@ onMounted(async () => {
getBusinessDeptListData(),
getSchoolLeaderListData(),
]);
// 编辑/查看:从 URL 参数 id 加载详情
const queryId = route.query.id;
if (queryId) {
try {
const res = await getObj(Number(queryId));
const detail = res?.data;
if (detail && typeof detail === 'object') {
Object.assign(dataForm, {
id: detail.id ?? dataForm.id,
projectName: detail.projectName ?? '',
projectType: detail.projectType ?? '',
projectContent: detail.projectContent ?? '',
applyDate: detail.applyDate ?? '',
fundSource: detail.fundSource ?? '',
budget: detail.budget != null ? Number(detail.budget) : null,
isCentralized: detail.isCentralized != null ? String(detail.isCentralized) : '',
isSpecial: detail.isSpecial != null ? String(detail.isSpecial) : '',
purchaseMode: detail.purchaseMode ?? '',
purchaseType: detail.purchaseType ?? '',
purchaseTypeUnion: detail.purchaseTypeUnion ?? '',
categoryCode: detail.categoryCode ?? '',
remark: detail.remark ?? '',
status: detail.status ?? '',
businessNegotiationTable: detail.businessNegotiationTable ?? '',
marketPurchaseMinutes: detail.marketPurchaseMinutes ?? '',
onlineMallMaterials: detail.onlineMallMaterials ?? '',
inquiryTemplate: detail.inquiryTemplate ?? '',
entrustCenterType: detail.entrustCenterType ?? '',
hasSupplier: detail.hasSupplier != null && detail.hasSupplier !== '' ? detail.hasSupplier : '0',
suppliers: detail.suppliers ?? '',
serviceDirectSelect: detail.serviceDirectSelect ?? '',
servicePublicSelect: detail.servicePublicSelect ?? '',
purchaseRequirementTemplate: detail.purchaseRequirementTemplate ?? '',
suppliers: detail.suppliers ?? '',
serviceInviteSelect: detail.serviceInviteSelect ?? '',
servicePublicSelectAuto: detail.servicePublicSelectAuto ?? '',
purchaseRequirement: detail.purchaseRequirement ?? '',
meetingMinutes: detail.meetingMinutes ?? '',
feasibilityReport: detail.feasibilityReport ?? '',
meetingMinutesUrgent: detail.meetingMinutesUrgent ?? '',
meetingMinutesSingle: detail.meetingMinutesSingle ?? '',
meetingMinutesImport: detail.meetingMinutesImport ?? '',
singleSourceProof: detail.singleSourceProof ?? '',
importApplication: detail.importApplication ?? '',
governmentPurchaseIntent: detail.governmentPurchaseIntent ?? '',
servicePublicSelectSchool: detail.servicePublicSelectSchool ?? '',
suppliers: detail.suppliers ?? '',
serviceInviteSelectSchool: detail.serviceInviteSelectSchool ?? '',
servicePublicSelectSchoolAuto: detail.servicePublicSelectSchoolAuto ?? '',
deptClassifyUserId: detail.deptClassifyUserId ?? '',
deptClassifyName: detail.deptClassifyName ?? '',
schoolLeaderUserId: detail.schoolLeaderUserId ?? '',
schoolLeaderName: detail.schoolLeaderName ?? '',
otherMaterials: detail.otherMaterials ?? '',
});
setCategoryCodePath();
if (dataForm.budget != null) {
budgetInputValue.value = dataForm.budget;
budgetUnit.value = 'yuan';
}
// 编辑时默认显示第一步,用户可自行切换到第二步查看
currentStep.value = 0;
// 加载已上传附件并按 fileType 回填到对应表单项
try {
const fileRes = await getApplyFiles(String(queryId));
const fileList: { id: string; fileType: string; fileTitle?: string }[] = fileRes?.data ?? [];
if (Array.isArray(fileList) && fileList.length > 0) {
const byType: Record<string, { id: string; fileTitle?: string }[]> = {};
fileList.forEach((f: any) => {
const t = String(f.fileType || '');
if (!byType[t]) byType[t] = [];
byType[t].push({ id: f.id, fileTitle: f.fileTitle });
});
Object.entries(byType).forEach(([fileType, files]) => {
const fields = FILE_TYPE_TO_FIELDS[fileType];
if (!fields || fields.length === 0) return;
const fileItems = files.map((f) => ({ id: f.id, name: f.fileTitle || '附件' }));
if (fields.length === 1) {
(dataForm as any)[fields[0]] = fileItems;
} else {
// 同类型多字段(如 120 采购需求表):后端不区分具体子项,将同一批文件回填到所有对应字段,当前展示的“需求文件”才能正确显示
fields.forEach((field) => {
(dataForm as any)[field] = fileItems.length ? [...fileItems] : '';
});
}
});
}
} catch (err) {
console.error('加载采购申请附件失败', err);
}
}
} catch (e) {
console.error('加载采购申请详情失败', e);
useMessage().error('加载详情失败');
}
}
// 新增模式下设置默认值(只有在没有 id 的情况下才设置)
if (!dataForm.id) {

File diff suppressed because it is too large Load Diff

View File

@@ -193,38 +193,21 @@
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="360">
<el-table-column label="操作" align="center" fixed="right" width="150">
<template #default="scope">
<el-button
icon="View"
link
type="primary"
@click="handleView(scope.row)">
查看
</el-button>
<el-button
v-if="scope.row.status === '-1'"
icon="Edit"
link
type="primary"
@click="handleEdit(scope.row)">
编辑
</el-button>
<el-button
v-if="scope.row.status === '-1'"
icon="Delete"
link
type="danger"
@click="handleDelete(scope.row)">
删除
</el-button>
<el-button
icon="DocumentChecked"
link
type="primary"
@click="handleAccept(scope.row)">
履约验收
</el-button>
<div class="op-cell">
<el-button
type="primary"
link
icon="View"
@click="handleView(scope.row)">
查看
</el-button>
<ActionDropdown
:items="getActionMenuItems(scope.row)"
@command="(command) => handleMoreCommand(command, scope.row)"
/>
</div>
</template>
</el-table-column>
</el-table>
@@ -241,28 +224,7 @@
</el-card>
</div>
<!-- 新增页面 iframe 对话框 -->
<el-dialog
v-model="showAddIframe"
title="新增采购申请"
width="90%"
:style="{ maxWidth: '1600px' }"
:close-on-click-modal="false"
:close-on-press-escape="true"
destroy-on-close
class="iframe-dialog"
@close="closeAddIframe">
<div class="iframe-dialog-content">
<iframe
ref="addIframeRef"
:src="addIframeSrc"
frameborder="0"
class="add-iframe"
/>
</div>
</el-dialog>
<!-- 编辑新增表单对话框 -->
<!-- 新增/编辑/查看统一使用 form.vue 弹窗iframe 引入 add.vue -->
<FormDialog
ref="formDialogRef"
:dict-data="dictData"
@@ -274,17 +236,18 @@
</template>
<script setup lang="ts" name="PurchasingRequisition">
import { ref, reactive, defineAsyncComponent, onUnmounted, onMounted } from 'vue'
import { ref, reactive, defineAsyncComponent, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { getPage, delObj } from "/@/api/finance/purchasingrequisition";
import { getPage, delObj, submitObj } from "/@/api/finance/purchasingrequisition";
import { useMessage, useMessageBox } from "/@/hooks/message";
import { getDicts } from '/@/api/admin/dict';
import { getTree } from '/@/api/finance/purchasingcategory';
import { List, Document, DocumentCopy, Search, Collection, Money, CircleCheck, InfoFilled, Calendar, OfficeBuilding, Warning, DocumentChecked } from '@element-plus/icons-vue'
import { List, Document, DocumentCopy, Search, Collection, Money, CircleCheck, InfoFilled, Calendar, OfficeBuilding, Warning, DocumentChecked, Edit, Delete, Upload } from '@element-plus/icons-vue'
// 引入组件
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
const ActionDropdown = defineAsyncComponent(() => import('/@/components/tools/action-dropdown.vue'));
const PurchasingAcceptModal = defineAsyncComponent(() => import('./accept/PurchasingAcceptModal.vue'));
// 字典数据和品目树数据
@@ -305,9 +268,6 @@ const formDialogRef = ref()
const acceptModalRef = ref()
const searchFormRef = ref()
const showSearch = ref(true)
const showAddIframe = ref(false)
const addIframeRef = ref<HTMLIFrameElement>()
const addIframeSrc = ref('')
/**
* 定义响应式表格数据
@@ -338,42 +298,10 @@ const handleReset = () => {
};
/**
* 新增采购申请 - 在 iframe 中展示
* 新增采购申请 - 统一通过 form.vue 弹窗iframe 引入 add.vue
*/
const handleAdd = () => {
// 构建 iframe 的 src使用当前页面的 hash 路由
const baseUrl = window.location.origin + window.location.pathname
addIframeSrc.value = `${baseUrl}#/finance/purchasingrequisition/add`
showAddIframe.value = true
// 监听来自 iframe 的消息
window.addEventListener('message', handleIframeMessage)
};
/**
* 关闭新增 iframe
*/
const closeAddIframe = () => {
showAddIframe.value = false
// 移除消息监听器
window.removeEventListener('message', handleIframeMessage)
};
/**
* 处理 iframe 发送的消息
*/
const handleIframeMessage = (event: MessageEvent) => {
// 验证消息来源(可选,根据实际需求)
// if (event.origin !== window.location.origin) return
if (event.data && event.data.type === 'purchasingrequisition:submitSuccess') {
// 提交成功,关闭 iframe 并刷新列表
closeAddIframe()
getDataList()
useMessage().success('提交成功')
} else if (event.data && event.data.type === 'purchasingrequisition:close') {
// 关闭 iframe
closeAddIframe()
}
formDialogRef.value?.openDialog('add')
};
/**
@@ -421,6 +349,71 @@ const handleDelete = async (row: any) => {
}
};
/** 暂存状态下提交采购申请(启动流程) */
const handleSubmit = async (row: any) => {
try {
await useMessageBox().confirm('确定要提交该采购申请并启动流程吗?');
} catch {
return;
}
try {
await submitObj({ id: row.id });
useMessage().success('提交成功');
getDataList();
} catch (err: any) {
useMessage().error(err?.msg || '提交失败');
}
};
/** 操作栏「更多」菜单项配置 */
const getActionMenuItems = (row: any) => {
const isTemp = row?.status === '-1';
return [
{
command: 'edit',
label: '编辑',
icon: Edit,
visible: () => isTemp,
},
{
command: 'submit',
label: '提交',
icon: Upload,
visible: () => isTemp,
},
{
command: 'delete',
label: '删除',
icon: Delete,
visible: () => isTemp,
},
{
command: 'accept',
label: '履约验收',
icon: DocumentChecked,
visible: () => true,
},
];
};
/** 处理更多操作下拉菜单命令 */
const handleMoreCommand = (command: string, row: any) => {
switch (command) {
case 'edit':
handleEdit(row);
break;
case 'submit':
handleSubmit(row);
break;
case 'delete':
handleDelete(row);
break;
case 'accept':
handleAccept(row);
break;
}
};
// 获取字典数据和品目树数据
const loadDictData = async () => {
try {
@@ -566,53 +559,15 @@ onMounted(() => {
loadDictData();
});
// 组件卸载时清理事件监听器
onUnmounted(() => {
window.removeEventListener('message', handleIframeMessage)
});
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
.iframe-dialog-content {
width: 100%;
height: 70vh;
min-height: 500px;
max-height: calc(100vh - 200px);
position: relative;
overflow: hidden;
.add-iframe {
width: 100%;
height: 100%;
min-height: 500px;
border: none;
display: block;
}
}
:deep(.iframe-dialog) {
.el-dialog {
display: flex;
flex-direction: column;
max-height: 90vh;
margin-top: 5vh !important;
}
.el-dialog__header {
flex-shrink: 0;
padding: 20px 20px 10px;
}
.el-dialog__body {
padding: 20px;
overflow-y: auto;
overflow-x: hidden;
flex: 1;
min-height: 0;
max-height: calc(100vh - 200px);
}
.op-cell {
display: flex;
align-items: center;
justify-content: center;
}
</style>