1
This commit is contained in:
1 parent
62546bc937
commit
33913c29d2
9 files changed
+1153
-239
No files matched your search
+499
-2
@@ -19,6 +19,7 @@ import {
|
||||
AlertDialogTrigger,
|
||||
} from 'reka-ui'
|
||||
import { decodeZwArchive, encodeZwArchive, ZW_FILE_EXTENSION } from '@/lib/zwArchive'
|
||||
import { exportFile, serviceList } from '@/sql'
|
||||
|
||||
interface DataEntry {
|
||||
key: string
|
||||
@@ -43,6 +44,176 @@ type XmInfoLike = {
|
||||
projectName?: unknown
|
||||
}
|
||||
|
||||
interface ScaleRowLike {
|
||||
id: string
|
||||
amount?: unknown
|
||||
landArea?: unknown
|
||||
}
|
||||
|
||||
interface XmInfoStorageLike extends XmInfoLike {
|
||||
detailRows?: ScaleRowLike[]
|
||||
}
|
||||
|
||||
interface ContractCardItem {
|
||||
id: string
|
||||
name?: string
|
||||
order?: number
|
||||
}
|
||||
|
||||
interface ZxFwRowLike {
|
||||
id: string
|
||||
subtotal?: unknown
|
||||
investScale?: unknown
|
||||
landScale?: unknown
|
||||
workload?: unknown
|
||||
hourly?: unknown
|
||||
}
|
||||
|
||||
interface ZxFwStorageLike {
|
||||
detailRows?: ZxFwRowLike[]
|
||||
}
|
||||
|
||||
interface ScaleMethodRowLike extends ScaleRowLike {
|
||||
basicFormula?: unknown
|
||||
optionalFormula?: unknown
|
||||
budgetFee?: unknown
|
||||
budgetFeeBasic?: unknown
|
||||
budgetFeeOptional?: unknown
|
||||
consultCategoryFactor?: unknown
|
||||
majorFactor?: unknown
|
||||
remark?: unknown
|
||||
}
|
||||
|
||||
interface WorkloadMethodRowLike {
|
||||
id: string
|
||||
budgetAdoptedUnitPrice?: unknown
|
||||
workload?: unknown
|
||||
basicFee?: unknown
|
||||
consultCategoryFactor?: unknown
|
||||
serviceFee?: unknown
|
||||
remark?: unknown
|
||||
}
|
||||
|
||||
interface HourlyMethodRowLike {
|
||||
id: string
|
||||
adoptedBudgetUnitPrice?: unknown
|
||||
personnelCount?: unknown
|
||||
workdayCount?: unknown
|
||||
serviceBudget?: unknown
|
||||
remark?: unknown
|
||||
}
|
||||
|
||||
interface DetailRowsStorageLike<T> {
|
||||
detailRows?: T[]
|
||||
}
|
||||
|
||||
interface ExportScaleRow {
|
||||
major: number
|
||||
cost: number
|
||||
area: number
|
||||
}
|
||||
|
||||
interface ExportMethod1Detail {
|
||||
major: number
|
||||
cost: number
|
||||
basicFee: number
|
||||
basicFormula: string
|
||||
basicFee_basic: number
|
||||
optionalFormula: string
|
||||
basicFee_optional: number
|
||||
serviceCoe: number
|
||||
majorCoe: number
|
||||
fee: number
|
||||
desc: string
|
||||
}
|
||||
|
||||
interface ExportMethod1 {
|
||||
cost: number
|
||||
basicFee: number
|
||||
basicFee_basic: number
|
||||
basicFee_optional: number
|
||||
fee: number
|
||||
det: ExportMethod1Detail[]
|
||||
}
|
||||
|
||||
interface ExportMethod2Detail {
|
||||
major: number
|
||||
area: number
|
||||
basicFee: number
|
||||
basicFormula: string
|
||||
basicFee_basic: number
|
||||
optionalFormula: string
|
||||
basicFee_optional: number
|
||||
serviceCoe: number
|
||||
majorCoe: number
|
||||
fee: number
|
||||
desc: string
|
||||
}
|
||||
|
||||
interface ExportMethod2 {
|
||||
area: number
|
||||
basicFee: number
|
||||
basicFee_basic: number
|
||||
basicFee_optional: number
|
||||
fee: number
|
||||
det: ExportMethod2Detail[]
|
||||
}
|
||||
|
||||
interface ExportMethod3Detail {
|
||||
task: number
|
||||
price: number
|
||||
amount: number
|
||||
basicFee: number
|
||||
serviceCoe: number
|
||||
fee: number
|
||||
desc: string
|
||||
}
|
||||
|
||||
interface ExportMethod3 {
|
||||
basicFee: number
|
||||
fee: number
|
||||
det: ExportMethod3Detail[]
|
||||
}
|
||||
|
||||
interface ExportMethod4Detail {
|
||||
expert: number
|
||||
price: number
|
||||
person_num: number
|
||||
work_day: number
|
||||
fee: number
|
||||
desc: string
|
||||
}
|
||||
|
||||
interface ExportMethod4 {
|
||||
person_num: number
|
||||
work_day: number
|
||||
fee: number
|
||||
det: ExportMethod4Detail[]
|
||||
}
|
||||
|
||||
interface ExportService {
|
||||
id: number
|
||||
fee: number
|
||||
method1?: ExportMethod1
|
||||
method2?: ExportMethod2
|
||||
method3?: ExportMethod3
|
||||
method4?: ExportMethod4
|
||||
}
|
||||
|
||||
interface ExportContract {
|
||||
name: string
|
||||
fee: number
|
||||
scale: ExportScaleRow[]
|
||||
services: ExportService[]
|
||||
}
|
||||
|
||||
interface ExportReportPayload {
|
||||
name: string
|
||||
fee: number
|
||||
scale: ExportScaleRow[]
|
||||
contracts: ExportContract[]
|
||||
}
|
||||
|
||||
const USER_GUIDE_COMPLETED_KEY = 'jgjs-user-guide-completed-v1'
|
||||
const userGuideSteps: UserGuideStep[] = [
|
||||
{
|
||||
@@ -453,6 +624,321 @@ const getExportProjectName = (entries: DataEntry[]): string => {
|
||||
return typeof data.projectName === 'string' ? sanitizeFileNamePart(data.projectName) : '造价项目'
|
||||
}
|
||||
|
||||
const toFiniteNumber = (value: unknown): number | null => {
|
||||
const num = Number(value)
|
||||
return Number.isFinite(num) ? num : null
|
||||
}
|
||||
|
||||
const toFiniteNumberOrZero = (value: unknown): number => toFiniteNumber(value) ?? 0
|
||||
|
||||
const toSafeInteger = (value: unknown): number | null => {
|
||||
const num = Number(value)
|
||||
if (!Number.isInteger(num)) return null
|
||||
if (!Number.isSafeInteger(num)) return null
|
||||
return num
|
||||
}
|
||||
|
||||
const sumNumbers = (values: Array<number | null | undefined>): number =>
|
||||
values.reduce<number>(
|
||||
(sum, value) => sum + (typeof value === 'number' && Number.isFinite(value) ? value : 0),
|
||||
0
|
||||
)
|
||||
|
||||
const isNonEmptyString = (value: unknown): value is string =>
|
||||
typeof value === 'string' && value.trim().length > 0
|
||||
|
||||
const getTaskIdFromRowId = (value: string): number | null => {
|
||||
const match = /^task-(\d+)-\d+$/.exec(value)
|
||||
return match ? toSafeInteger(match[1]) : null
|
||||
}
|
||||
|
||||
const getExpertIdFromRowId = (value: string): number | null => {
|
||||
const match = /^expert-(\d+)$/.exec(value)
|
||||
return match ? toSafeInteger(match[1]) : null
|
||||
}
|
||||
|
||||
const hasServiceId = (serviceId: string) =>
|
||||
Object.prototype.hasOwnProperty.call(serviceList as Record<string, unknown>, serviceId)
|
||||
|
||||
const buildScaleRows = (rows: ScaleRowLike[] | undefined): ExportScaleRow[] => {
|
||||
if (!Array.isArray(rows)) return []
|
||||
return rows
|
||||
.map(row => {
|
||||
const major = toSafeInteger(row.id)
|
||||
const cost = toFiniteNumber(row.amount)
|
||||
const area = toFiniteNumber(row.landArea)
|
||||
if (major == null || (cost == null && area == null)) return null
|
||||
return {
|
||||
major,
|
||||
cost: cost ?? 0,
|
||||
area: area ?? 0
|
||||
}
|
||||
})
|
||||
.filter((item): item is ExportScaleRow => Boolean(item))
|
||||
}
|
||||
|
||||
const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | null => {
|
||||
if (!Array.isArray(rows)) return null
|
||||
const det = rows
|
||||
.map(row => {
|
||||
const major = toSafeInteger(row.id)
|
||||
if (major == null) return null
|
||||
const cost = toFiniteNumber(row.amount)
|
||||
const basicFee = toFiniteNumber(row.budgetFee)
|
||||
const basicFeeBasic = toFiniteNumber(row.budgetFeeBasic)
|
||||
const basicFeeOptional = toFiniteNumber(row.budgetFeeOptional)
|
||||
const desc = typeof row.remark === 'string' ? row.remark : ''
|
||||
const hasValue =
|
||||
cost != null ||
|
||||
basicFee != null ||
|
||||
basicFeeBasic != null ||
|
||||
basicFeeOptional != null ||
|
||||
isNonEmptyString(desc)
|
||||
if (!hasValue) return null
|
||||
return {
|
||||
major,
|
||||
cost: cost ?? 0,
|
||||
basicFee: basicFee ?? 0,
|
||||
basicFormula: typeof row.basicFormula === 'string' ? row.basicFormula : '',
|
||||
basicFee_basic: basicFeeBasic ?? 0,
|
||||
optionalFormula: typeof row.optionalFormula === 'string' ? row.optionalFormula : '',
|
||||
basicFee_optional: basicFeeOptional ?? 0,
|
||||
serviceCoe: toFiniteNumberOrZero(row.consultCategoryFactor),
|
||||
majorCoe: toFiniteNumberOrZero(row.majorFactor),
|
||||
fee: basicFee ?? 0,
|
||||
desc
|
||||
}
|
||||
})
|
||||
.filter((item): item is ExportMethod1Detail => Boolean(item))
|
||||
|
||||
if (det.length === 0) return null
|
||||
return {
|
||||
cost: sumNumbers(det.map(item => item.cost)),
|
||||
basicFee: sumNumbers(det.map(item => item.basicFee)),
|
||||
basicFee_basic: sumNumbers(det.map(item => item.basicFee_basic)),
|
||||
basicFee_optional: sumNumbers(det.map(item => item.basicFee_optional)),
|
||||
fee: sumNumbers(det.map(item => item.fee)),
|
||||
det
|
||||
}
|
||||
}
|
||||
|
||||
const buildMethod2 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod2 | null => {
|
||||
if (!Array.isArray(rows)) return null
|
||||
const det = rows
|
||||
.map(row => {
|
||||
const major = toSafeInteger(row.id)
|
||||
if (major == null) return null
|
||||
const area = toFiniteNumber(row.landArea)
|
||||
const basicFee = toFiniteNumber(row.budgetFee)
|
||||
const basicFeeBasic = toFiniteNumber(row.budgetFeeBasic)
|
||||
const basicFeeOptional = toFiniteNumber(row.budgetFeeOptional)
|
||||
const desc = typeof row.remark === 'string' ? row.remark : ''
|
||||
const hasValue =
|
||||
area != null ||
|
||||
basicFee != null ||
|
||||
basicFeeBasic != null ||
|
||||
basicFeeOptional != null ||
|
||||
isNonEmptyString(desc)
|
||||
if (!hasValue) return null
|
||||
return {
|
||||
major,
|
||||
area: area ?? 0,
|
||||
basicFee: basicFee ?? 0,
|
||||
basicFormula: typeof row.basicFormula === 'string' ? row.basicFormula : '',
|
||||
basicFee_basic: basicFeeBasic ?? 0,
|
||||
optionalFormula: typeof row.optionalFormula === 'string' ? row.optionalFormula : '',
|
||||
basicFee_optional: basicFeeOptional ?? 0,
|
||||
serviceCoe: toFiniteNumberOrZero(row.consultCategoryFactor),
|
||||
majorCoe: toFiniteNumberOrZero(row.majorFactor),
|
||||
fee: basicFee ?? 0,
|
||||
desc
|
||||
}
|
||||
})
|
||||
.filter((item): item is ExportMethod2Detail => Boolean(item))
|
||||
|
||||
if (det.length === 0) return null
|
||||
return {
|
||||
area: sumNumbers(det.map(item => item.area)),
|
||||
basicFee: sumNumbers(det.map(item => item.basicFee)),
|
||||
basicFee_basic: sumNumbers(det.map(item => item.basicFee_basic)),
|
||||
basicFee_optional: sumNumbers(det.map(item => item.basicFee_optional)),
|
||||
fee: sumNumbers(det.map(item => item.fee)),
|
||||
det
|
||||
}
|
||||
}
|
||||
|
||||
const buildMethod3 = (rows: WorkloadMethodRowLike[] | undefined): ExportMethod3 | null => {
|
||||
if (!Array.isArray(rows)) return null
|
||||
const det = rows
|
||||
.map(row => {
|
||||
const task = getTaskIdFromRowId(row.id)
|
||||
if (task == null) return null
|
||||
const amount = toFiniteNumber(row.workload)
|
||||
const basicFee = toFiniteNumber(row.basicFee)
|
||||
const fee = toFiniteNumber(row.serviceFee)
|
||||
const desc = typeof row.remark === 'string' ? row.remark : ''
|
||||
const hasValue = amount != null || basicFee != null || fee != null || isNonEmptyString(desc)
|
||||
if (!hasValue) return null
|
||||
return {
|
||||
task,
|
||||
price: toFiniteNumberOrZero(row.budgetAdoptedUnitPrice),
|
||||
amount: amount ?? 0,
|
||||
basicFee: basicFee ?? 0,
|
||||
serviceCoe: toFiniteNumberOrZero(row.consultCategoryFactor),
|
||||
fee: fee ?? 0,
|
||||
desc
|
||||
}
|
||||
})
|
||||
.filter((item): item is ExportMethod3Detail => Boolean(item))
|
||||
|
||||
if (det.length === 0) return null
|
||||
return {
|
||||
basicFee: sumNumbers(det.map(item => item.basicFee)),
|
||||
fee: sumNumbers(det.map(item => item.fee)),
|
||||
det
|
||||
}
|
||||
}
|
||||
|
||||
const buildMethod4 = (rows: HourlyMethodRowLike[] | undefined): ExportMethod4 | null => {
|
||||
if (!Array.isArray(rows)) return null
|
||||
const det = rows
|
||||
.map(row => {
|
||||
const expert = getExpertIdFromRowId(row.id)
|
||||
if (expert == null) return null
|
||||
const personNum = toFiniteNumber(row.personnelCount)
|
||||
const workDay = toFiniteNumber(row.workdayCount)
|
||||
const fee = toFiniteNumber(row.serviceBudget)
|
||||
const desc = typeof row.remark === 'string' ? row.remark : ''
|
||||
const hasValue = personNum != null || workDay != null || fee != null || isNonEmptyString(desc)
|
||||
if (!hasValue) return null
|
||||
return {
|
||||
expert,
|
||||
price: toFiniteNumberOrZero(row.adoptedBudgetUnitPrice),
|
||||
person_num: personNum ?? 0,
|
||||
work_day: workDay ?? 0,
|
||||
fee: fee ?? 0,
|
||||
desc
|
||||
}
|
||||
})
|
||||
.filter((item): item is ExportMethod4Detail => Boolean(item))
|
||||
|
||||
if (det.length === 0) return null
|
||||
return {
|
||||
person_num: sumNumbers(det.map(item => item.person_num)),
|
||||
work_day: sumNumbers(det.map(item => item.work_day)),
|
||||
fee: sumNumbers(det.map(item => item.fee)),
|
||||
det
|
||||
}
|
||||
}
|
||||
|
||||
const buildServiceFee = (
|
||||
row: ZxFwRowLike,
|
||||
method1: ExportMethod1 | null,
|
||||
method2: ExportMethod2 | null,
|
||||
method3: ExportMethod3 | null,
|
||||
method4: ExportMethod4 | null
|
||||
) => {
|
||||
const subtotal = toFiniteNumber(row.subtotal)
|
||||
if (subtotal != null) return subtotal
|
||||
|
||||
const methodSum = sumNumbers([method1?.fee, method2?.fee, method3?.fee, method4?.fee])
|
||||
if (methodSum !== 0) return methodSum
|
||||
|
||||
return sumNumbers([
|
||||
toFiniteNumber(row.investScale),
|
||||
toFiniteNumber(row.landScale),
|
||||
toFiniteNumber(row.workload),
|
||||
toFiniteNumber(row.hourly)
|
||||
])
|
||||
}
|
||||
|
||||
const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
|
||||
const [xmInfoRaw, contractCardsRaw] = await Promise.all([
|
||||
localforage.getItem<XmInfoStorageLike>('xm-info-v3'),
|
||||
localforage.getItem<ContractCardItem[]>('ht-card-v1')
|
||||
])
|
||||
|
||||
const xmInfo = xmInfoRaw || {}
|
||||
const projectScale = buildScaleRows(xmInfo.detailRows)
|
||||
const projectName = isNonEmptyString(xmInfo.projectName) ? xmInfo.projectName.trim() : '造价项目'
|
||||
|
||||
const contractCards = (Array.isArray(contractCardsRaw) ? contractCardsRaw : [])
|
||||
.filter(item => item && typeof item.id === 'string')
|
||||
.sort((a, b) => (typeof a.order === 'number' ? a.order : Number.MAX_SAFE_INTEGER) - (typeof b.order === 'number' ? b.order : Number.MAX_SAFE_INTEGER))
|
||||
|
||||
const contracts: ExportContract[] = []
|
||||
|
||||
for (let index = 0; index < contractCards.length; index++) {
|
||||
const contract = contractCards[index]
|
||||
const contractId = contract.id
|
||||
const [htInfoRaw, zxFwRaw] = await Promise.all([
|
||||
localforage.getItem<DetailRowsStorageLike<ScaleRowLike>>(`ht-info-v3-${contractId}`),
|
||||
localforage.getItem<ZxFwStorageLike>(`zxFW-${contractId}`)
|
||||
])
|
||||
|
||||
const zxRows = Array.isArray(zxFwRaw?.detailRows) ? zxFwRaw.detailRows : []
|
||||
const fixedRow = zxRows.find(row => row.id === 'fixed-budget-c')
|
||||
const serviceRows = zxRows.filter(row => row.id !== 'fixed-budget-c' && hasServiceId(String(row.id)))
|
||||
|
||||
const services = (
|
||||
await Promise.all(
|
||||
serviceRows.map(async row => {
|
||||
const serviceIdText = String(row.id)
|
||||
const serviceId = toSafeInteger(serviceIdText)
|
||||
if (serviceId == null) return null
|
||||
|
||||
const [method1Raw, method2Raw, method3Raw, method4Raw] = await Promise.all([
|
||||
localforage.getItem<DetailRowsStorageLike<ScaleMethodRowLike>>(`tzGMF-${contractId}-${serviceIdText}`),
|
||||
localforage.getItem<DetailRowsStorageLike<ScaleMethodRowLike>>(`ydGMF-${contractId}-${serviceIdText}`),
|
||||
localforage.getItem<DetailRowsStorageLike<WorkloadMethodRowLike>>(`gzlF-${contractId}-${serviceIdText}`),
|
||||
localforage.getItem<DetailRowsStorageLike<HourlyMethodRowLike>>(`hourlyPricing-${contractId}-${serviceIdText}`)
|
||||
])
|
||||
|
||||
const method1 = buildMethod1(method1Raw?.detailRows)
|
||||
const method2 = buildMethod2(method2Raw?.detailRows)
|
||||
const method3 = buildMethod3(method3Raw?.detailRows)
|
||||
const method4 = buildMethod4(method4Raw?.detailRows)
|
||||
const fee = buildServiceFee(row, method1, method2, method3, method4)
|
||||
|
||||
const service: ExportService = {
|
||||
id: serviceId,
|
||||
fee
|
||||
}
|
||||
if (method1) service.method1 = method1
|
||||
if (method2) service.method2 = method2
|
||||
if (method3) service.method3 = method3
|
||||
if (method4) service.method4 = method4
|
||||
return service
|
||||
})
|
||||
)
|
||||
).filter((item): item is ExportService => Boolean(item))
|
||||
|
||||
const fixedSubtotal = toFiniteNumber(fixedRow?.subtotal)
|
||||
const serviceFeeSum = sumNumbers(services.map(item => item.fee))
|
||||
const fixedMethodSum = sumNumbers([
|
||||
toFiniteNumber(fixedRow?.investScale),
|
||||
toFiniteNumber(fixedRow?.landScale),
|
||||
toFiniteNumber(fixedRow?.workload),
|
||||
toFiniteNumber(fixedRow?.hourly)
|
||||
])
|
||||
const contractFee = fixedSubtotal ?? (serviceFeeSum !== 0 ? serviceFeeSum : fixedMethodSum)
|
||||
|
||||
contracts.push({
|
||||
name: isNonEmptyString(contract.name) ? contract.name : `合同段-${index + 1}`,
|
||||
fee: contractFee,
|
||||
scale: buildScaleRows(htInfoRaw?.detailRows),
|
||||
services
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
name: projectName,
|
||||
fee: sumNumbers(contracts.map(item => item.fee)),
|
||||
scale: projectScale,
|
||||
contracts
|
||||
}
|
||||
}
|
||||
|
||||
const exportData = async () => {
|
||||
try {
|
||||
const now = new Date()
|
||||
@@ -485,8 +971,19 @@ const exportData = async () => {
|
||||
dataMenuOpen.value = false
|
||||
}
|
||||
}
|
||||
const exportReport = async ()=>{
|
||||
|
||||
|
||||
const exportReport = async () => {
|
||||
try {
|
||||
const now = new Date()
|
||||
const payload = await buildExportReportPayload()
|
||||
const fileName = `${sanitizeFileNamePart(payload.name)}-报表-${formatExportTimestamp(now)}`
|
||||
await exportFile(fileName, payload)
|
||||
} catch (error) {
|
||||
console.error('export report failed:', error)
|
||||
window.alert('导出报表失败,请重试。')
|
||||
} finally {
|
||||
dataMenuOpen.value = false
|
||||
}
|
||||
}
|
||||
const triggerImport = () => {
|
||||
importFileRef.value?.click()
|
||||
|
||||
Reference in new issue
Block a user