修复大量问题

This commit is contained in:
wintsa 2026-03-19 18:34:46 +08:00
parent 8e5cb49da5
commit 59bab33e9b
10 changed files with 270 additions and 114 deletions

View File

@ -196,7 +196,7 @@ let data1 = {
], ],
}, },
tasks: [{ serviceid: 0, text: ['abc', 'efg'] }, tasks: [{ serviceid: 0, text: ['abc', 'efg'] },
{ serviceid: 2,text: ['abc', 'efg'] } //tasks不分组的时候传单对象[{text: ['abc', 'efg']}],分组的时候传分组的serviceid { serviceid: 2,text: ['abc', 'efg'] } //tasks不分组的时候传单对象[{text: ['abc', 'efg']}],分组的时候传分组的服务id[{ serviceid: 0, text: ['abc', 'efg'] },...]
],// 工作内容 ],// 工作内容
}, },
], ],

View File

@ -10,6 +10,7 @@ import {
} }
const DEFAULT_QUALITY = '造价咨询服务的综合评价应达到"较好"或综合评分90分' const DEFAULT_QUALITY = '造价咨询服务的综合评价应达到"较好"或综合评分90分'
const DEFAULT_DURATION = ''
const props = const props =
defineProps<{ defineProps<{
@ -43,13 +44,21 @@ import {
const loadForm = async () => { const loadForm = async () => {
const data = await const data = await
zxFwPricingStore.loadKeyState<HtBaseInfoState>(storageKey()) zxFwPricingStore.loadKeyState<HtBaseInfoState>(storageKey())
const hasStoredValue = Boolean(
data &&
(Object.prototype.hasOwnProperty.call(data, 'quality') || Object.prototype.hasOwnProperty.call(data, 'duration'))
)
quality.value = typeof data?.quality === 'string' && quality.value = typeof data?.quality === 'string' &&
data.quality ? data.quality : DEFAULT_QUALITY data.quality ? data.quality : DEFAULT_QUALITY
duration.value = typeof data?.duration === 'string' ? data.duration : duration.value = typeof data?.duration === 'string'
'' ? data.duration
: (hasStoredValue ? '' : DEFAULT_DURATION)
const payload: HtBaseInfoState = { quality: quality.value, duration: duration.value } const payload: HtBaseInfoState = { quality: quality.value, duration: duration.value }
lastSavedSnapshot.value = JSON.stringify(payload) lastSavedSnapshot.value = JSON.stringify(payload)
if (!hasStoredValue) {
saveForm(true)
}
} }
watch([quality, duration], () => { saveForm() watch([quality, duration], () => { saveForm()

View File

@ -6,8 +6,8 @@
:subtitle="`合同段ID${contractId}`" :subtitle="`合同段ID${contractId}`"
:meta-text="`合同段预算金额:${formatBudgetAmount(contractBudget)}`" :meta-text="`合同段预算金额:${formatBudgetAmount(contractBudget)}`"
:copy-text="contractId" :copy-text="contractId"
:storage-key="`project-active-cat-${contractId}`" :storage-key="typeLineStorageKey"
default-category="info" default-category="base-info"
:categories="xmCategories" :categories="xmCategories"
/> />
</template> </template>
@ -61,6 +61,7 @@ interface QuantityMethodStateLike {
} }
const contractBudget = ref<number | null>(null) const contractBudget = ref<number | null>(null)
const typeLineStorageKey = computed(() => `project-active-cat-${props.contractId}`)
let budgetRefreshTimer: ReturnType<typeof setTimeout> | null = null let budgetRefreshTimer: ReturnType<typeof setTimeout> | null = null
const toFiniteNumber = (value: unknown): number | null => { const toFiniteNumber = (value: unknown): number | null => {

View File

@ -356,7 +356,7 @@ const mergeWithStoredRows = (rowsFromDb: unknown): FeeMethodRow[] => {
return fixedNames.value.map((item, index) => { return fixedNames.value.map((item, index) => {
const fromDb = byName.get(item.name) const fromDb = byName.get(item.name)
return { return {
id: item?.id || `fee-method-fixed-${index}`, id: String(item?.id || `fee-method-fixed-${index}`),
name:item.name, name:item.name,
rateFee: fromDb?.rateFee ?? null, rateFee: fromDb?.rateFee ?? null,
hourlyFee: fromDb?.hourlyFee ?? null, hourlyFee: fromDb?.hourlyFee ?? null,
@ -424,7 +424,6 @@ const clearRow = async (id: string) => {
const editRow = (id: string) => { const editRow = (id: string) => {
const row = detailRows.value.find(item => item.id === id) const row = detailRows.value.find(item => item.id === id)
if (!row) return if (!row) return
console.log(id)
tabStore.openTab({ tabStore.openTab({
id: `ht-fee-edit-${props.storageKey}-${id}`, id: `ht-fee-edit-${props.storageKey}-${id}`,
title: `费用编辑-${row.name || '未命名'}`, title: `费用编辑-${row.name || '未命名'}`,
@ -454,7 +453,7 @@ const ActionCellRenderer = defineComponent({
const onActionClick = (action: 'edit' | 'clear') => (event: MouseEvent) => { const onActionClick = (action: 'edit' | 'clear') => (event: MouseEvent) => {
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
const rowId = props.params.data?.id const rowId = String(props.params.data?.id || '').trim()
if (!rowId) return if (!rowId) return
if (action === 'edit') { if (action === 'edit') {
props.params.context?.onActionEdit?.(rowId) props.params.context?.onActionEdit?.(rowId)

View File

@ -34,6 +34,7 @@ interface WorkContentRow {
content: string content: string
type: WorkType type: WorkType
serviceGroup?: string serviceGroup?: string
serviceid?: number | null
remark: string remark: string
checked: boolean checked: boolean
custom: boolean custom: boolean
@ -93,6 +94,12 @@ const syncGroupedRowsRender = async () => {
}, 16) }, 16)
} }
const toServiceId = (value: unknown): number | null => {
const parsed = Number(value)
if (!Number.isSafeInteger(parsed)) return null
return parsed
}
const loadProjectIndustryId = async () => { const loadProjectIndustryId = async () => {
@ -193,6 +200,7 @@ const buildDefaultRowsFromDict = async (): Promise<WorkContentRow[]> => {
content, content,
type: typeLabel, type: typeLabel,
serviceGroup, serviceGroup,
serviceid: toServiceId(entry.serviceid),
remark: '', remark: '',
checked: false, checked: false,
custom: false, custom: false,
@ -247,9 +255,27 @@ const loadFromStore = async () => {
const persistedRows = state.detailRows.map(item => ({ const persistedRows = state.detailRows.map(item => ({
...item, ...item,
type: item.custom ? '自定义' : (item.type || '基本工作'), type: item.custom ? '自定义' : (item.type || '基本工作'),
serviceid: toServiceId(item.serviceid),
path: Array.isArray(item.path) && item.path.length ? item.path : ['自定义', item.content || '未命名'] path: Array.isArray(item.path) && item.path.length ? item.path : ['自定义', item.content || '未命名']
})) as WorkContentRow[] })) as WorkContentRow[]
const defaultGroupServiceIdMap = new Map<string, number>()
for (const row of defaultRows) {
const groupName = String(row.serviceGroup || '').trim()
const serviceid = toServiceId(row.serviceid)
if (!groupName || serviceid == null) continue
defaultGroupServiceIdMap.set(groupName, serviceid)
}
for (const row of persistedRows) {
if (row.serviceid != null) continue
const groupName = String(row.serviceGroup || '').trim()
if (!groupName) continue
const fallbackServiceId = defaultGroupServiceIdMap.get(groupName)
if (fallbackServiceId != null) {
row.serviceid = fallbackServiceId
}
}
// / // /
if (defaultRows.length > 0) { if (defaultRows.length > 0) {
const persistedCustomRows = persistedRows.filter(item => item.custom) const persistedCustomRows = persistedRows.filter(item => item.custom)
@ -462,6 +488,7 @@ const createAddTriggerRow = (groupName?: string): WorkContentRow => {
content: '点击添加自定义内容', content: '点击添加自定义内容',
type: '自定义' as WorkType, type: '自定义' as WorkType,
serviceGroup: groupName || '', serviceGroup: groupName || '',
serviceid: null,
remark: '', remark: '',
checked: false, checked: false,
custom: false, custom: false,
@ -513,11 +540,19 @@ const addCustomRow = (groupName?: string) => {
const finalGroupName = isWholeProcessGroupedMode.value const finalGroupName = isWholeProcessGroupedMode.value
? String(groupName || groupedServiceGroups.value[0] || '').trim() ? String(groupName || groupedServiceGroups.value[0] || '').trim()
: '' : ''
const finalServiceId = isWholeProcessGroupedMode.value
? (() => {
const pureRows = getPersistableRows(rowData.value)
const hit = pureRows.find(item => String(item.serviceGroup || '').trim() === finalGroupName && item.serviceid != null)
return hit?.serviceid ?? null
})()
: null
const nextRow: WorkContentRow = { const nextRow: WorkContentRow = {
id: `custom-${ts}`, id: `custom-${ts}`,
content: '', content: '',
type: '自定义' as WorkType, type: '自定义' as WorkType,
serviceGroup: finalGroupName, serviceGroup: finalGroupName,
serviceid: finalServiceId,
remark: '', remark: '',
checked: false, checked: false,
custom: true, custom: true,

View File

@ -26,19 +26,20 @@ interface TypeLineCategoryItem {
const props = defineProps<{ const props = defineProps<{
sourceTitle?: string sourceTitle?: string
storageKey: string storageKey: string
rowId: string rowId: string | number
rowName?: string rowName?: string
contractId?: string contractId?: string
contractName?: string contractName?: string
}>() }>()
const sourceTitleText = computed(() => props.sourceTitle || '费用明细') const sourceTitleText = computed(() => props.sourceTitle || '费用明细')
const rowNameText = computed(() => props.rowName || '未命名') const rowNameText = computed(() => props.rowName || '未命名')
const rowIdText = computed(() => String(props.rowId || '').trim())
const contractIdText = computed(() => String(props.contractId || '').trim()) const contractIdText = computed(() => String(props.contractId || '').trim())
const contractNameText = computed(() => String(props.contractName || '').trim() || contractIdText.value || '-') const contractNameText = computed(() => String(props.contractName || '').trim() || contractIdText.value || '-')
const titleText = computed(() => `合同段:${contractNameText.value} · ${rowNameText.value || sourceTitleText.value}`) const titleText = computed(() => `合同段:${contractNameText.value} · ${rowNameText.value || sourceTitleText.value}`)
const activeTypeStorageKey = computed(() => `ht-fee-type-active-cat-${props.storageKey}-${props.rowId}`) const activeTypeStorageKey = computed(() => `ht-fee-type-active-cat-${props.storageKey}-${rowIdText.value}`)
const buildMethodStorageKey = (method: 'rate-fee' | 'hourly-fee' | 'quantity-unit-price-fee') => const buildMethodStorageKey = (method: 'rate-fee' | 'hourly-fee' | 'quantity-unit-price-fee') =>
`${props.storageKey}-${props.rowId}-${method}` `${props.storageKey}-${rowIdText.value}-${method}`
const quantityUnitPricePane = markRaw( const quantityUnitPricePane = markRaw(
defineComponent({ defineComponent({
@ -50,7 +51,7 @@ const quantityUnitPricePane = markRaw(
title: '数量单价', title: '数量单价',
storageKey: quantityStorageKey.value, storageKey: quantityStorageKey.value,
htMainStorageKey: props.storageKey, htMainStorageKey: props.storageKey,
htRowId: props.rowId, htRowId: rowIdText.value,
htMethodType: 'quantity-unit-price-fee' htMethodType: 'quantity-unit-price-fee'
}) })
} }
@ -67,7 +68,7 @@ const rateFeePane = markRaw(
storageKey: rateStorageKey.value, storageKey: rateStorageKey.value,
contractId: props.contractId, contractId: props.contractId,
htMainStorageKey: props.storageKey, htMainStorageKey: props.storageKey,
htRowId: props.rowId, htRowId: rowIdText.value,
htMethodType: 'rate-fee' htMethodType: 'rate-fee'
}) })
} }
@ -84,7 +85,7 @@ const hourlyFeePane = markRaw(
title: '工时法明细', title: '工时法明细',
storageKey: hourlyStorageKey.value, storageKey: hourlyStorageKey.value,
htMainStorageKey: props.storageKey, htMainStorageKey: props.storageKey,
htRowId: props.rowId, htRowId: rowIdText.value,
htMethodType: 'hourly-fee' htMethodType: 'hourly-fee'
}) })
} }
@ -109,7 +110,7 @@ const workContentPane = markRaw(
}) })
return () => h(AsyncWorkContentGrid, { return () => h(AsyncWorkContentGrid, {
title: '工作内容', title: '工作内容',
storageKey: `work-content-${props.storageKey}-${props.rowId}`, storageKey: `work-content-${props.storageKey}-${rowIdText.value}`,
dictMode: 'additional' dictMode: 'additional'
}) })
} }

View File

@ -29,6 +29,7 @@ import {
import { decodeZwArchive, encodeZwArchive, ZW_FILE_EXTENSION } from '@/lib/zwArchive' import { decodeZwArchive, encodeZwArchive, ZW_FILE_EXTENSION } from '@/lib/zwArchive'
import { PROJECT_TAB_ID, QUICK_TAB_ID, readWorkspaceMode, writeWorkspaceMode } from '@/lib/workspace' import { PROJECT_TAB_ID, QUICK_TAB_ID, readWorkspaceMode, writeWorkspaceMode } from '@/lib/workspace'
import { addNumbers, roundTo } from '@/lib/decimal' import { addNumbers, roundTo } from '@/lib/decimal'
import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee'
import { exportFile, serviceList } from '@/sql' import { exportFile, serviceList } from '@/sql'
interface DataEntry { interface DataEntry {
@ -110,6 +111,7 @@ interface WorkContentRowLike {
checked?: unknown checked?: unknown
custom?: unknown custom?: unknown
serviceGroup?: unknown serviceGroup?: unknown
serviceid?: unknown
isAddTrigger?: unknown isAddTrigger?: unknown
} }
@ -544,14 +546,11 @@ const finishReportExportProgress = (success: boolean, text: string, blobUrl?: st
reportExportStatus.value = success ? 'success' : 'error' reportExportStatus.value = success ? 'success' : 'error'
reportExportProgress.value = 100 reportExportProgress.value = 100
reportExportText.value = text reportExportText.value = text
console.log(blobUrl)
reportExportBlobUrl.value = success && blobUrl ? blobUrl : null reportExportBlobUrl.value = success && blobUrl ? blobUrl : null
reportExportToastOpen.value = true reportExportToastOpen.value = true
if (!success || !blobUrl) {
reportExportToastTimer = setTimeout(() => { reportExportToastTimer = setTimeout(() => {
reportExportToastOpen.value = false reportExportToastOpen.value = false
}, success ? 1200 : 1800) }, success ? 2000 : 1800)
}
} }
const openExportedReport = () => { const openExportedReport = () => {
@ -1037,6 +1036,7 @@ const sumNumbers = (values: Array<number | null | undefined>): number =>
(sum, value) => sum + (typeof value === 'number' && Number.isFinite(value) ? value : 0), (sum, value) => sum + (typeof value === 'number' && Number.isFinite(value) ? value : 0),
0 0
) )
const toMoney = (value: unknown): number => roundTo(toFiniteNumber(value) ?? 0, 2)
const isNonEmptyString = (value: unknown): value is string => const isNonEmptyString = (value: unknown): value is string =>
typeof value === 'string' && value.trim().length > 0 typeof value === 'string' && value.trim().length > 0
@ -1100,10 +1100,42 @@ const toScaleProNum = (row: ScaleMethodRowLike): number => {
} }
const normalizeTaskText = (value: unknown): string => String(value || '').trim() const normalizeTaskText = (value: unknown): string => String(value || '').trim()
const resolveTaskRowServiceId = (row: WorkContentRowLike): number | null =>
toSafeInteger((row as { serviceid?: unknown })?.serviceid)
const resolveScaleMethodFee = (row: ScaleMethodRowLike, mode: 'cost' | 'area') => {
const scaleValue = mode === 'cost' ? toFiniteNumber(row.amount) : toFiniteNumber(row.landArea)
const benchmarkSplit = getBenchmarkBudgetSplitByScale(scaleValue, mode)
const computedSplit = benchmarkSplit
? getScaleBudgetFeeSplit({
benchmarkBudgetBasic: benchmarkSplit.basic,
benchmarkBudgetOptional: benchmarkSplit.optional,
majorFactor: row.majorFactor,
consultCategoryFactor: row.consultCategoryFactor,
workStageFactor: row.workStageFactor,
workRatio: row.workRatio
})
: null
const basicFee = toFiniteNumber(row.budgetFee) ?? computedSplit?.total ?? null
const basicFeeBasic = toFiniteNumber(row.budgetFeeBasic) ?? computedSplit?.basic ?? null
const basicFeeOptional = toFiniteNumber(row.budgetFeeOptional) ?? computedSplit?.optional ?? null
const basicFormula = typeof row.basicFormula === 'string' && row.basicFormula.trim()
? row.basicFormula
: (benchmarkSplit?.basicFormula ?? '')
const optionalFormula = typeof row.optionalFormula === 'string' && row.optionalFormula.trim()
? row.optionalFormula
: (benchmarkSplit?.optionalFormula ?? '')
return {
basicFee,
basicFeeBasic,
basicFeeOptional,
basicFormula,
optionalFormula
}
}
const groupWorkContentTasks = ( const groupWorkContentTasks = (
rows: WorkContentRowLike[] | undefined, rows: WorkContentRowLike[] | undefined,
options?: { forceUngroup?: boolean; serviceLabelToId?: Map<string, number> } options?: { forceUngroup?: boolean }
): ExportTaskGroup[] => { ): ExportTaskGroup[] => {
const source = Array.isArray(rows) ? rows : [] const source = Array.isArray(rows) ? rows : []
const selected = source.filter(item => { const selected = source.filter(item => {
@ -1114,7 +1146,7 @@ const groupWorkContentTasks = (
}) })
if (selected.length === 0) return [] if (selected.length === 0) return []
const hasGroup = !options?.forceUngroup && selected.some(item => normalizeTaskText(item?.serviceGroup).length > 0) const hasGroup = !options?.forceUngroup && selected.some(item => resolveTaskRowServiceId(item) != null)
if (!hasGroup) { if (!hasGroup) {
const text = selected const text = selected
.map(item => normalizeTaskText(item?.content)) .map(item => normalizeTaskText(item?.content))
@ -1122,31 +1154,35 @@ const groupWorkContentTasks = (
return text.length > 0 ? [{ text }] : [] return text.length > 0 ? [{ text }] : []
} }
const grouped = new Map<string, string[]>() const grouped = new Map<number, string[]>()
const orderedGroupKeys: string[] = [] const orderedServiceIds: number[] = []
const ungroupedText: string[] = []
for (const item of selected) { 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) const content = normalizeTaskText(item?.content)
if (!content) continue if (!content) continue
grouped.get(key)?.push(content) const serviceid = resolveTaskRowServiceId(item)
if (serviceid == null) {
ungroupedText.push(content)
continue
}
if (!grouped.has(serviceid)) {
grouped.set(serviceid, [])
orderedServiceIds.push(serviceid)
}
grouped.get(serviceid)?.push(content)
} }
const byLabel = options?.serviceLabelToId || new Map<string, number>() const groupedTasks: ExportTaskGroup[] = []
return orderedGroupKeys for (const serviceid of orderedServiceIds) {
.map(groupName => { const text = grouped.get(serviceid) || []
const text = grouped.get(groupName) || [] if (text.length === 0) continue
if (text.length === 0) return null groupedTasks.push({ serviceid, text })
const entry: ExportTaskGroup = { text } }
const resolvedServiceId = byLabel.get(groupName)
if (resolvedServiceId != null) entry.serviceid = resolvedServiceId if (ungroupedText.length > 0) {
return entry groupedTasks.push({ text: ungroupedText })
}) }
.filter((item): item is ExportTaskGroup => Boolean(item)) return groupedTasks
} }
const buildProjectServiceCoes = (rows: FactorRowLike[] | undefined): ExportServiceCoe[] => { const buildProjectServiceCoes = (rows: FactorRowLike[] | undefined): ExportServiceCoe[] => {
@ -1205,14 +1241,15 @@ const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | n
const det = rows const det = rows
.map(row => { .map(row => {
const major = toScaleMajorId(row) const major = toScaleMajorId(row)
if (major == null || row.budgetFee == null) return null if (major == null) return null
const proNum = toScaleProNum(row) const proNum = toScaleProNum(row)
proSet.add(proNum) proSet.add(proNum)
const cost = toFiniteNumber(row.amount) const cost = toFiniteNumber(row.amount)
const basicFee = toFiniteNumber(row.budgetFee) const feeResolved = resolveScaleMethodFee(row, 'cost')
const basicFee = feeResolved.basicFee
if (basicFee != null) hasTotalValue = true if (basicFee != null) hasTotalValue = true
const basicFeeBasic = toFiniteNumber(row.budgetFeeBasic) const basicFeeBasic = feeResolved.basicFeeBasic
const basicFeeOptional = toFiniteNumber(row.budgetFeeOptional) const basicFeeOptional = feeResolved.basicFeeOptional
const remark = typeof row.remark === 'string' ? row.remark : '' const remark = typeof row.remark === 'string' ? row.remark : ''
const hasValue = const hasValue =
cost != null || cost != null ||
@ -1225,16 +1262,16 @@ const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | n
proNum, proNum,
major, major,
cost: cost ?? 0, cost: cost ?? 0,
basicFee: basicFee ?? 0, basicFee: toMoney(basicFee),
basicFormula: typeof row.basicFormula === 'string' ? row.basicFormula : '', basicFormula: feeResolved.basicFormula,
basicFee_basic: basicFeeBasic ?? 0, basicFee_basic: toMoney(basicFeeBasic),
optionalFormula: typeof row.optionalFormula === 'string' ? row.optionalFormula : '', optionalFormula: feeResolved.optionalFormula,
basicFee_optional: basicFeeOptional ?? 0, basicFee_optional: toMoney(basicFeeOptional),
serviceCoe: toFiniteNumberOrZero(row.consultCategoryFactor), serviceCoe: toFiniteNumberOrZero(row.consultCategoryFactor),
majorCoe: toFiniteNumberOrZero(row.majorFactor), majorCoe: toFiniteNumberOrZero(row.majorFactor),
processCoe: toFiniteNumber(row.workStageFactor) ?? 1, processCoe: toFiniteNumber(row.workStageFactor) ?? 1,
proportion: toFiniteNumber(row.workRatio) ?? 1, proportion: toFiniteNumber(row.workRatio) ?? 1,
fee: basicFee ?? 0, fee: toMoney(basicFee),
remark remark
} }
}) })
@ -1244,10 +1281,10 @@ const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | n
return { return {
proAmount: proSet.size > 0 ? proSet.size : 1, proAmount: proSet.size > 0 ? proSet.size : 1,
cost: sumNumbers(det.map(item => item.cost)), cost: sumNumbers(det.map(item => item.cost)),
basicFee: sumNumbers(det.map(item => item.basicFee)), basicFee: toMoney(sumNumbers(det.map(item => item.basicFee))),
basicFee_basic: sumNumbers(det.map(item => item.basicFee_basic)), basicFee_basic: toMoney(sumNumbers(det.map(item => item.basicFee_basic))),
basicFee_optional: sumNumbers(det.map(item => item.basicFee_optional)), basicFee_optional: toMoney(sumNumbers(det.map(item => item.basicFee_optional))),
fee: sumNumbers(det.map(item => item.fee)), fee: toMoney(sumNumbers(det.map(item => item.fee))),
det det
} }
} }
@ -1259,14 +1296,15 @@ const buildMethod2 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod2 | n
const det = rows const det = rows
.map(row => { .map(row => {
const major = toScaleMajorId(row) const major = toScaleMajorId(row)
if (major == null || row.budgetFee == null) return null if (major == null) return null
const proNum = toScaleProNum(row) const proNum = toScaleProNum(row)
proSet.add(proNum) proSet.add(proNum)
const area = toFiniteNumber(row.landArea) const area = toFiniteNumber(row.landArea)
const basicFee = toFiniteNumber(row.budgetFee) const feeResolved = resolveScaleMethodFee(row, 'area')
const basicFee = feeResolved.basicFee
if (basicFee != null) hasTotalValue = true if (basicFee != null) hasTotalValue = true
const basicFeeBasic = toFiniteNumber(row.budgetFeeBasic) const basicFeeBasic = feeResolved.basicFeeBasic
const basicFeeOptional = toFiniteNumber(row.budgetFeeOptional) const basicFeeOptional = feeResolved.basicFeeOptional
const remark = typeof row.remark === 'string' ? row.remark : '' const remark = typeof row.remark === 'string' ? row.remark : ''
const hasValue = const hasValue =
area != null || area != null ||
@ -1279,16 +1317,16 @@ const buildMethod2 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod2 | n
proNum, proNum,
major, major,
area: area ?? 0, area: area ?? 0,
basicFee: basicFee ?? 0, basicFee: toMoney(basicFee),
basicFormula: typeof row.basicFormula === 'string' ? row.basicFormula : '', basicFormula: feeResolved.basicFormula,
basicFee_basic: basicFeeBasic ?? 0, basicFee_basic: toMoney(basicFeeBasic),
optionalFormula: typeof row.optionalFormula === 'string' ? row.optionalFormula : '', optionalFormula: feeResolved.optionalFormula,
basicFee_optional: basicFeeOptional ?? 0, basicFee_optional: toMoney(basicFeeOptional),
serviceCoe: toFiniteNumberOrZero(row.consultCategoryFactor), serviceCoe: toFiniteNumberOrZero(row.consultCategoryFactor),
majorCoe: toFiniteNumberOrZero(row.majorFactor), majorCoe: toFiniteNumberOrZero(row.majorFactor),
processCoe: toFiniteNumber(row.workStageFactor) ?? 1, processCoe: toFiniteNumber(row.workStageFactor) ?? 1,
proportion: toFiniteNumber(row.workRatio) ?? 1, proportion: toFiniteNumber(row.workRatio) ?? 1,
fee: basicFee ?? 0, fee: toMoney(basicFee),
remark remark
} }
}) })
@ -1298,10 +1336,10 @@ const buildMethod2 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod2 | n
return { return {
proAmount: proSet.size > 0 ? proSet.size : 1, proAmount: proSet.size > 0 ? proSet.size : 1,
area: sumNumbers(det.map(item => item.area)), area: sumNumbers(det.map(item => item.area)),
basicFee: sumNumbers(det.map(item => item.basicFee)), basicFee: toMoney(sumNumbers(det.map(item => item.basicFee))),
basicFee_basic: sumNumbers(det.map(item => item.basicFee_basic)), basicFee_basic: toMoney(sumNumbers(det.map(item => item.basicFee_basic))),
basicFee_optional: sumNumbers(det.map(item => item.basicFee_optional)), basicFee_optional: toMoney(sumNumbers(det.map(item => item.basicFee_optional))),
fee: sumNumbers(det.map(item => item.fee)), fee: toMoney(sumNumbers(det.map(item => item.fee))),
det det
} }
} }
@ -1480,27 +1518,12 @@ const loadHtFeeMethodsByRow = async (mainStorageKey: string, rowId: string) => {
} }
} }
const buildServiceGroupLabelToIdMap = (serviceIds: string[]): Map<string, number> => {
const map = new Map<string, number>()
for (const serviceId of serviceIds) {
const item = (serviceList as Record<string, any>)[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 ( const buildServiceTasks = async (
contractId: string, contractId: string,
serviceId: string, serviceId: string
serviceLabelToId: Map<string, number>
): Promise<ExportTaskGroup[]> => { ): Promise<ExportTaskGroup[]> => {
const taskState = await zxFwPricingStore.loadKeyState<WorkContentStateLike>(`work-content-${contractId}-${serviceId}`) const taskState = await zxFwPricingStore.loadKeyState<WorkContentStateLike>(`work-content-${contractId}-${serviceId}`)
return groupWorkContentTasks(taskState?.detailRows, { serviceLabelToId }) return groupWorkContentTasks(taskState?.detailRows)
} }
const buildAdditionalRowTasks = async (contractId: string, rowId: string): Promise<ExportTaskGroup[]> => { const buildAdditionalRowTasks = async (contractId: string, rowId: string): Promise<ExportTaskGroup[]> => {
@ -1631,7 +1654,7 @@ const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
kvStore.getItem<ZxFwStorageLike>(`zxFW-${contractId}`), kvStore.getItem<ZxFwStorageLike>(`zxFW-${contractId}`),
kvStore.getItem<DetailRowsStorageLike<FactorRowLike>>(`ht-consult-category-factor-v1-${contractId}`), kvStore.getItem<DetailRowsStorageLike<FactorRowLike>>(`ht-consult-category-factor-v1-${contractId}`),
kvStore.getItem<DetailRowsStorageLike<FactorRowLike>>(`ht-major-factor-v1-${contractId}`), kvStore.getItem<DetailRowsStorageLike<FactorRowLike>>(`ht-major-factor-v1-${contractId}`),
kvStore.getItem<HtBaseInfoLike>(`ht-base-info-${contractId}`) zxFwPricingStore.loadKeyState<HtBaseInfoLike>(`ht-base-info-${contractId}`)
]) ])
const contractState = zxFwPricingStore.getContractState(contractId) const contractState = zxFwPricingStore.getContractState(contractId)
@ -1673,8 +1696,6 @@ const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
const serviceIdTexts = sortServiceIdsByDict( const serviceIdTexts = sortServiceIdsByDict(
(selectedIds.length > 0 ? selectedIds : fallbackServiceIds).filter(hasServiceId) (selectedIds.length > 0 ? selectedIds : fallbackServiceIds).filter(hasServiceId)
) )
const serviceLabelToId = buildServiceGroupLabelToIdMap(serviceIdTexts)
const services = ( const services = (
await Promise.all( await Promise.all(
serviceIdTexts.map(async serviceIdText => { serviceIdTexts.map(async serviceIdText => {
@ -1699,7 +1720,7 @@ const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
const method4 = buildMethod4(method4Raw?.detailRows) const method4 = buildMethod4(method4Raw?.detailRows)
const fee = buildServiceFee(sourceRow, method1, method2, method3, method4) const fee = buildServiceFee(sourceRow, method1, method2, method3, method4)
const finalFee = buildServiceFinalFee(sourceRow, method1, method2, method3, method4) const finalFee = buildServiceFinalFee(sourceRow, method1, method2, method3, method4)
const tasks = await buildServiceTasks(contractId, serviceIdText, serviceLabelToId) const tasks = await buildServiceTasks(contractId, serviceIdText)
const process = Number(sourceRow?.process) === 1 ? 1 : 0 const process = Number(sourceRow?.process) === 1 ? 1 : 0
const service: ExportService = { const service: ExportService = {
id: serviceId, id: serviceId,
@ -1717,6 +1738,8 @@ const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
) )
).filter((item): item is ExportService => Boolean(item)) ).filter((item): item is ExportService => Boolean(item))
const fixedFinalFee = toFiniteNumber(fixedRow?.finalFee)
const serviceFinalFeeSum = sumNumbers(services.map(item => item.finalFee))
const fixedSubtotal = toFiniteNumber(fixedRow?.subtotal) const fixedSubtotal = toFiniteNumber(fixedRow?.subtotal)
const serviceFeeSum = sumNumbers(services.map(item => item.fee)) const serviceFeeSum = sumNumbers(services.map(item => item.fee))
const fixedMethodSum = sumNumbers([ const fixedMethodSum = sumNumbers([
@ -1725,7 +1748,9 @@ const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
toFiniteNumber(fixedRow?.workload), toFiniteNumber(fixedRow?.workload),
toFiniteNumber(fixedRow?.hourly) toFiniteNumber(fixedRow?.hourly)
]) ])
const serviceFee = fixedSubtotal ?? (serviceFeeSum !== 0 ? serviceFeeSum : fixedMethodSum) const serviceFee =
fixedFinalFee ??
(services.length > 0 ? serviceFinalFeeSum : (fixedSubtotal ?? (serviceFeeSum !== 0 ? serviceFeeSum : fixedMethodSum)))
const [addtional, reserve] = await Promise.all([ const [addtional, reserve] = await Promise.all([
buildAdditionalExport(contractId), buildAdditionalExport(contractId),
buildReserveExport(contractId) buildReserveExport(contractId)
@ -1741,7 +1766,6 @@ const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
const contractServiceCoesRaw = buildProjectServiceCoes(htConsultCategoryFactorRaw?.detailRows) const contractServiceCoesRaw = buildProjectServiceCoes(htConsultCategoryFactorRaw?.detailRows)
const contractMajorCoesRaw = buildProjectMajorCoes(htMajorFactorRaw?.detailRows) const contractMajorCoesRaw = buildProjectMajorCoes(htMajorFactorRaw?.detailRows)
contracts.push({ contracts.push({
name: isNonEmptyString(contract.name) ? contract.name : `合同段-${index + 1}`, name: isNonEmptyString(contract.name) ? contract.name : `合同段-${index + 1}`,
serviceFee, serviceFee,
@ -2291,9 +2315,19 @@ watch(
class="pointer-events-auto rounded-xl border border-border bg-card px-4 py-3 text-foreground shadow-lg" class="pointer-events-auto rounded-xl border border-border bg-card px-4 py-3 text-foreground shadow-lg"
@update:open="(val) => { if (!val) dismissReportToast() }" @update:open="(val) => { if (!val) dismissReportToast() }"
> >
<div class="flex items-start justify-between gap-2">
<ToastTitle class="text-sm font-semibold text-foreground"> <ToastTitle class="text-sm font-semibold text-foreground">
{{ reportExportStatus === 'running' ? '导出报表' : (reportExportStatus === 'success' ? '导出成功' : '导出失败') }} {{ reportExportStatus === 'running' ? '导出报表' : (reportExportStatus === 'success' ? '导出成功' : '导出失败') }}
</ToastTitle> </ToastTitle>
<Button
variant="ghost"
size="sm"
class="h-6 w-6 p-0 text-muted-foreground hover:text-foreground"
@click="dismissReportToast"
>
<X class="h-3.5 w-3.5" />
</Button>
</div>
<ToastDescription class="mt-1 text-xs text-muted-foreground">{{ reportExportText }}</ToastDescription> <ToastDescription class="mt-1 text-xs text-muted-foreground">{{ reportExportText }}</ToastDescription>
<!-- <div v-if="reportExportStatus === 'success' && reportExportBlobUrl" class="mt-2 flex items-center gap-2"> <!-- <div v-if="reportExportStatus === 'success' && reportExportBlobUrl" class="mt-2 flex items-center gap-2">
<Button size="sm" class="h-7 rounded-md px-3 text-xs" @click="openExportedReport"> <Button size="sm" class="h-7 rounded-md px-3 text-xs" @click="openExportedReport">

View File

@ -30,6 +30,7 @@ const props = withDefaults(
categories: TypeLineCategory[] categories: TypeLineCategory[]
storageKey?: string storageKey?: string
defaultCategory?: string defaultCategory?: string
persistActiveCategory?: boolean
}>(), }>(),
{ {
scene: 'default', scene: 'default',
@ -38,15 +39,28 @@ const props = withDefaults(
metaText: '', metaText: '',
copyText: '', copyText: '',
storageKey: '', storageKey: '',
defaultCategory: '' defaultCategory: '',
persistActiveCategory: true
} }
) )
const cacheKey = computed(() => props.storageKey || `type-line-active-cat-${props.scene}`) const cacheKey = computed(() => props.storageKey || `type-line-active-cat-${props.scene}`)
const readStoredCategory = (key: string) => {
const sessionValue = sessionStorage.getItem(key)
if (sessionValue) return sessionValue
return localStorage.getItem(key)
}
const writeStoredCategory = (key: string, value: string) => {
sessionStorage.setItem(key, value)
localStorage.setItem(key, value)
}
const resolveInitialCategory = () => { const resolveInitialCategory = () => {
const defaultKey = props.defaultCategory || props.categories[0]?.key || '' const defaultKey = props.defaultCategory || props.categories[0]?.key || ''
const savedKey = sessionStorage.getItem(cacheKey.value) if (!props.persistActiveCategory) return defaultKey
const savedKey = readStoredCategory(cacheKey.value)
const validSavedKey = props.categories.some(item => item.key === savedKey) const validSavedKey = props.categories.some(item => item.key === savedKey)
return validSavedKey ? (savedKey as string) : defaultKey return validSavedKey ? (savedKey as string) : defaultKey
} }
@ -56,6 +70,8 @@ const activeCategory = ref(resolveInitialCategory())
watch( watch(
() => [props.categories, props.defaultCategory, cacheKey.value], () => [props.categories, props.defaultCategory, cacheKey.value],
() => { () => {
const isCurrentValid = props.categories.some(item => item.key === activeCategory.value)
if (isCurrentValid) return
activeCategory.value = resolveInitialCategory() activeCategory.value = resolveInitialCategory()
}, },
{ deep: true } { deep: true }
@ -65,7 +81,8 @@ watch(
const switchCategory = (cat: string) => { const switchCategory = (cat: string) => {
activeCategory.value = cat activeCategory.value = cat
sessionStorage.setItem(cacheKey.value, cat) if (!props.persistActiveCategory) return
writeStoredCategory(cacheKey.value, cat)
} }
const activeComponent = computed(() => { const activeComponent = computed(() => {

View File

@ -189,6 +189,20 @@ const toRowMap = <TRow extends { id: string }>(rows?: TRow[]) => {
return map return map
} }
const parseScopedMajorId = (value: unknown) => {
const raw = String(value || '').trim()
const scoped = /^\d+::(.+)$/.exec(raw)
return (scoped ? String(scoped[1] || '').trim() : raw) || raw
}
const hasScopedScaleRows = (rows?: Array<Record<string, unknown>>) =>
(rows || []).some(row => {
const id = String(row?.id || '')
if (/^\d+::/.test(id)) return true
const projectIndex = Number((row as { projectIndex?: unknown })?.projectIndex)
return Number.isFinite(projectIndex) && projectIndex > 1
})
const getDefaultConsultCategoryFactor = (serviceId: string | number) => { const getDefaultConsultCategoryFactor = (serviceId: string | number) => {
const service = (getServiceDictById() as Record<string, ServiceLite | undefined>)[String(serviceId)] const service = (getServiceDictById() as Record<string, ServiceLite | undefined>)[String(serviceId)]
return toFiniteNumberOrNull(service?.defCoe) return toFiniteNumberOrNull(service?.defCoe)
@ -614,6 +628,44 @@ const resolveScaleRows = (
return buildDefaultScaleRows(serviceId, consultCategoryFactorMap, majorFactorMap) return buildDefaultScaleRows(serviceId, consultCategoryFactorMap, majorFactorMap)
} }
const normalizeScopedScaleRows = (
serviceId: string,
rowsFromDb: Array<Record<string, unknown>> | undefined,
consultCategoryFactorMap?: Map<string, number | null>,
majorFactorMap?: Map<string, number | null>
): ScaleRow[] => {
const rows = stripGroupScaleRows(rowsFromDb) as Array<Record<string, unknown>>
if (rows.length === 0) return []
const defaultConsultCategoryFactor =
consultCategoryFactorMap?.get(String(serviceId)) ?? getDefaultConsultCategoryFactor(serviceId)
return rows.map(row => {
const parsedMajorId = parseScopedMajorId(row.id)
const resolvedMajorId = majorById.has(parsedMajorId) ? parsedMajorId : (majorIdAliasMap.get(parsedMajorId) || parsedMajorId)
const hasConsultCategoryFactor = hasOwn(row, 'consultCategoryFactor')
const hasMajorFactor = hasOwn(row, 'majorFactor')
const hasWorkStageFactor = hasOwn(row, 'workStageFactor')
const hasWorkRatio = hasOwn(row, 'workRatio')
return {
id: resolvedMajorId,
amount: toFiniteNumberOrNull(row.amount),
landArea: toFiniteNumberOrNull(row.landArea),
consultCategoryFactor:
toFiniteNumberOrNull(row.consultCategoryFactor) ??
(hasConsultCategoryFactor ? null : defaultConsultCategoryFactor),
majorFactor:
toFiniteNumberOrNull(row.majorFactor) ??
(hasMajorFactor ? null : (majorFactorMap?.get(resolvedMajorId) ?? getDefaultMajorFactorById(resolvedMajorId))),
workStageFactor:
toFiniteNumberOrNull(row.workStageFactor) ??
(hasWorkStageFactor ? null : 1),
workRatio:
toFiniteNumberOrNull(row.workRatio) ??
(hasWorkRatio ? null : 100)
}
})
}
// 统一生成某合同下某个咨询服务四种计费方式的存储键。 // 统一生成某合同下某个咨询服务四种计费方式的存储键。
// 优先复用 Pinia store 当前约定的 key避免与旧版 fallback key 脱节。 // 优先复用 Pinia store 当前约定的 key避免与旧版 fallback key 脱节。
export const getPricingMethodDetailDbKeys = ( export const getPricingMethodDetailDbKeys = (
@ -778,6 +830,14 @@ export const getPricingMethodTotalsForService = async (params: {
// 优先使用对应计费页的数据;不存在时回退合同段规模信息,再回退默认字典行。 // 优先使用对应计费页的数据;不存在时回退合同段规模信息,再回退默认字典行。
const excludeInvestmentCostAndAreaRows = params.options?.excludeInvestmentCostAndAreaRows === true const excludeInvestmentCostAndAreaRows = params.options?.excludeInvestmentCostAndAreaRows === true
const investScaleRowsSource = stripGroupScaleRows(investData?.detailRows as Array<Record<string, unknown>> | undefined)
const landScaleRowsSource = stripGroupScaleRows(landData?.detailRows as Array<Record<string, unknown>> | undefined)
const scopedInvestRows = hasScopedScaleRows(investScaleRowsSource)
? normalizeScopedScaleRows(serviceId, investScaleRowsSource, consultCategoryFactorMap, majorFactorMap)
: null
const scopedLandRows = hasScopedScaleRows(landScaleRowsSource)
? normalizeScopedScaleRows(serviceId, landScaleRowsSource, consultCategoryFactorMap, majorFactorMap)
: null
const investScale = onlyCostScale const investScale = onlyCostScale
? getOnlyCostScaleBudgetFee( ? getOnlyCostScaleBudgetFee(
serviceId, serviceId,
@ -788,7 +848,7 @@ export const getPricingMethodTotalsForService = async (params: {
industryId industryId
) )
: (() => { : (() => {
const investRows = resolveScaleRows( const investRows = scopedInvestRows || resolveScaleRows(
serviceId, serviceId,
investData, investData,
htData, htData,
@ -802,7 +862,7 @@ export const getPricingMethodTotalsForService = async (params: {
}) })
})() })()
const landRows = resolveScaleRows( const landRows = scopedLandRows || resolveScaleRows(
serviceId, serviceId,
landData, landData,
htData, htData,

View File

@ -857,7 +857,7 @@ export async function exportFile(fileName: string, data: any | (() => Promise<an
// 按模板生成最终工作簿:填充封面、目录、各分表及汇总数据。 // 按模板生成最终工作簿:填充封面、目录、各分表及汇总数据。
async function generateTemplate(data) { async function generateTemplate(data) {
// const downTextTmp = { richText: [{ font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: '常规' }, { font: { charset: 134, color: { theme: 1 }, italic: true, name: 'Calibri', size: 10, vertAlign: 'subscript' }, text: '下标' }] }; console.log(data)
// 编制说明 → 工作内容的前后默认项 // 编制说明 → 工作内容的前后默认项
let prefixIDs = [6, 7, 8, 9, 10, 11, 12, 13, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27]; let prefixIDs = [6, 7, 8, 9, 10, 11, 12, 13, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27];
let suffixIDs = [6, 7, 8, 9, 10, 11, 12, 13, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27]; let suffixIDs = [6, 7, 8, 9, 10, 11, 12, 13, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27];