From fb308ca6912c3d89cebaf2fdab66497d287e6009 Mon Sep 17 00:00:00 2001 From: nebula_chen Date: Tue, 31 Mar 2026 19:37:45 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8A=A5=E4=BB=B7=E5=8D=95=E8=AF=A6=E6=83=85?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E3=80=81=E8=AF=A2=E4=BB=B7=E5=8D=95=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E7=AA=97=E5=8F=A3=E3=80=81=E6=AF=94=E4=BB=B7=E7=AA=97?= =?UTF-8?q?=E5=8F=A3=E8=BD=AC=E4=B8=BA=E5=AF=B9=E5=BA=94=E8=B7=AF=E7=94=B1?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apps/pisadmin/miscprocurement/models.py | 14 +- web/src/router/backEnd.ts | 85 +- .../miscprocurement/rfqmiscellaneous/api.ts | 2 + .../rfqmiscellaneous/comparePrice.vue | 1595 +++++++ .../miscprocurement/rfqmiscellaneous/crud.tsx | 207 +- .../rfqmiscellaneous/index.vue | 3783 +---------------- .../rfqmiscellaneous/miscInquiryDetail.vue | 2613 ++++++++++++ web/src/views/pissupplier/quotation/api.ts | 3 +- web/src/views/pissupplier/quotation/crud.tsx | 84 +- .../views/pissupplier/quotation/detail.vue | 837 ++++ web/src/views/pissupplier/quotation/index.vue | 693 +-- 11 files changed, 5459 insertions(+), 4457 deletions(-) create mode 100644 web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/comparePrice.vue create mode 100644 web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/miscInquiryDetail.vue create mode 100644 web/src/views/pissupplier/quotation/detail.vue diff --git a/backend/apps/pisadmin/miscprocurement/models.py b/backend/apps/pisadmin/miscprocurement/models.py index 08b0dcc..3ddafc9 100644 --- a/backend/apps/pisadmin/miscprocurement/models.py +++ b/backend/apps/pisadmin/miscprocurement/models.py @@ -266,7 +266,7 @@ class Inquiry(CoreModel): bid_end_time = models.DateTimeField(null=True, blank=True, verbose_name="投标截止时间") class Meta: - db_table = table_prefix + "proc_inquiry_master" + db_table = table_prefix + "misc_proc_inquiry_master" verbose_name = "询价单" verbose_name_plural = verbose_name ordering = ("-create_time", "-id") @@ -401,7 +401,7 @@ class InquirySupplier(models.Model): create_user = models.CharField(max_length=20, db_column="createuser", null=True, blank=True, verbose_name="添加人") class Meta: - db_table = table_prefix + "proc_inquiry_supplier" + db_table = table_prefix + "misc_proc_inquiry_supplier" verbose_name = "杂采询价单-供应商关联表" verbose_name_plural = verbose_name unique_together = ("inquiry_no", "part_id", "supplier_code") @@ -436,7 +436,7 @@ class InquiryAttachment(models.Model): upload_user = models.CharField(max_length=20, db_column="uploaduser", null=True, blank=True, verbose_name="上传人") class Meta: - db_table = table_prefix + "proc_inquiry_attachment" + db_table = table_prefix + "misc_proc_inquiry_attachment" verbose_name = "杂采询价单-附件关联表" verbose_name_plural = verbose_name unique_together = ("inquiry_no", "part_id", "file_name") @@ -484,7 +484,7 @@ class InquiryMaterialCost(models.Model): create_user = models.CharField(max_length=20, db_column="createuser", null=True, blank=True, verbose_name="创建人") class Meta: - db_table = table_prefix + "proc_inquiry_material_cost" + db_table = table_prefix + "misc_proc_inquiry_material_cost" verbose_name = "杂采询价单-材料成本明细表" verbose_name_plural = verbose_name ordering = ("id",) @@ -526,7 +526,7 @@ class InquiryProcessCost(models.Model): create_user = models.CharField(max_length=20, db_column="createuser", null=True, blank=True, verbose_name="创建人") class Meta: - db_table = table_prefix + "proc_inquiry_process_cost" + db_table = table_prefix + "misc_proc_inquiry_process_cost" verbose_name = "杂采询价单-加工成本明细表" verbose_name_plural = verbose_name ordering = ("id",) @@ -560,7 +560,7 @@ class InquiryOtherCost(models.Model): create_user = models.CharField(max_length=20, db_column="createuser", null=True, blank=True, verbose_name="创建人") class Meta: - db_table = table_prefix + "proc_inquiry_other_cost" + db_table = table_prefix + "misc_proc_inquiry_other_cost" verbose_name = "杂采询价单-其它成本明细表" verbose_name_plural = verbose_name ordering = ("id",) @@ -594,7 +594,7 @@ class InquiryProfitCost(models.Model): create_user = models.CharField(max_length=20, db_column="createuser", null=True, blank=True, verbose_name="创建人") class Meta: - db_table = table_prefix + "proc_inquiry_profit_cost" + db_table = table_prefix + "misc_proc_inquiry_profit_cost" verbose_name = "杂采询价单-税费利润明细表" verbose_name_plural = verbose_name ordering = ("id",) diff --git a/web/src/router/backEnd.ts b/web/src/router/backEnd.ts index 4b45623..e810bd8 100644 --- a/web/src/router/backEnd.ts +++ b/web/src/router/backEnd.ts @@ -1,4 +1,72 @@ import { RouteRecordRaw } from 'vue-router'; + +/** 供应商报价详情(隐藏菜单)。仅带 `?mode=edit` 为编辑,否则默认查看(含仅点标签页进入)。 */ +const pissupplierQuotationDetailRoute: RouteRecordRaw = { + path: '/pissupplier/quotation/detail/:id', + name: 'PissupplierQuotationDetail', + component: () => import('/@/views/pissupplier/quotation/detail.vue'), + meta: { + title: '报价单详情', + isLink: '', + isHide: true, + isKeepAlive: false, + isAffix: false, + isIframe: false, + roles: ['admin'], + icon: '' + } +}; + +/** 采购端杂采询价单详情(隐藏菜单)。`id=new` 且 `mode=create` 为新建;否则 `id` 为数字主键,`mode=edit|view`。 */ +const pisadminRfqMiscInquiryDetailRoute: RouteRecordRaw = { + path: '/pisadmin/miscprocurement/rfqmiscellaneous/miscInquiryDetail/:id', + name: 'PisadminRfqMiscInquiryDetail', + component: () => import('/@/views/pisadmin/miscprocurement/rfqmiscellaneous/miscInquiryDetail.vue'), + meta: { + title: '询价单详情', + isLink: '', + isHide: true, + isKeepAlive: false, + isAffix: false, + isIframe: false, + roles: ['admin'], + icon: '' + } +}; + +/** 杂采比价/议价(隐藏菜单)。`id` 为询价单主键。 */ +const pisadminRfqMiscComparePriceRoute: RouteRecordRaw = { + path: '/pisadmin/miscprocurement/rfqmiscellaneous/comparePrice/:id', + name: 'PisadminRfqMiscComparePrice', + component: () => import('/@/views/pisadmin/miscprocurement/rfqmiscellaneous/comparePrice.vue'), + meta: { + title: '比价/议价', + isLink: '', + isHide: true, + isKeepAlive: false, + isAffix: false, + isIframe: false, + roles: ['admin'], + icon: '' + } +}; + +/** 杂采材料管理(隐藏菜单,供比价页链入;菜单中若已配置同页可并存)。 */ +const pisadminMiscMaterialsIndexRoute: RouteRecordRaw = { + path: '/pisadmin/miscprocurement/misc_materials/index', + name: 'PisadminMiscMaterialsIndex', + component: () => import('/@/views/pisadmin/miscprocurement/misc_materials/index.vue'), + meta: { + title: '杂采材料管理', + isLink: '', + isHide: true, + isKeepAlive: false, + isAffix: false, + isIframe: false, + roles: ['admin'], + icon: '' + } +}; import { storeToRefs } from 'pinia'; import pinia from '/@/stores/index'; import { useUserInfo } from '/@/stores/userInfo'; @@ -53,7 +121,14 @@ export async function initBackEndControlRoutes() { // if (res.data.length <= 0) return Promise.resolve(true); // 处理路由(component),替换 dynamicRoutes(/@/router/route)第一个顶级 children 的路由 const {frameIn,frameOut} = handleMenu(res.data) - dynamicRoutes[0].children = await backEndComponent(frameIn); + const frameInProcessed = await backEndComponent(frameIn) + dynamicRoutes[0].children = [ + ...(frameInProcessed || []), + pissupplierQuotationDetailRoute, + pisadminRfqMiscInquiryDetailRoute, + pisadminRfqMiscComparePriceRoute, + pisadminMiscMaterialsIndexRoute + ] // 添加动态路由 await setAddRoute(); // 设置路由到 vuex routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组 @@ -64,7 +139,13 @@ export async function setRouters(){ const {frameInRoutes,frameOutRoutes} = await useFrontendMenuStore().getRouter() const frameInRouter = toRaw(frameInRoutes) const frameOutRouter = toRaw(frameOutRoutes) - dynamicRoutes[0].children = frameInRouter + dynamicRoutes[0].children = [ + ...frameInRouter, + pissupplierQuotationDetailRoute, + pisadminRfqMiscInquiryDetailRoute, + pisadminRfqMiscComparePriceRoute, + pisadminMiscMaterialsIndexRoute + ] dynamicRoutes.forEach((item:any)=>{ router.addRoute(item) }) diff --git a/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/api.ts b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/api.ts index d30e18e..e66cd2d 100644 --- a/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/api.ts +++ b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/api.ts @@ -2,6 +2,8 @@ import { request } from '/@/utils/service' const baseUrl = '/api/pisadmin/miscprocurement/inquiry/' +/** 列表/增删改 API;完整表单在隐藏路由 `PisadminRfqMiscInquiryDetail`(`miscInquiryDetail/:id`)。议价相关接口由隐藏路由 `PisadminRfqMiscComparePrice`(`comparePrice/:id`)使用。 */ + export const GetList = (params: any) => request({ url: baseUrl, method: 'get', params }) export const GetObj = (id: string | number) => request({ url: baseUrl + id + '/', method: 'get' }) export const AddObj = (data: any) => request({ url: baseUrl, method: 'post', data }) diff --git a/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/comparePrice.vue b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/comparePrice.vue new file mode 100644 index 0000000..e5e3d58 --- /dev/null +++ b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/comparePrice.vue @@ -0,0 +1,1595 @@ + + + + + diff --git a/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/crud.tsx b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/crud.tsx index db173d3..b24afa5 100644 --- a/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/crud.tsx +++ b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/crud.tsx @@ -65,8 +65,8 @@ type ExtraHooks = { onEdit?: (row: any) => void onView?: (row: any) => void /** - * 打开比价/议价弹窗(比议价中、价格审核、核价通过);保存时按报价单写入杂采议价记录(议价前总价快照等)。 - * 弹窗内表头供应商名称可点击,预览对应 GET quotation_master/{autoid}/ 报价明细。 + * 跳转杂采比价/议价路由页(`/pisadmin/miscprocurement/rfqmiscellaneous/comparePrice/:id`;比议价中、价格审核、核价通过等由页内按钮状态控制)。 + * 比价页表头供应商名称可点击,进入报价单详情(`/pissupplier/quotation/detail/:id`,查看模式)。 */ onComparison?: (row: any) => void /** 列表多选变化(用于后续多询价单比价等) */ @@ -276,6 +276,20 @@ export function shouldSkipProcessDetailMetric(fieldKey: string | undefined): boo return normCmpTplKey(fieldKey) === 'processstation' } +/** 制程最低价列:重量/单价行跳转目标 */ +export type CompareMinLink = + | { kind: 'quotation'; id: string | number } + /** 杂采材料单价更低时链向杂采材料管理;sourceFactory 为对应「交易厂区」文案,供浮窗展示 */ + | { kind: 'misc_materials'; sourceFactory?: string } + /** 主表「材料成本」:最低重量与最低单价来自不同报价单(或单价来自杂采材料信息)时,仅展示浮窗说明,不跳转 */ + | { + kind: 'material_cost_split' + weight: number + unitPrice: number + weightSource: string + unitPriceSource: string + } + /** 与后端制程最低价落库一致:全报价最低重量/最低单价(单价含杂采材料信息)、材料费=三者乘积;加工费=各报价单加工费合计之最小值 */ export type LowPriceMinContext = { minW?: number @@ -283,27 +297,97 @@ export type LowPriceMinContext = { materialProduct?: number minProcessTotal?: number pid: string + /** 全报价中材料明细最低重量所在报价单主键 */ + minWeightQuotationId?: string | number + /** 全报价中材料明细最低单价所在报价单主键(若最低单价由杂采材料信息决定则为空) */ + minUnitPriceQuotationId?: string | number + /** 仅当杂采单价严格低于所有报价单价时,单价/材料成本链向杂采材料管理;与报价持平或更高则链向报价单 */ + minUnitPriceFromMisc?: boolean + /** 杂采材料最低价对应交易厂区(与杂采材料表 factory 等字段一致,供浮窗) */ + miscMinFactory?: string +} + +const _nearlyEqualMin = (a: number, b: number) => Math.abs(a - b) < 1e-6 + +/** 各供应商列数值完全一致时,制程最低价列不展示超链接(仅纯文本) */ +export function allCompareSupplierValuesEqual( + values: Record, + supplierKeys: string[] +): boolean { + if (!supplierKeys.length) return true + const nums: number[] = [] + for (const k of supplierKeys) { + const v = values[k] + if (v === '-' || v === '' || v == null) return false + const n = Number(v) + if (!Number.isFinite(n)) return false + nums.push(n) + } + if (nums.length !== supplierKeys.length) return false + const a0 = nums[0]! + return nums.every((x) => _nearlyEqualMin(x, a0)) } export function computeLowPriceMinContext( quotes: any[], - miscMinUnitPrice?: number | null + miscMinUnitPrice?: number | null, + miscMinFactory?: string | null ): LowPriceMinContext { const pid = String(quotes[0]?.rfq_items?.[0]?.part_id || '').trim() const qty = Number(quotes[0]?.rfq_items?.[0]?.qty) let minW: number | undefined - let minUp: number | undefined + /** 各报价单材料行单价之最小值(不含杂采) */ + let minQuoteUp: number | undefined + let minWeightQuotationId: string | number | undefined + let minUnitPriceQuotationId: string | number | undefined + const qid = (q: any) => q?.autoid ?? q?.id + for (const q of quotes) { for (const r of q.material_costs || []) { if (pid && String(r.part_id || '').trim() !== pid) continue const w = Number(r.weight) - if (Number.isFinite(w)) minW = minW === undefined ? w : Math.min(minW, w) + if (Number.isFinite(w)) { + if (minW === undefined || w < minW) { + minW = w + minWeightQuotationId = qid(q) + } + } const up = Number(r.unit_price) - if (Number.isFinite(up)) minUp = minUp === undefined ? up : Math.min(minUp, up) + if (Number.isFinite(up)) { + if (minQuoteUp === undefined || up < minQuoteUp) { + minQuoteUp = up + minUnitPriceQuotationId = qid(q) + } + } } } + + let minUp: number | undefined = minQuoteUp + let minUnitPriceFromMisc = false + let miscFactoryOut: string | undefined + const pickMiscFactory = () => { + const f = miscMinFactory != null && String(miscMinFactory).trim() !== '' ? String(miscMinFactory).trim() : '' + if (f) miscFactoryOut = f + } + if (miscMinUnitPrice != null && Number.isFinite(miscMinUnitPrice)) { - minUp = minUp === undefined ? miscMinUnitPrice : Math.min(minUp, miscMinUnitPrice) + const misc = miscMinUnitPrice + if (minQuoteUp === undefined) { + minUp = misc + minUnitPriceFromMisc = true + minUnitPriceQuotationId = undefined + pickMiscFactory() + } else if (misc < minQuoteUp && !_nearlyEqualMin(misc, minQuoteUp)) { + /** 仅当杂采单价严格低于所有报价单价时,制程最低价单价才记为来自杂采 */ + minUp = misc + minUnitPriceFromMisc = true + minUnitPriceQuotationId = undefined + pickMiscFactory() + } else { + /** 杂采高于或与最低报价持平:取 min(报价最低, 杂采),来源优先报价单(含持平) */ + minUp = Math.min(minQuoteUp, misc) + minUnitPriceFromMisc = false + } } let materialProduct: number | undefined if (minW !== undefined && minUp !== undefined && Number.isFinite(qty) && qty > 0) { @@ -323,27 +407,126 @@ export function computeLowPriceMinContext( } if (ok) minProcessTotal = minProcessTotal === undefined ? s : Math.min(minProcessTotal, s) } - return { minW, minUp, materialProduct, minProcessTotal, pid } + return { + minW, + minUp, + materialProduct, + minProcessTotal, + pid, + minWeightQuotationId, + minUnitPriceQuotationId, + minUnitPriceFromMisc, + miscMinFactory: miscFactoryOut + } +} + +/** 主表「材料成本」制程最低价:同源则链向报价单详情,异源则链样式 + 浮窗说明分项来源 */ +export function buildMaterialCostMinLink( + ctx: LowPriceMinContext, + quotes: any[], + opts?: { materialRowValues?: Record; supplierKeys?: string[] } +): CompareMinLink | null { + const materialAllEqual = + !!opts?.materialRowValues && + !!opts.supplierKeys?.length && + allCompareSupplierValuesEqual(opts.materialRowValues, opts.supplierKeys) + + /** 各报价一致且杂采单价严格低于所有报价:制程最低价指向杂采材料管理 */ + if (materialAllEqual && ctx.minUnitPriceFromMisc) { + return { + kind: 'misc_materials', + sourceFactory: ctx.miscMinFactory + } + } + if (materialAllEqual) { + return null + } + if (ctx.materialProduct === undefined) return null + const w = ctx.minW + const up = ctx.minUp + if (w === undefined || up === undefined || !Number.isFinite(w) || !Number.isFinite(up)) return null + + const wid = ctx.minWeightQuotationId + const uid = ctx.minUnitPriceQuotationId + const misc = ctx.minUnitPriceFromMisc + + const quoteLabel = (qid: string | number | undefined) => { + if (qid == null || qid === '') return '' + const q = quotes.find((x) => String(x.autoid ?? x.id) === String(qid)) + const name = String(q?.supplier_name || q?.supplierName || '').trim() + const code = String(q?.supplier_code || q?.supplierCode || '').trim() + const label = name || code + return label || `报价单 #${qid}` + } + + const weightSrc = quoteLabel(wid) + const unitSrc = misc ? '杂采材料信息' : quoteLabel(uid) + + const sameQuotation = + !misc && wid != null && wid !== '' && uid != null && uid !== '' && String(wid) === String(uid) + + if (sameQuotation) { + return { kind: 'quotation', id: wid } + } + + return { + kind: 'material_cost_split', + weight: w, + unitPrice: up, + weightSource: weightSrc || '—', + unitPriceSource: unitSrc || '—' + } } /** 展开仍按规格分行时,「制程最低价」列用全局口径覆盖该行 min */ export function applyMaterialDetailLowPriceMin( - line: { min?: number }, + line: { min?: number; minLink?: CompareMinLink | null; values: Record }, m: ComparisonDetailMetric, - ctx: LowPriceMinContext + ctx: LowPriceMinContext, + supplierKeys?: string[] ): void { const fk = m.fieldKey ? normCmpTplKey(m.fieldKey) : '' const label = String(m.label || '').trim() if (ctx.minW !== undefined && (fk === 'weight' || /用量|重量/.test(label))) { line.min = ctx.minW + const qid = ctx.minWeightQuotationId + line.minLink = + qid != null && qid !== '' + ? { kind: 'quotation', id: qid } + : null + if (supplierKeys?.length && allCompareSupplierValuesEqual(line.values, supplierKeys)) { + line.minLink = null + } return } - if (ctx.minUp !== undefined && (fk === 'unitprice' || /材料单价/.test(label))) { + const isUnitPriceRow = + fk === 'unitprice' || /材料单价/.test(label) || label === '单价' + if (ctx.minUp !== undefined && isUnitPriceRow) { line.min = ctx.minUp + if (ctx.minUnitPriceFromMisc) { + line.minLink = { + kind: 'misc_materials', + sourceFactory: ctx.miscMinFactory + } + } else { + const qid = ctx.minUnitPriceQuotationId + line.minLink = + qid != null && qid !== '' + ? { kind: 'quotation', id: qid } + : null + } + if ( + supplierKeys?.length && + allCompareSupplierValuesEqual(line.values, supplierKeys) && + !ctx.minUnitPriceFromMisc + ) { + line.minLink = null + } return } if (ctx.materialProduct !== undefined && (fk === 'materialcost' || /材料费用|材料费/.test(label))) { line.min = ctx.materialProduct + line.minLink = null } } @@ -427,7 +610,7 @@ export const createCrudOptions = function ({ }, request: { pageRequest: async (query) => api.GetList(query), - // 新版:新增/编辑由 index.vue 自定义弹窗负责(嵌套子表一次提交) + // 新版:新增/编辑由 detail.vue 路由页负责(嵌套子表一次提交) addRequest: async ({ form }) => api.AddObj(form), editRequest: async ({ form, row }) => api.UpdateObj({ ...form, id: row.id }), delRequest: async ({ row }) => { diff --git a/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/index.vue b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/index.vue index 0055fe2..cde2ced 100644 --- a/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/index.vue +++ b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/index.vue @@ -1,3514 +1,57 @@ - + diff --git a/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/miscInquiryDetail.vue b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/miscInquiryDetail.vue new file mode 100644 index 0000000..56b7b1b --- /dev/null +++ b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/miscInquiryDetail.vue @@ -0,0 +1,2613 @@ + + + + + diff --git a/web/src/views/pissupplier/quotation/api.ts b/web/src/views/pissupplier/quotation/api.ts index 0878479..62e7e48 100644 --- a/web/src/views/pissupplier/quotation/api.ts +++ b/web/src/views/pissupplier/quotation/api.ts @@ -31,7 +31,8 @@ export type PaymentCode = 1 | 2 | 3 | 4 * 报价主表(QuotationMaster)仅含 inquiry_no,不含询价名称与询价模板编号;列表展示由 crud 中 * 关联 Inquiry 与成本/价格模板接口补全 title、template 与模板名称。 * - * 更新(PUT)在弹窗「保存」时调用,不改变 status / quotetime;正式提交走 POST `{id}/submit/`。 + * 前端详情路由:`#/.../quotation/detail/:id` 无 `mode` 时默认**查看**;列表「报价」会带 `?mode=edit`。 + * 更新(PUT)在报价详情页「保存」时调用,不改变 status / quotetime;正式提交走 POST `{id}/submit/`。 * 更新(PUT)时序列化要求 supplier_code / supplier_name 等字段;前端从列表/详情映射 supplierCode 并在保存时写回。 * 成本结构须提交 material_costs、process_costs、other_costs、profit_costs(与 QuotationMasterCreateUpdateSerializer), * 勿使用 cost_items——后端不识别,子表不会更新。 diff --git a/web/src/views/pissupplier/quotation/crud.tsx b/web/src/views/pissupplier/quotation/crud.tsx index 6b87d13..edd00d5 100644 --- a/web/src/views/pissupplier/quotation/crud.tsx +++ b/web/src/views/pissupplier/quotation/crud.tsx @@ -395,6 +395,20 @@ const paymentMapBackendToFront: Record = { 4: 'tt_30_70' } +/** 与报价基础信息 `paymentTerm` 选项一致,供详情页只读展示 */ +export const PAYMENT_TERM_LABELS: Record = { + tt_30_70: 'T/T 30%预付,70%出货前', + net30: '月结30天', + net45: '月结45天', + prepaid: '全额预付' +} + +export function formatPaymentTermLabel(term: string | null | undefined) { + if (term == null || String(term).trim() === '') return '—' + const k = String(term).trim() + return PAYMENT_TERM_LABELS[k] ?? k +} + const costEnabledSections = ['材料成本', '加工成本', '其它成本', '利润', '税金'] const costDisabledSections = ['产品明细', '利润', '税金'] @@ -1092,7 +1106,8 @@ const costRowsToNestedPayload = (rows: CostRow[]) => { return { material_costs, process_costs, other_costs, profit_costs } } -const unwrapQuotationDetail = (res: any): any => { +/** 详情接口响应解包(列表弹窗与详情页共用) */ +export const unwrapQuotationDetail = (res: any): any => { const r = res?.data !== undefined ? res.data : res if (r && typeof r === 'object' && r.quotation_no != null) return r if (r && typeof r === 'object' && r.data && typeof r.data === 'object' && r.data.quotation_no != null) return r.data @@ -1179,7 +1194,14 @@ function blankQuote(): Quote { } } -export function useQuoteCrud(options?: { onChange?: () => void }) { +export function useQuoteCrud(options?: { + onChange?: () => void + /** 列表页用弹窗;详情页用独立路由,不打开 dialog */ + uiContext?: 'list' | 'detail' + /** 详情页保存成功后(例如返回列表) */ + onSaveSuccess?: () => void +}) { + const uiContext = options?.uiContext ?? 'list' const filters = reactive<{ inquiryPlant: string isAwarded: string | number | '' @@ -1630,7 +1652,9 @@ export function useQuoteCrud(options?: { onChange?: () => void }) { } onMounted(() => { - loadQuotes() + if (uiContext === 'list') { + loadQuotes() + } loadMaterialOptions() loadStationOptions() loadUnitOptions() @@ -1860,26 +1884,41 @@ export function useQuoteCrud(options?: { onChange?: () => void }) { current.costItems = costItemsForCurrent.value } + /** 编辑报价:由列表「报价」进入(`?mode=edit`)或本页显式打开;路由无 `mode` 时详情页走 `viewQuote` */ const openQuote = async (row: Quote) => { - const bidReason = getSupplierBidWindowRejectReason(row) + /** 路由详情仅带 id 时先拉主表,保证招标投标窗口校验正确 */ + let rowForFlow = row + if (row.id && row.buyingMethod == null) { + try { + const preRes = await api.getDetail(row.id) + const preRaw = unwrapQuotationDetail(preRes) + if (preRaw) { + rowForFlow = mapBackendQuote(preRaw) + await enrichQuotesWithInquiryData([rowForFlow]) + } + } catch (e) { + console.warn('预加载报价详情失败', e) + } + } + const bidReason = getSupplierBidWindowRejectReason(rowForFlow) if (bidReason) { ElMessage.warning(bidReason) return } dialog.mode = 'edit' - dialog.quoteId = row.id + dialog.quoteId = rowForFlow.id loading.value = true try { await ensureTemplateNameLookup() - let quoteData: Quote = { ...row } + let quoteData: Quote = { ...rowForFlow } let rawDetail: any = null - if (row.id) { + if (rowForFlow.id) { // 点击「报价」后先进入报价中(2),以便后续保存/编辑仍可通过后端校验 - if (isPendingQuotation(row)) { - await api.quoteOfficial(row.id) + if (isPendingQuotation(rowForFlow)) { + await api.quoteOfficial(rowForFlow.id) } try { - const detailRes = await api.getDetail(row.id) + const detailRes = await api.getDetail(rowForFlow.id) rawDetail = unwrapQuotationDetail(detailRes) if (rawDetail) quoteData = mapBackendQuote(rawDetail) } catch (e) { @@ -1887,10 +1926,10 @@ export function useQuoteCrud(options?: { onChange?: () => void }) { } } // 详情接口若未展开 supplier 字段,保留列表行上的供应商代码/名称(PUT 保存必填) - if (!quoteData.supplierCode && row.supplierCode) quoteData.supplierCode = row.supplierCode - if (!quoteData.supplierName && row.supplierName) quoteData.supplierName = row.supplierName - if (quoteData.inquiryStatusCode == null && row.inquiryStatusCode != null) { - quoteData.inquiryStatusCode = row.inquiryStatusCode + if (!quoteData.supplierCode && rowForFlow.supplierCode) quoteData.supplierCode = rowForFlow.supplierCode + if (!quoteData.supplierName && rowForFlow.supplierName) quoteData.supplierName = rowForFlow.supplierName + if (quoteData.inquiryStatusCode == null && rowForFlow.inquiryStatusCode != null) { + quoteData.inquiryStatusCode = rowForFlow.inquiryStatusCode } let effectiveRows: CostRow[] = [] @@ -1898,7 +1937,7 @@ export function useQuoteCrud(options?: { onChange?: () => void }) { let inquiry: any = null try { - const inquiryCode = quoteData.inquiryCode || (row as any).inquiry_no + const inquiryCode = quoteData.inquiryCode || (rowForFlow as any).inquiry_no if (inquiryCode) { const res = await inquiryApi.GetList({ inquiry_no: inquiryCode, @@ -1991,7 +2030,9 @@ export function useQuoteCrud(options?: { onChange?: () => void }) { const idx = quotes.value.findIndex((q) => q.id === quoteData.id) if (idx >= 0) quotes.value.splice(idx, 1, quoteData) } - dialog.visible = true + if (uiContext === 'list') { + dialog.visible = true + } } finally { loading.value = false } @@ -2000,7 +2041,9 @@ export function useQuoteCrud(options?: { onChange?: () => void }) { const viewQuote = (row: Quote) => { dialog.mode = 'view' dialog.quoteId = row.id - dialog.visible = true + if (uiContext === 'list') { + dialog.visible = true + } loading.value = true ;(async () => { try { @@ -2202,7 +2245,11 @@ export function useQuoteCrud(options?: { onChange?: () => void }) { const idx = quotes.value.findIndex((q) => q.id === updated.id) if (idx >= 0) quotes.value.splice(idx, 1, updated) else quotes.value.unshift(updated) - dialog.visible = false + if (uiContext === 'detail') { + options?.onSaveSuccess?.() + } else { + dialog.visible = false + } ElMessage.success('已保存') options?.onChange?.() } catch (err) { @@ -2418,6 +2465,7 @@ export function useQuoteCrud(options?: { onChange?: () => void }) { statusOptions, resetFilter, filteredQuotes, + loading, loadQuotes, isPendingQuotation, isQuotedQuotation, diff --git a/web/src/views/pissupplier/quotation/detail.vue b/web/src/views/pissupplier/quotation/detail.vue new file mode 100644 index 0000000..53b96dd --- /dev/null +++ b/web/src/views/pissupplier/quotation/detail.vue @@ -0,0 +1,837 @@ + + + + + diff --git a/web/src/views/pissupplier/quotation/index.vue b/web/src/views/pissupplier/quotation/index.vue index b1b004e..8b10c2f 100644 --- a/web/src/views/pissupplier/quotation/index.vue +++ b/web/src/views/pissupplier/quotation/index.vue @@ -17,334 +17,25 @@ {{ formatMoney(row.quoteAmount) }} - - -
- -
-
- 报价状态: - {{ statusLabel(current.status) }} -
-
- - - - -
- - - - - - - - - - - - - - - - - - - - - 询价附件 -
-
-
{{ group.label }}
- -
-
暂无询价附件
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 报价合计 -
- - - - - - -
-
未税价
-
{{ formatMoney(quoteAmountPreTax) }}
-
最终报价
-
{{ formatMoney(quoteTotal) }}
-
-
- - 报价附件与说明 -
- - - -
拖拽或点击上传 (PDF/DOC/JPG/PNG)
-
- -
-
- - -
- - 清空已填内容 -
-
-
-
-
{{ section }}
- - 新增一行 -
- - - - - - - - -
- -
-
-
-
{{ section }}
-
- - - - - -
-
-
-
- -
-
- - -