From 168e134e1bffbc4a5acf0b11f65f669196128349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E7=BA=A2=E5=85=B5?= <374362909@qq.com> Date: Tue, 3 Mar 2026 13:59:57 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/purchase/purchasingRuleConfig.ts | 73 ++++++ src/hooks/usePurchaseRules.ts | 110 ++++++++ .../purchase/purchasingRuleConfig/form.vue | 246 ++++++++++++++++++ .../purchase/purchasingRuleConfig/index.vue | 169 ++++++++++++ .../purchase/purchasingrequisition/add.vue | 64 +++-- 5 files changed, 642 insertions(+), 20 deletions(-) create mode 100644 src/api/purchase/purchasingRuleConfig.ts create mode 100644 src/hooks/usePurchaseRules.ts create mode 100644 src/views/purchase/purchasingRuleConfig/form.vue create mode 100644 src/views/purchase/purchasingRuleConfig/index.vue diff --git a/src/api/purchase/purchasingRuleConfig.ts b/src/api/purchase/purchasingRuleConfig.ts new file mode 100644 index 0000000..4f40f99 --- /dev/null +++ b/src/api/purchase/purchasingRuleConfig.ts @@ -0,0 +1,73 @@ +import request from "/@/utils/request" + +const BASE_URL = '/purchase/purchasingruleconfig' + +export function fetchList(query?: Object) { + return request({ + url: `${BASE_URL}/page`, + method: 'get', + params: query + }) +} + +export function getObj(id: string) { + return request({ + url: `${BASE_URL}/${id}`, + method: 'get' + }) +} + +export function addObj(obj: Object) { + return request({ + url: BASE_URL, + method: 'post', + data: obj + }) +} + +export function putObj(obj: Object) { + return request({ + url: `${BASE_URL}/edit`, + method: 'post', + data: obj + }) +} + +export function delObjs(ids: string[]) { + return request({ + url: `${BASE_URL}/delete`, + method: 'post', + data: ids + }) +} + +export function getRuleTypes() { + return request({ + url: `${BASE_URL}/rule-types`, + method: 'get' + }) +} + +export function getEnabledRules() { + return request({ + url: `${BASE_URL}/enabled`, + method: 'get' + }) +} + +export interface RuleEvaluateParams { + budget: number + isCentralized?: string + isSpecial?: string + hasSupplier?: string + projectType?: string + ruleType?: string +} + +export function evaluateRules(params: RuleEvaluateParams) { + return request({ + url: `${BASE_URL}/evaluate`, + method: 'post', + data: params + }) +} \ No newline at end of file diff --git a/src/hooks/usePurchaseRules.ts b/src/hooks/usePurchaseRules.ts new file mode 100644 index 0000000..395bd9f --- /dev/null +++ b/src/hooks/usePurchaseRules.ts @@ -0,0 +1,110 @@ +import { ref, computed, watch } from 'vue'; +import { evaluateRules, getEnabledRules } from '/@/api/purchase/purchasingRuleConfig'; + +const RULE_CACHE_KEY = 'purchase_rule_cache'; +const CACHE_EXPIRE_TIME = 5 * 60 * 1000; + +interface RuleCache { + data: any[]; + timestamp: number; +} + +interface RuleResult { + purchaseMode: string; + purchaseSchool: string; + bidTemplate: string; + requiredFiles: { fieldName: string; fileTypeName: string; required: boolean }[]; + matchedRules: any[]; +} + +const ruleCache = ref(null); + +export function usePurchaseRules() { + const loading = ref(false); + const rules = ref([]); + + const loadRules = async (forceRefresh = false) => { + if (!forceRefresh && ruleCache.value) { + const now = Date.now(); + if (now - ruleCache.value.timestamp < CACHE_EXPIRE_TIME) { + rules.value = ruleCache.value.data; + return; + } + } + + loading.value = true; + try { + const res = await getEnabledRules(); + rules.value = res.data || []; + ruleCache.value = { + data: rules.value, + timestamp: Date.now() + }; + } catch (e) { + console.error('加载采购规则失败', e); + } finally { + loading.value = false; + } + }; + + const evaluate = async (params: { + budget: number; + isCentralized?: string; + isSpecial?: string; + hasSupplier?: string; + projectType?: string; + }): Promise => { + try { + const res = await evaluateRules(params); + return res.data; + } catch (e) { + console.error('评估规则失败', e); + return null; + } + }; + + const getThresholds = () => { + const thresholds: Record = { + deptPurchase: 50000, + feasibility: 300000, + publicSelect: 400000, + govPurchase: 1000000 + }; + + rules.value.forEach(rule => { + if (rule.ruleCode === 'DEPT_PURCHASE_THRESHOLD' && rule.amountMax) { + thresholds.deptPurchase = Number(rule.amountMax); + } + if (rule.ruleCode === 'FEASIBILITY_REPORT' && rule.amountMin) { + thresholds.feasibility = Number(rule.amountMin); + } + if (rule.ruleCode === 'PUBLIC_BID_40W_100W' && rule.amountMin) { + thresholds.publicSelect = Number(rule.amountMin); + } + if (rule.ruleCode === 'GOV_PURCHASE_THRESHOLD' && rule.amountMin) { + thresholds.govPurchase = Number(rule.amountMin); + } + }); + + return thresholds; + }; + + loadRules(); + + return { + loading, + rules, + loadRules, + evaluate, + getThresholds + }; +} + +let globalRulesInstance: ReturnType | null = null; + +export function usePurchaseRulesSingleton() { + if (!globalRulesInstance) { + globalRulesInstance = usePurchaseRules(); + } + return globalRulesInstance; +} \ No newline at end of file diff --git a/src/views/purchase/purchasingRuleConfig/form.vue b/src/views/purchase/purchasingRuleConfig/form.vue new file mode 100644 index 0000000..98aab36 --- /dev/null +++ b/src/views/purchase/purchasingRuleConfig/form.vue @@ -0,0 +1,246 @@ + + + \ No newline at end of file diff --git a/src/views/purchase/purchasingRuleConfig/index.vue b/src/views/purchase/purchasingRuleConfig/index.vue new file mode 100644 index 0000000..b04bf47 --- /dev/null +++ b/src/views/purchase/purchasingRuleConfig/index.vue @@ -0,0 +1,169 @@ + + + \ No newline at end of file diff --git a/src/views/purchase/purchasingrequisition/add.vue b/src/views/purchase/purchasingrequisition/add.vue index 377a475..38ed37c 100644 --- a/src/views/purchase/purchasingrequisition/add.vue +++ b/src/views/purchase/purchasingrequisition/add.vue @@ -121,12 +121,17 @@ - - - - - - + + + + + + + + + + + 下载商务洽谈表模版 @@ -517,6 +522,7 @@ import { addObj, tempStore, getObj, editObj, getApplyFiles } from '/@/api/purcha import { getTree } from '/@/api/purchase/purchasingcategory'; import { getDicts } from '/@/api/admin/dict'; import { useMessage } from '/@/hooks/message'; +import { usePurchaseRulesSingleton } from '/@/hooks/usePurchaseRules'; import UploadFile from '/@/components/Upload/index.vue'; import other from '/@/utils/other'; import { Document, Download, QuestionFilled } from '@element-plus/icons-vue'; @@ -656,7 +662,8 @@ const dataForm = reactive({ agentId: '', agentName: '', representorName:'', - representorType: '' + representorType: '', + negotiationReason: '' }); /** 查看时展示的招标文件列表(实施采购上传的 type=130) */ const viewImplementPurchaseFiles = ref<{ id: string; fileTitle?: string; createTime?: string; remark?: string }[]>([]); @@ -714,11 +721,13 @@ Object.entries(FILE_TYPE_MAP).forEach(([field, type]) => { FILE_TYPE_TO_FIELDS[type].push(field); }); -// 金额阈值常量(与后端 PurchaseConstants 保持一致) -const BUDGET_DEPT_PURCHASE_THRESHOLD = 50000; // 部门自行采购上限(< 5 万) -const BUDGET_FEASIBILITY_THRESHOLD = 300000; // 可行性论证/会议纪要起点(≥ 30 万) -const BUDGET_PUBLIC_SELECT_THRESHOLD = 400000; // 公开比选起点(≥ 40 万) -const BUDGET_GOV_PURCHASE_THRESHOLD = 1000000; // 政府采购起点(≥ 100 万) +// 金额阈值(从规则配置动态获取,默认值与后端 PurchaseConstants 保持一致) +const { rules: purchaseRules, getThresholds, evaluate: evaluatePurchaseRules } = usePurchaseRulesSingleton(); + +const BUDGET_DEPT_PURCHASE_THRESHOLD = computed(() => getThresholds().deptPurchase || 50000); +const BUDGET_FEASIBILITY_THRESHOLD = computed(() => getThresholds().feasibility || 300000); +const BUDGET_PUBLIC_SELECT_THRESHOLD = computed(() => getThresholds().publicSelect || 400000); +const BUDGET_GOV_PURCHASE_THRESHOLD = computed(() => getThresholds().govPurchase || 1000000); // 部门采购方式字典 value(与 DeptPurchaseTypeEnum 一致) const DEPT_PURCHASE_TYPE = { @@ -775,7 +784,7 @@ const isDeptPurchase = computed(() => { return !!(isSpecialNoValue && isCentralizedNoValue && dataForm.isSpecial === isSpecialNoValue && dataForm.isCentralized === isCentralizedNoValue && - dataForm.budget && dataForm.budget < 50000); + dataForm.budget && dataForm.budget < BUDGET_DEPT_PURCHASE_THRESHOLD.value); }); // 是否为“委托采购中心采购”途径 @@ -795,8 +804,8 @@ const showPurchaseDetailBlocks = computed(() => { const schoolUnifiedPurchaseFormDefault = computed(() => { if (isDeptPurchase.value || dataForm.budget == null) return null; const budget = Number(dataForm.budget); - if (budget >= BUDGET_GOV_PURCHASE_THRESHOLD) return '1'; // 政府采购 - if (budget >= BUDGET_DEPT_PURCHASE_THRESHOLD && budget < BUDGET_GOV_PURCHASE_THRESHOLD) { + if (budget >= BUDGET_GOV_PURCHASE_THRESHOLD.value) return '1'; // 政府采购 + if (budget >= BUDGET_DEPT_PURCHASE_THRESHOLD.value && budget < BUDGET_GOV_PURCHASE_THRESHOLD.value) { if (dataForm.isCentralized === '0') return '2'; // 集采=否 → 学校自主采购 if (dataForm.isCentralized === '1') return '1'; // 政府集中采购 → 政府采购 if (dataForm.isCentralized === '2') return '2'; // 学校集中采购 → 学校自主采购 @@ -959,7 +968,7 @@ watch( const isAutoSelectPurchaseType = computed(() => { if (!dataForm.budget) return false; const budget = dataForm.budget; - return budget >= BUDGET_DEPT_PURCHASE_THRESHOLD && budget < BUDGET_PUBLIC_SELECT_THRESHOLD && isServiceCategory.value && isSpecialServiceCategory.value; + return budget >= BUDGET_DEPT_PURCHASE_THRESHOLD.value && budget < BUDGET_PUBLIC_SELECT_THRESHOLD.value && isServiceCategory.value && isSpecialServiceCategory.value; }); // 判断是否显示自动邀请比选模版(5万<=金额<40万,服务类目,特殊服务类目) @@ -967,7 +976,7 @@ const showAutoInviteSelect = computed(() => { if (!isDeptPurchase.value) return false; if (!dataForm.budget) return false; const budget = dataForm.budget; - return budget >= BUDGET_DEPT_PURCHASE_THRESHOLD && budget < BUDGET_PUBLIC_SELECT_THRESHOLD && isServiceCategory.value && isSpecialServiceCategory.value; + return budget >= BUDGET_DEPT_PURCHASE_THRESHOLD.value && budget < BUDGET_PUBLIC_SELECT_THRESHOLD.value && isServiceCategory.value && isSpecialServiceCategory.value; }); // 判断是否显示学校统一采购的自动邀请比选模版(5万<=金额<40万,服务类目,特殊服务类目) @@ -975,7 +984,7 @@ const showAutoInviteSelectSchool = computed(() => { if (isDeptPurchase.value) return false; if (!dataForm.budget) return false; const budget = dataForm.budget; - return budget >= BUDGET_DEPT_PURCHASE_THRESHOLD && budget < BUDGET_PUBLIC_SELECT_THRESHOLD && isSpecialServiceCategory.value; + return budget >= BUDGET_DEPT_PURCHASE_THRESHOLD.value && budget < BUDGET_PUBLIC_SELECT_THRESHOLD.value && isSpecialServiceCategory.value; }); // 判断是否显示自动公开比选模版(40万<=金额<100万,特殊服务类目:isMallService=1、isProjectService=1) @@ -983,7 +992,7 @@ const showAutoPublicSelect = computed(() => { if (isDeptPurchase.value) return false; if (!dataForm.budget) return false; const budget = dataForm.budget; - return budget >= BUDGET_PUBLIC_SELECT_THRESHOLD && budget < BUDGET_GOV_PURCHASE_THRESHOLD && isSpecialServiceCategory.value; + return budget >= BUDGET_PUBLIC_SELECT_THRESHOLD.value && budget < BUDGET_GOV_PURCHASE_THRESHOLD.value && isSpecialServiceCategory.value; }); // 获取需求文件的 prop 名称(用于表单验证) @@ -1005,7 +1014,7 @@ const isAutoSelectPurchaseTypeUnion = computed(() => { if (isDeptPurchase.value) return false; if (!dataForm.budget) return false; const budget = dataForm.budget; - return budget >= BUDGET_DEPT_PURCHASE_THRESHOLD && budget < BUDGET_PUBLIC_SELECT_THRESHOLD && isSpecialServiceCategory.value; + return budget >= BUDGET_DEPT_PURCHASE_THRESHOLD.value && budget < BUDGET_PUBLIC_SELECT_THRESHOLD.value && isSpecialServiceCategory.value; }); // 监听品目编码、预算金额、采购类型及采购途径变化,自动设置/清空采购方式 @@ -1217,6 +1226,20 @@ const dataRules = reactive({ trigger: 'change' } ], + negotiationReason: [ + { + validator: (_rule: any, value: string, callback: (e?: Error) => void) => { + if (isPurchaseType(DEPT_PURCHASE_TYPE.BUSINESS_NEGOTIATION)) { + if (!value || String(value).trim() === '') { + callback(new Error('采购方式为商务洽谈时,洽谈理由不能为空')); + return; + } + } + callback(); + }, + trigger: 'blur' + } + ], }); // 取消 @@ -1298,6 +1321,7 @@ async function loadDetail(applyId: string | number) { agentName: detail.agentName ?? '', representorName: detail.representorName ?? '', representorType: detail.representorType ?? '', + negotiationReason: detail.negotiationReason ?? '', }); setCategoryCodePath(); try {