final
This commit is contained in:
parent
398fca9265
commit
f4f6e5c618
@ -119,6 +119,7 @@ const buildDefaultRows = (): DetailRow[] => {
|
|||||||
const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] => {
|
const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] => {
|
||||||
const dbValueMap = new Map<string, DetailRow>()
|
const dbValueMap = new Map<string, DetailRow>()
|
||||||
for (const row of rowsFromDb || []) {
|
for (const row of rowsFromDb || []) {
|
||||||
|
if (row?.isGroupRow === true) continue
|
||||||
const rowId = String(row.id)
|
const rowId = String(row.id)
|
||||||
dbValueMap.set(rowId, row)
|
dbValueMap.set(rowId, row)
|
||||||
const aliasId = majorIdAliasMap.get(rowId)
|
const aliasId = majorIdAliasMap.get(rowId)
|
||||||
@ -140,6 +141,45 @@ const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] =>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const buildGroupRows = (rows: DetailRow[]): DetailRow[] => {
|
||||||
|
const rowById = new Map(rows.map(row => [String(row.id || ''), row] as const))
|
||||||
|
const groupRows: DetailRow[] = []
|
||||||
|
for (const group of detailDict.value) {
|
||||||
|
let amountTotal = 0
|
||||||
|
let hasAmount = false
|
||||||
|
let landAreaTotal = 0
|
||||||
|
let hasLandArea = false
|
||||||
|
for (const child of group.children) {
|
||||||
|
const leaf = rowById.get(String(child.id || ''))
|
||||||
|
const amount = leaf?.amount
|
||||||
|
if (typeof amount === 'number' && Number.isFinite(amount)) {
|
||||||
|
amountTotal += amount
|
||||||
|
hasAmount = true
|
||||||
|
}
|
||||||
|
const landArea = leaf?.landArea
|
||||||
|
if (typeof landArea === 'number' && Number.isFinite(landArea)) {
|
||||||
|
landAreaTotal += landArea
|
||||||
|
hasLandArea = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
groupRows.push({
|
||||||
|
id: group.id,
|
||||||
|
groupCode: group.code,
|
||||||
|
groupName: group.name,
|
||||||
|
majorCode: group.code,
|
||||||
|
majorName: group.name,
|
||||||
|
hasCost: true,
|
||||||
|
hasArea: true,
|
||||||
|
amount: hasAmount ? roundTo(amountTotal, 3) : null,
|
||||||
|
landArea: hasLandArea ? roundTo(landAreaTotal, 3) : null,
|
||||||
|
path: [`${group.code} ${group.name}`],
|
||||||
|
hide: false,
|
||||||
|
isGroupRow: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return groupRows
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const applyPinnedTotalAmount = (
|
const applyPinnedTotalAmount = (
|
||||||
api: GridApi<DetailRow> | null | undefined,
|
api: GridApi<DetailRow> | null | undefined,
|
||||||
@ -239,6 +279,7 @@ interface DetailRow {
|
|||||||
landArea: number | null
|
landArea: number | null
|
||||||
path: string[]
|
path: string[]
|
||||||
hide?: boolean
|
hide?: boolean
|
||||||
|
isGroupRow?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface XmBaseInfoState {
|
interface XmBaseInfoState {
|
||||||
@ -398,11 +439,13 @@ const pinnedTopRowData = ref<DetailRow[]>([
|
|||||||
|
|
||||||
const saveToIndexedDB = async () => {
|
const saveToIndexedDB = async () => {
|
||||||
try {
|
try {
|
||||||
|
const leafRows = detailRows.value.map(row => ({
|
||||||
|
...JSON.parse(JSON.stringify(row)),
|
||||||
|
hide: Boolean(row.hide),
|
||||||
|
isGroupRow: false
|
||||||
|
}))
|
||||||
const payload: GridPersistState = {
|
const payload: GridPersistState = {
|
||||||
detailRows: detailRows.value.map(row => ({
|
detailRows: [...leafRows, ...buildGroupRows(leafRows)]
|
||||||
...JSON.parse(JSON.stringify(row)),
|
|
||||||
hide: Boolean(row.hide)
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
payload.roughCalcEnabled = roughCalcEnabled.value
|
payload.roughCalcEnabled = roughCalcEnabled.value
|
||||||
payload.totalAmount = pinnedTopRowData.value[0].amount
|
payload.totalAmount = pinnedTopRowData.value[0].amount
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
import { computed, defineComponent, h, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, defineComponent, h, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import type { ComponentPublicInstance, PropType } from 'vue'
|
import type { ComponentPublicInstance, PropType } from 'vue'
|
||||||
import { AgGridVue } from 'ag-grid-vue3'
|
import { AgGridVue } from 'ag-grid-vue3'
|
||||||
import type { ColDef, GridOptions, ICellRendererParams, IHeaderParams } from 'ag-grid-community'
|
import type { ColDef, GridOptions, ICellRendererParams } from 'ag-grid-community'
|
||||||
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
||||||
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
|
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
|
||||||
import { addNumbers } from '@/lib/decimal'
|
import { addNumbers } from '@/lib/decimal'
|
||||||
@ -14,7 +14,7 @@ import {
|
|||||||
getPricingMethodTotalsForServices,
|
getPricingMethodTotalsForServices,
|
||||||
type PricingMethodTotals
|
type PricingMethodTotals
|
||||||
} from '@/lib/pricingMethodTotals'
|
} from '@/lib/pricingMethodTotals'
|
||||||
import { Pencil, Eraser, Trash2, CircleHelp } from 'lucide-vue-next'
|
import { Pencil, Eraser, Trash2 } from 'lucide-vue-next'
|
||||||
import {
|
import {
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
@ -554,45 +554,37 @@ const ProcessCellRenderer = defineComponent({
|
|||||||
return () => {
|
return () => {
|
||||||
const row = props.params.data
|
const row = props.params.data
|
||||||
if (!row || isFixedRow(row)) return null
|
if (!row || isFixedRow(row)) return null
|
||||||
const checked = row.process === 1
|
const processValue = row.process === 1 ? 1 : 0
|
||||||
const onToggle = (event: Event) => {
|
const onSelect = (event: Event, value: 0 | 1) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
const target = event.target as HTMLInputElement | null
|
void props.params.context?.onSetProcess?.(row.id, value)
|
||||||
void props.params.context?.onToggleProcess?.(row.id, Boolean(target?.checked))
|
|
||||||
}
|
}
|
||||||
return h('div', { class: 'flex items-center justify-center w-full' }, [
|
const radioName = `process-${row.id}`
|
||||||
h('input', {
|
return h('div', { class: 'flex items-center justify-center gap-4 w-full text-sm' }, [
|
||||||
type: 'checkbox',
|
h('label', { class: 'inline-flex items-center gap-1.5 cursor-pointer' }, [
|
||||||
checked,
|
h('input', {
|
||||||
class: 'cursor-pointer',
|
type: 'radio',
|
||||||
onChange: onToggle
|
name: radioName,
|
||||||
})
|
checked: processValue === 0,
|
||||||
])
|
class: 'cursor-pointer h-4 w-4',
|
||||||
}
|
onClick: (event: Event) => event.stopPropagation(),
|
||||||
}
|
onChange: (event: Event) => onSelect(event, 0)
|
||||||
})
|
}),
|
||||||
|
h('span', '编制')
|
||||||
const ProcessHeaderRenderer = defineComponent({
|
]),
|
||||||
name: 'ProcessHeaderRenderer',
|
h('label', { class: 'inline-flex items-center gap-1.5 cursor-pointer' }, [
|
||||||
props: {
|
h('input', {
|
||||||
params: {
|
type: 'radio',
|
||||||
type: Object as PropType<IHeaderParams>,
|
name: radioName,
|
||||||
required: true
|
checked: processValue === 1,
|
||||||
}
|
class: 'cursor-pointer h-4 w-4',
|
||||||
},
|
onClick: (event: Event) => event.stopPropagation(),
|
||||||
setup(props) {
|
onChange: (event: Event) => onSelect(event, 1)
|
||||||
const tooltipText = '默认为编制,勾选为审核'
|
}),
|
||||||
props.params.setTooltip?.(tooltipText, () => true)
|
h('span', '审核')
|
||||||
return () =>
|
|
||||||
h('div', { class: 'flex items-center justify-center gap-1 w-full' }, [
|
|
||||||
h('span', props.params.displayName || '工作环节'),
|
|
||||||
h('span', { class: 'inline-flex items-center pointer-events-auto' }, [
|
|
||||||
h(CircleHelp, {
|
|
||||||
size: 20,
|
|
||||||
class: 'text-muted-foreground pointer-events-none'
|
|
||||||
})
|
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -624,10 +616,10 @@ const columnDefs: ColDef<DetailRow>[] = [
|
|||||||
{
|
{
|
||||||
headerName: '工作环节',
|
headerName: '工作环节',
|
||||||
field: 'process',
|
field: 'process',
|
||||||
headerClass: 'ag-center-header',
|
headerClass: 'ag-center-header zxfw-process-header',
|
||||||
minWidth: 90,
|
minWidth: 170,
|
||||||
maxWidth: 110,
|
maxWidth: 220,
|
||||||
flex: 1,
|
flex: 1.2,
|
||||||
editable: false,
|
editable: false,
|
||||||
sortable: false,
|
sortable: false,
|
||||||
filter: false,
|
filter: false,
|
||||||
@ -641,7 +633,6 @@ const columnDefs: ColDef<DetailRow>[] = [
|
|||||||
if (!params.data || isFixedRow(params.data)) return null
|
if (!params.data || isFixedRow(params.data)) return null
|
||||||
return params.data.process === 1 ? 1 : 0
|
return params.data.process === 1 ? 1 : 0
|
||||||
},
|
},
|
||||||
headerComponent: ProcessHeaderRenderer,
|
|
||||||
cellRenderer: ProcessCellRenderer
|
cellRenderer: ProcessCellRenderer
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -754,15 +745,17 @@ const detailGridOptions: GridOptions<DetailRow> = {
|
|||||||
treeData: false,
|
treeData: false,
|
||||||
getDataPath: undefined,
|
getDataPath: undefined,
|
||||||
context: {
|
context: {
|
||||||
onToggleProcess: async (rowId: string, checked: boolean) => {
|
onSetProcess: async (rowId: string, value: 0 | 1) => {
|
||||||
const currentState = getCurrentContractState()
|
const currentState = getCurrentContractState()
|
||||||
let changed = false
|
let changed = false
|
||||||
const nextRows = currentState.detailRows.map(row => {
|
const nextRows = currentState.detailRows.map(row => {
|
||||||
if (isFixedRow(row) || String(row.id) !== String(rowId)) return row
|
if (isFixedRow(row) || String(row.id) !== String(rowId)) return row
|
||||||
|
const nextValue = value === 1 ? 1 : 0
|
||||||
|
if ((row.process === 1 ? 1 : 0) === nextValue) return row
|
||||||
changed = true
|
changed = true
|
||||||
return {
|
return {
|
||||||
...row,
|
...row,
|
||||||
process: checked ? 1 : 0
|
process: nextValue
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (!changed) return
|
if (!changed) return
|
||||||
@ -1208,3 +1201,13 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.zxfw-process-header .ag-header-cell-label) {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.zxfw-process-header .ag-header-cell-text) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -20,6 +20,11 @@ import {
|
|||||||
AlertDialogRoot,
|
AlertDialogRoot,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
|
ToastDescription,
|
||||||
|
ToastProvider,
|
||||||
|
ToastRoot,
|
||||||
|
ToastTitle,
|
||||||
|
ToastViewport,
|
||||||
} from 'reka-ui'
|
} from 'reka-ui'
|
||||||
import { decodeZwArchive, encodeZwArchive, ZW_FILE_EXTENSION } from '@/lib/zwArchive'
|
import { decodeZwArchive, encodeZwArchive, ZW_FILE_EXTENSION } from '@/lib/zwArchive'
|
||||||
import { addNumbers, roundTo } from '@/lib/decimal'
|
import { addNumbers, roundTo } from '@/lib/decimal'
|
||||||
@ -62,6 +67,7 @@ interface ScaleRowLike {
|
|||||||
id: string
|
id: string
|
||||||
amount: number | null
|
amount: number | null
|
||||||
landArea: number | null
|
landArea: number | null
|
||||||
|
isGroupRow?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface XmInfoStorageLike extends XmInfoLike {
|
interface XmInfoStorageLike extends XmInfoLike {
|
||||||
@ -457,6 +463,10 @@ const pendingImportPayload = shallowRef<DataPackage | null>(null)
|
|||||||
const pendingImportFileName = ref('')
|
const pendingImportFileName = ref('')
|
||||||
const userGuideOpen = ref(false)
|
const userGuideOpen = ref(false)
|
||||||
const userGuideStepIndex = ref(0)
|
const userGuideStepIndex = ref(0)
|
||||||
|
const reportExportToastOpen = ref(false)
|
||||||
|
const reportExportProgress = ref(0)
|
||||||
|
const reportExportStatus = ref<'running' | 'success' | 'error'>('running')
|
||||||
|
const reportExportText = ref('')
|
||||||
const tabItemElMap = new Map<string, HTMLElement>()
|
const tabItemElMap = new Map<string, HTMLElement>()
|
||||||
const tabTitleElMap = new Map<string, HTMLElement>()
|
const tabTitleElMap = new Map<string, HTMLElement>()
|
||||||
const tabPanelElMap = new Map<string, HTMLElement>()
|
const tabPanelElMap = new Map<string, HTMLElement>()
|
||||||
@ -468,6 +478,7 @@ const isTabDragging = ref(false)
|
|||||||
const tabTitleOverflowMap = ref<Record<string, boolean>>({})
|
const tabTitleOverflowMap = ref<Record<string, boolean>>({})
|
||||||
let tabStripViewportEl: HTMLElement | null = null
|
let tabStripViewportEl: HTMLElement | null = null
|
||||||
let tabTitleOverflowRafId: number | null = null
|
let tabTitleOverflowRafId: number | null = null
|
||||||
|
let reportExportToastTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
const tabsModel = computed({
|
const tabsModel = computed({
|
||||||
get: () => tabStore.tabs,
|
get: () => tabStore.tabs,
|
||||||
@ -502,6 +513,31 @@ const closeMenus = () => {
|
|||||||
dataMenuOpen.value = false
|
dataMenuOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clearReportExportToastTimer = () => {
|
||||||
|
if (!reportExportToastTimer) return
|
||||||
|
clearTimeout(reportExportToastTimer)
|
||||||
|
reportExportToastTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const showReportExportProgress = (progress: number, text: string) => {
|
||||||
|
clearReportExportToastTimer()
|
||||||
|
reportExportStatus.value = 'running'
|
||||||
|
reportExportProgress.value = Math.max(0, Math.min(100, progress))
|
||||||
|
reportExportText.value = text
|
||||||
|
reportExportToastOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const finishReportExportProgress = (success: boolean, text: string) => {
|
||||||
|
clearReportExportToastTimer()
|
||||||
|
reportExportStatus.value = success ? 'success' : 'error'
|
||||||
|
reportExportProgress.value = 100
|
||||||
|
reportExportText.value = text
|
||||||
|
reportExportToastOpen.value = true
|
||||||
|
reportExportToastTimer = setTimeout(() => {
|
||||||
|
reportExportToastOpen.value = false
|
||||||
|
}, success ? 1200 : 1800)
|
||||||
|
}
|
||||||
|
|
||||||
const markGuideCompleted = () => {
|
const markGuideCompleted = () => {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(USER_GUIDE_COMPLETED_KEY, '1')
|
localStorage.setItem(USER_GUIDE_COMPLETED_KEY, '1')
|
||||||
@ -1016,6 +1052,16 @@ const toExportScaleRows = (rows: ScaleRowLike[] | undefined): ExportScaleRow[] =
|
|||||||
.filter((item): item is ExportScaleRow => Boolean(item))
|
.filter((item): item is ExportScaleRow => Boolean(item))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sumLeafScaleCost = (rows: ScaleRowLike[] | undefined) => {
|
||||||
|
if (!Array.isArray(rows)) return 0
|
||||||
|
return sumNumbers(
|
||||||
|
rows.map(row => {
|
||||||
|
if (row?.isGroupRow === true) return null
|
||||||
|
return toFiniteNumber(row?.amount)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | null => {
|
const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | null => {
|
||||||
@ -1365,9 +1411,8 @@ const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
|
|||||||
|
|
||||||
const projectInfo = projectInfoRaw || {}
|
const projectInfo = projectInfoRaw || {}
|
||||||
const projectScaleSource = projectScaleRaw || {}
|
const projectScaleSource = projectScaleRaw || {}
|
||||||
|
|
||||||
const projectScale = projectScaleSource.roughCalcEnabled ? [] : toExportScaleRows(projectScaleSource.detailRows)
|
const projectScale = projectScaleSource.roughCalcEnabled ? [] : toExportScaleRows(projectScaleSource.detailRows)
|
||||||
const projectScaleCost = toFiniteNumber(projectScaleSource.totalAmount) ?? sumNumbers(projectScale.map(item => item.cost))
|
const projectScaleCost = toFiniteNumber(projectScaleSource.totalAmount) ?? sumLeafScaleCost(projectScaleSource.detailRows)
|
||||||
projectScale.push({
|
projectScale.push({
|
||||||
major: -1, cost: projectScaleCost,
|
major: -1, cost: projectScaleCost,
|
||||||
area: null
|
area: null
|
||||||
@ -1572,14 +1617,17 @@ const exportData = async () => {
|
|||||||
|
|
||||||
const exportReport = async () => {
|
const exportReport = async () => {
|
||||||
try {
|
try {
|
||||||
|
showReportExportProgress(10, '正在准备报表导出...')
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
showReportExportProgress(40, '正在汇总报表数据...')
|
||||||
const payload = await buildExportReportPayload()
|
const payload = await buildExportReportPayload()
|
||||||
|
showReportExportProgress(80, '正在生成并写出报表文件...')
|
||||||
const fileName = `${sanitizeFileNamePart(payload.name)}-报表-${formatExportTimestamp(now)}`
|
const fileName = `${sanitizeFileNamePart(payload.name)}-报表-${formatExportTimestamp(now)}`
|
||||||
console.log(payload)
|
|
||||||
await exportFile(fileName, payload)
|
await exportFile(fileName, payload)
|
||||||
|
finishReportExportProgress(true, '报表导出完成')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('export report failed:', error)
|
console.error('export report failed:', error)
|
||||||
// window.alert('导出报表失败,请重试。')
|
finishReportExportProgress(false, '报表导出失败,请重试')
|
||||||
} finally {
|
} finally {
|
||||||
dataMenuOpen.value = false
|
dataMenuOpen.value = false
|
||||||
}
|
}
|
||||||
@ -1710,6 +1758,7 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
clearReportExportToastTimer()
|
||||||
window.removeEventListener('mousedown', handleGlobalMouseDown)
|
window.removeEventListener('mousedown', handleGlobalMouseDown)
|
||||||
window.removeEventListener('keydown', handleGlobalKeyDown)
|
window.removeEventListener('keydown', handleGlobalKeyDown)
|
||||||
window.removeEventListener('resize', scheduleUpdateTabTitleOverflow)
|
window.removeEventListener('resize', scheduleUpdateTabTitleOverflow)
|
||||||
@ -1761,6 +1810,7 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<ToastProvider>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div class="flex flex-col w-full h-screen bg-background overflow-hidden">
|
<div class="flex flex-col w-full h-screen bg-background overflow-hidden">
|
||||||
<div class="grid grid-cols-[minmax(0,1fr)_auto] items-start gap-2 border-b bg-muted/30 px-2 pt-1 h-15 flex-none">
|
<div class="grid grid-cols-[minmax(0,1fr)_auto] items-start gap-2 border-b bg-muted/30 px-2 pt-1 h-15 flex-none">
|
||||||
@ -1966,8 +2016,30 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ToastRoot
|
||||||
|
:open="reportExportToastOpen"
|
||||||
|
:duration="0"
|
||||||
|
class="pointer-events-auto rounded-xl border border-border bg-card px-4 py-3 text-foreground shadow-lg"
|
||||||
|
>
|
||||||
|
<ToastTitle class="text-sm font-semibold text-foreground">
|
||||||
|
{{ reportExportStatus === 'running' ? '导出报表' : (reportExportStatus === 'success' ? '导出成功' : '导出失败') }}
|
||||||
|
</ToastTitle>
|
||||||
|
<ToastDescription class="mt-1 text-xs text-muted-foreground">{{ reportExportText }}</ToastDescription>
|
||||||
|
<div class="mt-2 h-1.5 w-full overflow-hidden rounded-full bg-muted">
|
||||||
|
<div
|
||||||
|
class="h-full transition-all duration-300"
|
||||||
|
:class="reportExportStatus === 'error'
|
||||||
|
? 'bg-red-500'
|
||||||
|
: (reportExportStatus === 'success' ? 'bg-foreground/70' : 'bg-foreground')"
|
||||||
|
:style="{ width: `${reportExportProgress}%` }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ToastRoot>
|
||||||
|
<ToastViewport class="fixed bottom-5 right-5 z-[85] flex w-[380px] max-w-[92vw] flex-col gap-2 outline-none" />
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
</ToastProvider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -30,5 +30,14 @@ export const sumByNumber = <T>(list: T[], pick: (item: T) => MaybeNumber) => {
|
|||||||
return total.toNumber()
|
return total.toNumber()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const decimalAggSum = (params: { values?: unknown[] }) =>
|
export const decimalAggSum = (params: { values?: unknown[] }) => {
|
||||||
sumFiniteValues(params.values || [])
|
const values = params.values || []
|
||||||
|
let hasFinite = false
|
||||||
|
for (const value of values) {
|
||||||
|
if (!isFiniteNumber(value)) continue
|
||||||
|
hasFinite = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (!hasFinite) return null
|
||||||
|
return sumFiniteValues(values)
|
||||||
|
}
|
||||||
|
|||||||
@ -164,6 +164,12 @@ const toStoredDetailRowsState = <TRow = unknown>(state: { detailRows?: TRow[] }
|
|||||||
const hasOwn = (obj: unknown, key: string) =>
|
const hasOwn = (obj: unknown, key: string) =>
|
||||||
Object.prototype.hasOwnProperty.call(obj || {}, key)
|
Object.prototype.hasOwnProperty.call(obj || {}, key)
|
||||||
|
|
||||||
|
const isGroupScaleRow = (row: unknown) =>
|
||||||
|
Boolean(row && typeof row === 'object' && (row as Record<string, unknown>).isGroupRow === true)
|
||||||
|
|
||||||
|
const stripGroupScaleRows = <TRow>(rows: TRow[] | undefined): TRow[] =>
|
||||||
|
(rows || []).filter(row => !isGroupScaleRow(row))
|
||||||
|
|
||||||
const getRowNumberOrFallback = (
|
const getRowNumberOrFallback = (
|
||||||
row: Record<string, unknown> | undefined,
|
row: Record<string, unknown> | undefined,
|
||||||
key: string,
|
key: string,
|
||||||
@ -306,8 +312,9 @@ const mergeScaleRows = (
|
|||||||
consultCategoryFactorMap?: Map<string, number | null>,
|
consultCategoryFactorMap?: Map<string, number | null>,
|
||||||
majorFactorMap?: Map<string, number | null>
|
majorFactorMap?: Map<string, number | null>
|
||||||
): ScaleRow[] => {
|
): ScaleRow[] => {
|
||||||
const dbValueMap = toRowMap(rowsFromDb)
|
const sourceRows = stripGroupScaleRows(rowsFromDb)
|
||||||
for (const row of rowsFromDb || []) {
|
const dbValueMap = toRowMap(sourceRows)
|
||||||
|
for (const row of sourceRows) {
|
||||||
const rowId = String(row.id)
|
const rowId = String(row.id)
|
||||||
const nextId = majorIdAliasMap.get(rowId)
|
const nextId = majorIdAliasMap.get(rowId)
|
||||||
if (nextId && !dbValueMap.has(nextId)) {
|
if (nextId && !dbValueMap.has(nextId)) {
|
||||||
@ -370,7 +377,7 @@ const getOnlyCostScaleBudgetFee = (
|
|||||||
industryId?: string | null
|
industryId?: string | null
|
||||||
) => {
|
) => {
|
||||||
const industryMajorEntry = getIndustryMajorEntryByIndustryId(industryId)
|
const industryMajorEntry = getIndustryMajorEntryByIndustryId(industryId)
|
||||||
const sourceRows = rowsFromDb || []
|
const sourceRows = stripGroupScaleRows(rowsFromDb)
|
||||||
const defaultConsultCategoryFactor =
|
const defaultConsultCategoryFactor =
|
||||||
consultCategoryFactorMap?.get(String(serviceId)) ?? getDefaultConsultCategoryFactor(serviceId)
|
consultCategoryFactorMap?.get(String(serviceId)) ?? getDefaultConsultCategoryFactor(serviceId)
|
||||||
const defaultMajorFactor =
|
const defaultMajorFactor =
|
||||||
@ -426,14 +433,15 @@ const buildOnlyCostScaleDetailRows = (
|
|||||||
majorFactorMap?: Map<string, number | null>,
|
majorFactorMap?: Map<string, number | null>,
|
||||||
industryId?: string | null
|
industryId?: string | null
|
||||||
) => {
|
) => {
|
||||||
const totalAmount = sumByNumberNullable(rowsFromDb || [], row =>
|
const sourceRows = stripGroupScaleRows(rowsFromDb)
|
||||||
|
const totalAmount = sumByNumberNullable(sourceRows, row =>
|
||||||
typeof row?.amount === 'number' && Number.isFinite(row.amount) ? row.amount : null
|
typeof row?.amount === 'number' && Number.isFinite(row.amount) ? row.amount : null
|
||||||
)
|
)
|
||||||
const industryMajorEntry = getIndustryMajorEntryByIndustryId(industryId)
|
const industryMajorEntry = getIndustryMajorEntryByIndustryId(industryId)
|
||||||
const onlyCostRowId = industryMajorEntry?.id || ONLY_COST_SCALE_ROW_ID
|
const onlyCostRowId = industryMajorEntry?.id || ONLY_COST_SCALE_ROW_ID
|
||||||
const onlyRow =
|
const onlyRow =
|
||||||
(rowsFromDb || []).find(row => String(row?.id || '') === ONLY_COST_SCALE_ROW_ID) ||
|
sourceRows.find(row => String(row?.id || '') === ONLY_COST_SCALE_ROW_ID) ||
|
||||||
(rowsFromDb || []).find(row => String(row?.id || '') === onlyCostRowId)
|
sourceRows.find(row => String(row?.id || '') === onlyCostRowId)
|
||||||
const consultCategoryFactor = getRowNumberOrFallback(
|
const consultCategoryFactor = getRowNumberOrFallback(
|
||||||
onlyRow,
|
onlyRow,
|
||||||
'consultCategoryFactor',
|
'consultCategoryFactor',
|
||||||
@ -598,7 +606,7 @@ const resolveScaleRows = (
|
|||||||
if (htData?.detailRows != null) {
|
if (htData?.detailRows != null) {
|
||||||
return mergeScaleRows(
|
return mergeScaleRows(
|
||||||
serviceId,
|
serviceId,
|
||||||
htData.detailRows as any,
|
stripGroupScaleRows(htData.detailRows as any),
|
||||||
consultCategoryFactorMap,
|
consultCategoryFactorMap,
|
||||||
majorFactorMap
|
majorFactorMap
|
||||||
)
|
)
|
||||||
|
|||||||
21
src/sql.ts
21
src/sql.ts
@ -2,7 +2,6 @@
|
|||||||
import { addNumbers, roundTo, toDecimal } from '@/lib/decimal'
|
import { addNumbers, roundTo, toDecimal } from '@/lib/decimal'
|
||||||
import { formatThousands } from '@/lib/numberFormat'
|
import { formatThousands } from '@/lib/numberFormat'
|
||||||
import ExcelJS from "ExcelJS";
|
import ExcelJS from "ExcelJS";
|
||||||
import { number } from 'motion-v/es';
|
|
||||||
// 统一数字千分位格式化,默认保留 2 位小数。
|
// 统一数字千分位格式化,默认保留 2 位小数。
|
||||||
const numberFormatter = (value: unknown, fractionDigits = 2) =>
|
const numberFormatter = (value: unknown, fractionDigits = 2) =>
|
||||||
formatThousands(value, fractionDigits)
|
formatThousands(value, fractionDigits)
|
||||||
@ -552,7 +551,6 @@ export async function exportFile(fileName: string, data: any): Promise<void> {
|
|||||||
// 按模板生成最终工作簿:填充封面、目录、各分表及汇总数据。
|
// 按模板生成最终工作簿:填充封面、目录、各分表及汇总数据。
|
||||||
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: '下标' }] };
|
// 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)
|
|
||||||
try {
|
try {
|
||||||
// 获取模板
|
// 获取模板
|
||||||
let templateExcel = 'template20260226001test010';
|
let templateExcel = 'template20260226001test010';
|
||||||
@ -724,7 +722,12 @@ async function generateTemplate(data) {
|
|||||||
sheet_5.getRow(3).getCell(3).value = '/';
|
sheet_5.getRow(3).getCell(3).value = '/';
|
||||||
sheet_5.getRow(3).getCell(4).value = '/';
|
sheet_5.getRow(3).getCell(4).value = '/';
|
||||||
sheet_5.getRow(3).getCell(5).value = '/';
|
sheet_5.getRow(3).getCell(5).value = '/';
|
||||||
sheet_5.getRow(3).getCell(6).value = numberFormatter((ci.method5.addtional?.reduce((a, b) => a + b.m5.fee, 0) || 0) + (ci.method5.reserve?.fee || 0), 2);
|
const method5AdditionalFee = ci.method5.addtional?.reduce((a, b) =>
|
||||||
|
addNumbers(a, toFiniteNumber(b?.m5?.fee)), 0) || 0;
|
||||||
|
sheet_5.getRow(3).getCell(6).value = numberFormatter(
|
||||||
|
addNumbers(method5AdditionalFee, toFiniteNumber(ci.method5.reserve?.fee)),
|
||||||
|
2
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新目录的第三部分
|
// 更新目录的第三部分
|
||||||
@ -772,22 +775,22 @@ async function generateTemplate(data) {
|
|||||||
targetRow.getCell(3).value = serviceX.name;
|
targetRow.getCell(3).value = serviceX.name;
|
||||||
if (sobj.method1) {
|
if (sobj.method1) {
|
||||||
targetRow.getCell(4).value = numberFormatter(sobj.method1.fee, 2);
|
targetRow.getCell(4).value = numberFormatter(sobj.method1.fee, 2);
|
||||||
m1Sum += sobj.method1.fee;
|
m1Sum = addNumbers(m1Sum, toFiniteNumber(sobj.method1.fee));
|
||||||
}
|
}
|
||||||
if (sobj.method2) {
|
if (sobj.method2) {
|
||||||
targetRow.getCell(5).value = numberFormatter(sobj.method2.fee, 2);
|
targetRow.getCell(5).value = numberFormatter(sobj.method2.fee, 2);
|
||||||
m2Sum += sobj.method2.fee;
|
m2Sum = addNumbers(m2Sum, toFiniteNumber(sobj.method2.fee));
|
||||||
}
|
}
|
||||||
if (sobj.method3) {
|
if (sobj.method3) {
|
||||||
targetRow.getCell(6).value = numberFormatter(sobj.method3.fee, 2);
|
targetRow.getCell(6).value = numberFormatter(sobj.method3.fee, 2);
|
||||||
m3Sum += sobj.method3.fee;
|
m3Sum = addNumbers(m3Sum, toFiniteNumber(sobj.method3.fee));
|
||||||
}
|
}
|
||||||
if (sobj.method4) {
|
if (sobj.method4) {
|
||||||
targetRow.getCell(7).value = numberFormatter(sobj.method4.fee, 2);
|
targetRow.getCell(7).value = numberFormatter(sobj.method4.fee, 2);
|
||||||
m4Sum += sobj.method4.fee;
|
m4Sum = addNumbers(m4Sum, toFiniteNumber(sobj.method4.fee));
|
||||||
}
|
}
|
||||||
targetRow.getCell(8).value = numberFormatter(sobj.fee, 2);
|
targetRow.getCell(8).value = numberFormatter(sobj.fee, 2);
|
||||||
serviceSum += sobj.fee;
|
serviceSum = addNumbers(serviceSum, toFiniteNumber(sobj.fee));
|
||||||
});
|
});
|
||||||
if (sobj.method1 || sobj.method2) {
|
if (sobj.method1 || sobj.method2) {
|
||||||
cusInsertRowFunc(4 + num_2, [sheet_2.getRow(4)], sheet_2, (targetRow) => {
|
cusInsertRowFunc(4 + num_2, [sheet_2.getRow(4)], sheet_2, (targetRow) => {
|
||||||
@ -1800,8 +1803,6 @@ async function generateTemplate(data) {
|
|||||||
descSheet.spliceRows(descRowNum, 5);
|
descSheet.spliceRows(descRowNum, 5);
|
||||||
descRowNum++;
|
descRowNum++;
|
||||||
}
|
}
|
||||||
console.log(descRowNum);
|
|
||||||
|
|
||||||
return workbook;
|
return workbook;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user