From 68357babda23ca505bbc500116cd9d015d82a075 Mon Sep 17 00:00:00 2001 From: wintsa <770775984@qq.com> Date: Thu, 19 Mar 2026 17:22:50 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96generateTemplate=E6=96=B9?= =?UTF-8?q?=E6=B3=95,=E4=BF=AE=E5=A4=8D=E4=B8=8B=E8=BD=BDa=E9=93=BE?= =?UTF-8?q?=E6=8E=A5=E6=97=A0=E6=95=88,=E4=BF=AE=E5=A4=8D=E8=BE=B9?= =?UTF-8?q?=E7=95=8C=E9=97=AE=E9=A2=98,=E4=BF=AE=E5=A4=8D=E8=B7=A8?= =?UTF-8?q?=E7=A9=BA=E9=97=B4=E5=BC=95=E7=94=A8=E5=8F=98=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/layout/tab.vue | 209 ++++++++++++++++++++++++++++++++++++++++++--- src/main.ts | 4 +- src/sql.ts | 59 ++++++++++--- 3 files changed, 246 insertions(+), 26 deletions(-) diff --git a/src/layout/tab.vue b/src/layout/tab.vue index 08d7c77..ef652ef 100644 --- a/src/layout/tab.vue +++ b/src/layout/tab.vue @@ -62,6 +62,14 @@ type XmInfoLike = { reviewedBy?: unknown preparedDate?: unknown projectIndustry?: unknown + preparedCompany?: unknown + overview?: unknown + desc?: unknown +} + +type HtBaseInfoLike = { + quality?: unknown + duration?: unknown } interface ScaleRowLike { @@ -89,12 +97,26 @@ interface ZxFwRowLike { id: string process?: unknown subtotal?: unknown + finalFee?: unknown investScale?: unknown landScale?: unknown workload?: unknown hourly?: unknown } +interface WorkContentRowLike { + id?: unknown + content?: unknown + checked?: unknown + custom?: unknown + serviceGroup?: unknown + isAddTrigger?: unknown +} + +interface WorkContentStateLike { + detailRows?: WorkContentRowLike[] +} + interface ZxFwStorageLike { selectedIds?: string[] selectedCodes?: string[] @@ -175,6 +197,7 @@ interface ExportScaleRow { } interface ExportMethod1Detail { + proNum: number major: number cost: number basicFee: number @@ -191,6 +214,7 @@ interface ExportMethod1Detail { } interface ExportMethod1 { + proAmount: number cost: number basicFee: number basicFee_basic: number @@ -200,6 +224,7 @@ interface ExportMethod1 { } interface ExportMethod2Detail { + proNum: number major: number area: number basicFee: number @@ -216,6 +241,7 @@ interface ExportMethod2Detail { } interface ExportMethod2 { + proAmount: number area: number basicFee: number basicFee_basic: number @@ -259,13 +285,20 @@ interface ExportMethod4 { interface ExportService { id: number fee: number + finalFee: number process: number + tasks: ExportTaskGroup[] method1?: ExportMethod1 method2?: ExportMethod2 method3?: ExportMethod3 method4?: ExportMethod4 } +interface ExportTaskGroup { + serviceid?: number + text: string[] +} + interface ExportServiceCoe { serviceid: number coe: number @@ -284,6 +317,8 @@ interface ExportContract { addtionalFee: number reserveFee: number fee: number + quality: string + duration: string scale: ExportScaleRow[] serviceCoes: ExportServiceCoe[] majorCoes: ExportMajorCoe[] @@ -314,10 +349,11 @@ interface ExportMethod5 { } interface ExportAdditionalDetail { - id: number + id: number | string code?: unknown name: string fee: number + tasks: ExportTaskGroup[] m0?: ExportMethod0 m4?: ExportMethod4 m5?: ExportMethod5 @@ -334,6 +370,7 @@ interface ExportReserve { code?: unknown name: string fee: number + tasks: ExportTaskGroup[] m0?: ExportMethod0 m4?: ExportMethod4 m5?: ExportMethod5 @@ -343,10 +380,13 @@ interface ExportReportPayload { name: string writer: string reviewer: string + company: string date: string industry: number fee: number scaleCost: number + overview: string + desc: string scale: ExportScaleRow[] serviceCoes: ExportServiceCoe[] majorCoes: ExportMajorCoe[] @@ -1032,6 +1072,83 @@ const mapIndustryCodeToExportIndustry = (value: unknown): number => { return 0 } +const parseScaleScopedRowId = (rowId: unknown) => { + const raw = String(rowId || '').trim() + const scopedMatch = /^(\d+)::(.+)$/.exec(raw) + if (scopedMatch) { + return { + proNum: toSafeInteger(scopedMatch[1]) ?? 1, + majorPart: String(scopedMatch[2] || '').trim() + } + } + return { + proNum: 1, + majorPart: raw + } +} + +const toScaleMajorId = (row: ScaleMethodRowLike): number | null => { + const direct = toSafeInteger((row as { majorDictId?: unknown }).majorDictId) + if (direct != null) return direct + const parsed = parseScaleScopedRowId(row.id) + return toSafeInteger(parsed.majorPart) +} + +const toScaleProNum = (row: ScaleMethodRowLike): number => { + const parsed = parseScaleScopedRowId(row.id) + return parsed.proNum > 0 ? parsed.proNum : 1 +} + +const normalizeTaskText = (value: unknown): string => String(value || '').trim() + +const groupWorkContentTasks = ( + rows: WorkContentRowLike[] | undefined, + options?: { forceUngroup?: boolean; serviceLabelToId?: Map } +): ExportTaskGroup[] => { + const source = Array.isArray(rows) ? rows : [] + const selected = source.filter(item => { + if (item && item.isAddTrigger === true) return false + const isCustom = Boolean(item?.custom) + const isChecked = Boolean(item?.checked) + return isCustom || isChecked + }) + if (selected.length === 0) return [] + + const hasGroup = !options?.forceUngroup && selected.some(item => normalizeTaskText(item?.serviceGroup).length > 0) + if (!hasGroup) { + const text = selected + .map(item => normalizeTaskText(item?.content)) + .filter(Boolean) + return text.length > 0 ? [{ text }] : [] + } + + const grouped = new Map() + const orderedGroupKeys: string[] = [] + for (const item of selected) { + const groupName = normalizeTaskText(item?.serviceGroup) + const key = groupName || '__ungrouped__' + if (!grouped.has(key)) { + grouped.set(key, []) + orderedGroupKeys.push(key) + } + const content = normalizeTaskText(item?.content) + if (!content) continue + grouped.get(key)?.push(content) + } + + const byLabel = options?.serviceLabelToId || new Map() + return orderedGroupKeys + .map(groupName => { + const text = grouped.get(groupName) || [] + if (text.length === 0) return null + const entry: ExportTaskGroup = { text } + const resolvedServiceId = byLabel.get(groupName) + if (resolvedServiceId != null) entry.serviceid = resolvedServiceId + return entry + }) + .filter((item): item is ExportTaskGroup => Boolean(item)) +} + const buildProjectServiceCoes = (rows: FactorRowLike[] | undefined): ExportServiceCoe[] => { if (!Array.isArray(rows)) return [] return rows @@ -1084,10 +1201,13 @@ const toExportScaleRows = (rows: ScaleRowLike[] | undefined): ExportScaleRow[] = const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | null => { if (!Array.isArray(rows)) return null let hasTotalValue = false + const proSet = new Set() const det = rows .map(row => { - const major = toSafeInteger(row.id) + const major = toScaleMajorId(row) if (major == null || row.budgetFee == null) return null + const proNum = toScaleProNum(row) + proSet.add(proNum) const cost = toFiniteNumber(row.amount) const basicFee = toFiniteNumber(row.budgetFee) if (basicFee != null) hasTotalValue = true @@ -1102,6 +1222,7 @@ const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | n isNonEmptyString(remark) if (!hasValue) return null return { + proNum, major, cost: cost ?? 0, basicFee: basicFee ?? 0, @@ -1121,6 +1242,7 @@ const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | n if (det.length === 0 || !hasTotalValue) return null return { + proAmount: proSet.size > 0 ? proSet.size : 1, cost: sumNumbers(det.map(item => item.cost)), basicFee: sumNumbers(det.map(item => item.basicFee)), basicFee_basic: sumNumbers(det.map(item => item.basicFee_basic)), @@ -1133,10 +1255,13 @@ const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | n const buildMethod2 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod2 | null => { if (!Array.isArray(rows)) return null let hasTotalValue = false + const proSet = new Set() const det = rows .map(row => { - const major = toSafeInteger(row.id) + const major = toScaleMajorId(row) if (major == null || row.budgetFee == null) return null + const proNum = toScaleProNum(row) + proSet.add(proNum) const area = toFiniteNumber(row.landArea) const basicFee = toFiniteNumber(row.budgetFee) if (basicFee != null) hasTotalValue = true @@ -1151,6 +1276,7 @@ const buildMethod2 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod2 | n isNonEmptyString(remark) if (!hasValue) return null return { + proNum, major, area: area ?? 0, basicFee: basicFee ?? 0, @@ -1170,6 +1296,7 @@ const buildMethod2 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod2 | n if (det.length === 0 || !hasTotalValue) return null return { + proAmount: proSet.size > 0 ? proSet.size : 1, area: sumNumbers(det.map(item => item.area)), basicFee: sumNumbers(det.map(item => item.basicFee)), basicFee_basic: sumNumbers(det.map(item => item.basicFee_basic)), @@ -1353,6 +1480,50 @@ const loadHtFeeMethodsByRow = async (mainStorageKey: string, rowId: string) => { } } +const buildServiceGroupLabelToIdMap = (serviceIds: string[]): Map => { + const map = new Map() + for (const serviceId of serviceIds) { + const item = (serviceList as Record)[serviceId] + if (!item) continue + const id = toSafeInteger(serviceId) + if (id == null) continue + const label = `${String(item.code || '').trim()} ${String(item.name || '').trim()}`.trim() + if (!label) continue + map.set(label, id) + } + return map +} + +const buildServiceTasks = async ( + contractId: string, + serviceId: string, + serviceLabelToId: Map +): Promise => { + const taskState = await zxFwPricingStore.loadKeyState(`work-content-${contractId}-${serviceId}`) + return groupWorkContentTasks(taskState?.detailRows, { serviceLabelToId }) +} + +const buildAdditionalRowTasks = async (contractId: string, rowId: string): Promise => { + const taskStorageKey = `work-content-htExtraFee-${contractId}-additional-work-${rowId}` + const taskState = await zxFwPricingStore.loadKeyState(taskStorageKey) + return groupWorkContentTasks(taskState?.detailRows, { forceUngroup: true }) +} + +const buildServiceFinalFee = ( + row: ZxFwRowLike | null | undefined, + method1: ExportMethod1 | null, + method2: ExportMethod2 | null, + method3: ExportMethod3 | null, + method4: ExportMethod4 | null +) => { + const finalFee = toFiniteNumber(row?.finalFee) + if (finalFee != null) return finalFee + const subtotal = toFiniteNumber(row?.subtotal) + if (subtotal != null) return subtotal + const methodSum = sumNumbers([method1?.fee, method2?.fee, method3?.fee, method4?.fee]) + return methodSum +} + const buildAdditionalExport = async (contractId: string): Promise => { @@ -1363,15 +1534,17 @@ const buildAdditionalExport = async (contractId: string): Promise { + rows.map(async row => { const methodPayload = await loadHtFeeMethodsByRow(storageKey, row.id) if (!methodPayload) return null + const tasks = await buildAdditionalRowTasks(contractId, row.id) const item: ExportAdditionalDetail = { - id: index, + id: row.id, code: { richText: [{ font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: 'C' }, { font: { charset: 134, color: { theme: 1 }, italic: true, name: 'Calibri', size: 10, vertAlign: 'subscript' }, text: 'F' }] }, name: row.name, - fee: methodPayload.fee + fee: methodPayload.fee, + tasks } if (methodPayload.m0) item.m0 = methodPayload.m0 if (methodPayload.m4) item.m4 = methodPayload.m4 @@ -1402,7 +1575,8 @@ const buildReserveExport = async (contractId: string): Promise => { const projectName = isNonEmptyString(projectInfo.projectName) ? projectInfo.projectName.trim() : '造价项目' const writer = isNonEmptyString(projectInfo.preparedBy) ? projectInfo.preparedBy.trim() : '' const reviewer = isNonEmptyString(projectInfo.reviewedBy) ? projectInfo.reviewedBy.trim() : '' + const company = isNonEmptyString(projectInfo.preparedCompany) ? projectInfo.preparedCompany.trim() : '' const date = isNonEmptyString(projectInfo.preparedDate) ? projectInfo.preparedDate.trim() : '' const industry = mapIndustryCodeToExportIndustry(projectInfo.projectIndustry) + const overview = isNonEmptyString(projectInfo.overview) ? projectInfo.overview.trim() : '' + const desc = isNonEmptyString(projectInfo.desc) ? projectInfo.desc.trim() : '' const contractCards = (Array.isArray(contractCardsRaw) ? contractCardsRaw : []) .filter(item => item && typeof item.id === 'string') @@ -1449,11 +1626,12 @@ const buildExportReportPayload = async (): Promise => { const contract = contractCards[index] const contractId = contract.id await zxFwPricingStore.loadContract(contractId) - const [htInfoRaw, zxFwRaw, htConsultCategoryFactorRaw, htMajorFactorRaw] = await Promise.all([ + const [htInfoRaw, zxFwRaw, htConsultCategoryFactorRaw, htMajorFactorRaw, htBaseInfoRaw] = await Promise.all([ kvStore.getItem>(`ht-info-v3-${contractId}`), kvStore.getItem(`zxFW-${contractId}`), kvStore.getItem>(`ht-consult-category-factor-v1-${contractId}`), - kvStore.getItem>(`ht-major-factor-v1-${contractId}`) + kvStore.getItem>(`ht-major-factor-v1-${contractId}`), + kvStore.getItem(`ht-base-info-${contractId}`) ]) const contractState = zxFwPricingStore.getContractState(contractId) @@ -1462,6 +1640,7 @@ const buildExportReportPayload = async (): Promise => { id: String(row.id || ''), process: row.process, subtotal: row.subtotal, + finalFee: row.finalFee, investScale: row.investScale, landScale: row.landScale, workload: row.workload, @@ -1494,6 +1673,7 @@ const buildExportReportPayload = async (): Promise => { const serviceIdTexts = sortServiceIdsByDict( (selectedIds.length > 0 ? selectedIds : fallbackServiceIds).filter(hasServiceId) ) + const serviceLabelToId = buildServiceGroupLabelToIdMap(serviceIdTexts) const services = ( await Promise.all( @@ -1518,11 +1698,15 @@ const buildExportReportPayload = async (): Promise => { const method3 = buildMethod3(method3Raw?.detailRows) const method4 = buildMethod4(method4Raw?.detailRows) const fee = buildServiceFee(sourceRow, method1, method2, method3, method4) + const finalFee = buildServiceFinalFee(sourceRow, method1, method2, method3, method4) + const tasks = await buildServiceTasks(contractId, serviceIdText, serviceLabelToId) const process = Number(sourceRow?.process) === 1 ? 1 : 0 const service: ExportService = { id: serviceId, process, - fee + fee, + finalFee, + tasks } if (method1) service.method1 = method1 if (method2) service.method2 = method2 @@ -1564,6 +1748,8 @@ const buildExportReportPayload = async (): Promise => { addtionalFee, reserveFee, fee: contractFee, + quality: isNonEmptyString(htBaseInfoRaw?.quality) ? htBaseInfoRaw.quality.trim() : '', + duration: isNonEmptyString(htBaseInfoRaw?.duration) ? htBaseInfoRaw.duration.trim() : '', scale: contractScale, serviceCoes: contractServiceCoesRaw, majorCoes: contractMajorCoesRaw, @@ -1577,10 +1763,13 @@ const buildExportReportPayload = async (): Promise => { name: projectName, writer, reviewer, + company, date, industry, fee: sumNumbers(contracts.map(item => item.fee)), scaleCost: projectScaleCost, + overview, + desc, scale: projectScale, serviceCoes: projectServiceCoes, majorCoes: projectMajorCoes, diff --git a/src/main.ts b/src/main.ts index 1da7ae2..df855fb 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,7 +9,7 @@ import { PinnedRowModule, RowAutoHeightModule, TextEditorModule, - TooltipModule, + TooltipModule,ClientSideRowModelApiModule , UndoRedoEditModule,RenderApiModule ,ColumnApiModule ,CellSpanModule ,RowStyleModule ,RowSelectionModule } from 'ag-grid-community' @@ -39,7 +39,7 @@ const AG_GRID_MODULES = [ RowAutoHeightModule,ContextMenuModule, LargeTextEditorModule, UndoRedoEditModule, - CellStyleModule, + CellStyleModule,ClientSideRowModelApiModule , PinnedRowModule,RenderApiModule ,ColumnApiModule , TooltipModule, TreeDataModule, diff --git a/src/sql.ts b/src/sql.ts index 82dce36..bcffd9d 100644 --- a/src/sql.ts +++ b/src/sql.ts @@ -11,6 +11,34 @@ const toFiniteNumber = (value: unknown) => { const num = Number(value) return Number.isFinite(num) ? num : 0 } + +// 兼容导出 tasks 对象结构:[{ text: [] }, { serviceid, text: [] }] +const normalizeTaskTexts = (tasks: unknown): string[] => { + if (!Array.isArray(tasks)) return []; + const result: string[] = []; + const seen = new Set(); + const pushText = (value: unknown) => { + const text = String(value ?? '').trim(); + if (!text) return; + if (seen.has(text)) return; + seen.add(text); + result.push(text); + }; + tasks.forEach(item => { + if (typeof item === 'string') { + pushText(item); + return; + } + if (!item || typeof item !== 'object') return; + const textField = item.text; + if (Array.isArray(textField)) { + textField.forEach(pushText); + return; + } + pushText(textField); + }); + return result; +} export type WorkType = '基本工作' | '可选工作' | '日常顾问' | '专项顾问' | '附加工作' | '自定义' export const TYPE_LABEL_MAP: Record = { @@ -797,7 +825,7 @@ export async function exportFile(fileName: string, data: any | (() => Promise Promise { + const serviceTaskTexts = normalizeTaskTexts(si.tasks); + if (serviceTaskTexts.length) { + serviceTaskTexts.forEach((sti, stindex) => { let stiTextArr = paragraphLineBreakFor1112(` ${stindex + 1})${sti}。`, ctx); stiTextArr.forEach(ti => { cusInsertRowFunc(descRowNum, [descSheet.getRow(descRowNum - 1)], descSheet, (targetRow) => { @@ -2244,8 +2272,9 @@ async function generateTemplate(data) { targetRow.getCell(1).value = ` (${ciTastNum})${ai.id == 1 ? '负责协调工作,具体工作内容包括:' : '其他附加工作,具体工作内容包括:'}`; ciTastNum++; }); - if (ai.tasks?.length) { - ai.tasks.forEach((ati, atindex) => { + const additionalTaskTexts = normalizeTaskTexts(ai.tasks); + if (additionalTaskTexts.length) { + additionalTaskTexts.forEach((ati, atindex) => { let atiTextArr = paragraphLineBreakFor1112(` ${atindex + 1})${ati}。`, ctx); atiTextArr.forEach(ti => { cusInsertRowFunc(descRowNum, [descSheet.getRow(descRowNum - 1)], descSheet, (targetRow) => { @@ -2287,7 +2316,6 @@ async function generateTemplate(data) { let descRowNum1 = descRowNum + 1; let descRowNum2 = descRowNum + 2; let descRowNum3 = descRowNum + 2; - console.log( data.contracts) data.contracts.forEach((ci, cindex) => { descRowNum3 = descRowNum3 - descRowNum2; descRowNum2 = descRowNum2 - descRowNum1 - 1; @@ -2369,8 +2397,9 @@ async function generateTemplate(data) { ciTastNum++; }); }); - if (si.tasks?.length) { - si.tasks.forEach((sti, stindex) => { + const serviceTaskTexts = normalizeTaskTexts(si.tasks); + if (serviceTaskTexts.length) { + serviceTaskTexts.forEach((sti, stindex) => { let stiTextArr = paragraphLineBreakFor1112(` ${stindex + 1})${sti}。`, ctx); stiTextArr.forEach(ti => { cusInsertRowFunc(descRowNum3, [descSheet.getRow(descRowNum3 - 1)], descSheet, (targetRow) => { @@ -2393,8 +2422,9 @@ async function generateTemplate(data) { targetRow.getCell(1).value = ` (${ciTastNum})${ai.id == 1 ? '负责协调工作,具体工作内容包括:' : '其他附加工作,具体工作内容包括:'}`; ciTastNum++; }); - if (ai.tasks?.length) { - ai.tasks.forEach((ati, atindex) => { + const additionalTaskTexts = normalizeTaskTexts(ai.tasks); + if (additionalTaskTexts.length) { + additionalTaskTexts.forEach((ati, atindex) => { let atiTextArr = paragraphLineBreakFor1112(` ${atindex + 1})${ati}。`, ctx); atiTextArr.forEach(ti => { cusInsertRowFunc(descRowNum3, [descSheet.getRow(descRowNum3 - 1)], descSheet, (targetRow) => { @@ -2490,9 +2520,10 @@ async function generateTemplate(data) { }); } } - if (data.desc) { + if (String(data.desc || '').trim()) { descRowNum++; - let otherDesc = paragraphLineBreakFor1112(` ${data.desc}${/。$/.test(ci.duration) ? '' : '。'}`, ctx); + const otherDescText = String(data.desc || '').trim(); + let otherDesc = paragraphLineBreakFor1112(` ${otherDescText}${/。$/.test(otherDescText) ? '' : '。'}`, ctx); descSheet.getRow(descRowNum).getCell(1).value = otherDesc[0]; descRowNum++; if (otherDesc.length > 1) { @@ -2742,4 +2773,4 @@ function ArabicToChinese(Arabic_numerals) { return chineseNumerals[parseInt(match)]; }); return mixNumerals; -} \ No newline at end of file +}