Compare commits
No commits in common. "55e133852ef8b7c5fee5997221521fcafe161c51" and "0f7deb0f1b5b6301358004fcef9d92fdabf15245" have entirely different histories.
55e133852e
...
0f7deb0f1b
@ -11,10 +11,7 @@
|
|||||||
"Bash(bun run:*)",
|
"Bash(bun run:*)",
|
||||||
"Bash(grep:*)",
|
"Bash(grep:*)",
|
||||||
"mcp__context7__resolve-library-id",
|
"mcp__context7__resolve-library-id",
|
||||||
"mcp__context7__query-docs",
|
"mcp__context7__query-docs"
|
||||||
"mcp__ag-mcp__detect_version",
|
|
||||||
"WebSearch",
|
|
||||||
"WebFetch(domain:reka-ui.com)"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1756,21 +1756,21 @@ watch(budgetRefreshSignature, (next, prev) => {
|
|||||||
<ToastRoot
|
<ToastRoot
|
||||||
v-model:open="toastOpen"
|
v-model:open="toastOpen"
|
||||||
:duration="1800"
|
:duration="1800"
|
||||||
class="group pointer-events-auto flex items-center gap-3 rounded-xl border border-border bg-card px-4 py-3 text-foreground shadow-lg"
|
class="group pointer-events-auto flex items-center gap-3 rounded-xl border border-slate-800/90 bg-slate-900 px-4 py-3 text-white shadow-xl"
|
||||||
>
|
>
|
||||||
<div class="grid gap-1">
|
<div class="grid gap-1">
|
||||||
<ToastTitle class="text-sm font-semibold text-foreground">{{ toastTitle }}</ToastTitle>
|
<ToastTitle class="text-sm font-semibold text-white">{{ toastTitle }}</ToastTitle>
|
||||||
<ToastDescription class="text-xs text-muted-foreground">{{ toastText }}</ToastDescription>
|
<ToastDescription class="text-xs text-slate-100">{{ toastText }}</ToastDescription>
|
||||||
</div>
|
</div>
|
||||||
<ToastAction
|
<ToastAction
|
||||||
alt-text="知道了"
|
alt-text="知道了"
|
||||||
class="ml-auto cursor-pointer inline-flex h-7 items-center rounded-md border border-border bg-muted px-2 text-xs text-foreground hover:bg-muted/80"
|
class="ml-auto inline-flex h-7 items-center rounded-md border border-white/30 bg-white/10 px-2 text-xs text-white hover:bg-white/20"
|
||||||
@click="toastOpen = false"
|
@click="toastOpen = false"
|
||||||
>
|
>
|
||||||
知道了
|
知道了
|
||||||
</ToastAction>
|
</ToastAction>
|
||||||
</ToastRoot>
|
</ToastRoot>
|
||||||
<ToastViewport class="fixed bottom-5 right-5 z-[85] flex w-[380px] max-w-[92vw] flex-col gap-2 outline-none" />
|
<ToastViewport class="fixed bottom-5 right-5 z-[80] flex w-[380px] max-w-[92vw] flex-col gap-2 outline-none" />
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -5,9 +5,9 @@ import { AgGridVue } from 'ag-grid-vue3'
|
|||||||
import type { ColDef, GridOptions, ICellRendererParams } from 'ag-grid-community'
|
import type { ColDef, GridOptions, ICellRendererParams } from 'ag-grid-community'
|
||||||
import { myTheme, gridOptions, agGridWrapClass, agGridStyle } from '@/lib/diyAgGridOptions'
|
import { myTheme, gridOptions, agGridWrapClass, agGridStyle } 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, roundTo } from '@/lib/decimal'
|
import { addNumbers } from '@/lib/decimal'
|
||||||
import { parseNumberOrNull } from '@/lib/number'
|
import { parseNumberOrNull } from '@/lib/number'
|
||||||
import { formatThousands, formatThousandsFlexible } from '@/lib/numberFormat'
|
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
||||||
import {
|
import {
|
||||||
ensurePricingMethodDetailRowsForServices,
|
ensurePricingMethodDetailRowsForServices,
|
||||||
getPricingMethodTotalsForService,
|
getPricingMethodTotalsForService,
|
||||||
@ -51,7 +51,6 @@ interface DetailRow {
|
|||||||
workload: number | null
|
workload: number | null
|
||||||
hourly: number | null
|
hourly: number | null
|
||||||
subtotal?: number | null
|
subtotal?: number | null
|
||||||
finalFee?: number | null
|
|
||||||
actions?: unknown
|
actions?: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,7 +154,6 @@ const detailRows = computed<DetailRow[]>(() =>
|
|||||||
workload: typeof row.workload === 'number' ? row.workload : null,
|
workload: typeof row.workload === 'number' ? row.workload : null,
|
||||||
hourly: typeof row.hourly === 'number' ? row.hourly : null,
|
hourly: typeof row.hourly === 'number' ? row.hourly : null,
|
||||||
subtotal: typeof row.subtotal === 'number' ? row.subtotal : null,
|
subtotal: typeof row.subtotal === 'number' ? row.subtotal : null,
|
||||||
finalFee: typeof (row as any).finalFee === 'number' ? (row as any).finalFee : null,
|
|
||||||
actions: row.actions
|
actions: row.actions
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
@ -176,7 +174,6 @@ const getCurrentContractState = (): ZxFwViewState => {
|
|||||||
workload: typeof row.workload === 'number' ? row.workload : null,
|
workload: typeof row.workload === 'number' ? row.workload : null,
|
||||||
hourly: typeof row.hourly === 'number' ? row.hourly : null,
|
hourly: typeof row.hourly === 'number' ? row.hourly : null,
|
||||||
subtotal: typeof row.subtotal === 'number' ? row.subtotal : null,
|
subtotal: typeof row.subtotal === 'number' ? row.subtotal : null,
|
||||||
finalFee: typeof (row as any).finalFee === 'number' ? (row as any).finalFee : null,
|
|
||||||
actions: row.actions
|
actions: row.actions
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@ -460,21 +457,36 @@ const clearRowValues = async (row: DetailRow) => {
|
|||||||
})
|
})
|
||||||
const sanitizedTotals = sanitizePricingTotalsByService(row.id, totals)
|
const sanitizedTotals = sanitizePricingTotalsByService(row.id, totals)
|
||||||
const currentState = getCurrentContractState()
|
const currentState = getCurrentContractState()
|
||||||
const clearedRows = currentState.detailRows.map(item => {
|
const clearedRows = currentState.detailRows.map(item =>
|
||||||
if (item.id !== row.id) return item
|
item.id !== row.id
|
||||||
const newSubtotal = sumNullableNumbers([sanitizedTotals.investScale, sanitizedTotals.landScale, sanitizedTotals.workload, sanitizedTotals.hourly])
|
? item
|
||||||
return {
|
: {
|
||||||
...item,
|
...item,
|
||||||
investScale: sanitizedTotals.investScale,
|
investScale: sanitizedTotals.investScale,
|
||||||
landScale: sanitizedTotals.landScale,
|
landScale: sanitizedTotals.landScale,
|
||||||
workload: sanitizedTotals.workload,
|
workload: sanitizedTotals.workload,
|
||||||
hourly: sanitizedTotals.hourly,
|
hourly: sanitizedTotals.hourly
|
||||||
finalFee: newSubtotal != null ? roundTo(newSubtotal, 2) : null
|
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
const nextInvestScale = getMethodTotalFromRows(clearedRows, 'investScale')
|
||||||
|
const nextLandScale = getMethodTotalFromRows(clearedRows, 'landScale')
|
||||||
|
const nextWorkload = getMethodTotalFromRows(clearedRows, 'workload')
|
||||||
|
const nextHourly = getMethodTotalFromRows(clearedRows, 'hourly')
|
||||||
|
const nextRows = clearedRows.map(item =>
|
||||||
|
isFixedRow(item)
|
||||||
|
? {
|
||||||
|
...item,
|
||||||
|
investScale: nextInvestScale,
|
||||||
|
landScale: nextLandScale,
|
||||||
|
workload: nextWorkload,
|
||||||
|
hourly: nextHourly,
|
||||||
|
subtotal: sumNullableNumbers([nextInvestScale, nextLandScale, nextWorkload, nextHourly])
|
||||||
|
}
|
||||||
|
: item
|
||||||
|
)
|
||||||
await setCurrentContractState({
|
await setCurrentContractState({
|
||||||
...currentState,
|
...currentState,
|
||||||
detailRows: applyFixedRowTotals(clearedRows)
|
detailRows: nextRows
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -716,35 +728,6 @@ const columnDefs: ColDef<DetailRow>[] = [
|
|||||||
},
|
},
|
||||||
valueFormatter: params => (params.value == null ? '' : formatThousandsFlexible(params.value, 3))
|
valueFormatter: params => (params.value == null ? '' : formatThousandsFlexible(params.value, 3))
|
||||||
},
|
},
|
||||||
{
|
|
||||||
headerName: '确认金额',
|
|
||||||
field: 'finalFee',
|
|
||||||
headerClass: 'ag-right-aligned-header',
|
|
||||||
flex: 3,
|
|
||||||
minWidth: 140,
|
|
||||||
cellClass: 'ag-right-aligned-cell',
|
|
||||||
editable: params => !isFixedRow(params.data),
|
|
||||||
valueGetter: params => {
|
|
||||||
if (!params.data) return null
|
|
||||||
if (isFixedRow(params.data)) {
|
|
||||||
return sumNullableNumbers(
|
|
||||||
detailRows.value.filter(r => !isFixedRow(r)).map(r => r.finalFee)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (params.data.finalFee != null) return params.data.finalFee
|
|
||||||
return sumNullableNumbers([
|
|
||||||
params.data.investScale,
|
|
||||||
params.data.landScale,
|
|
||||||
params.data.workload,
|
|
||||||
params.data.hourly
|
|
||||||
])
|
|
||||||
},
|
|
||||||
valueParser: params => {
|
|
||||||
const parsed = parseNumberOrNull(params.newValue, { precision: 2 })
|
|
||||||
return parsed != null ? roundTo(parsed, 2) : null
|
|
||||||
},
|
|
||||||
valueFormatter: params => (params.value == null ? '' : formatThousands(params.value, 2))
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
headerName: '操作',
|
headerName: '操作',
|
||||||
field: 'actions',
|
field: 'actions',
|
||||||
@ -808,16 +791,7 @@ const applyFixedRowTotals = (rows: DetailRow[]) => {
|
|||||||
const nextLandScale = getMethodTotalFromRows(rows, 'landScale')
|
const nextLandScale = getMethodTotalFromRows(rows, 'landScale')
|
||||||
const nextWorkload = getMethodTotalFromRows(rows, 'workload')
|
const nextWorkload = getMethodTotalFromRows(rows, 'workload')
|
||||||
const nextHourly = getMethodTotalFromRows(rows, 'hourly')
|
const nextHourly = getMethodTotalFromRows(rows, 'hourly')
|
||||||
// 先更新普通行:finalFee 跟随小计(若未手动编辑过或值为 null 则同步)
|
return rows.map(row =>
|
||||||
const updatedRows = rows.map(row => {
|
|
||||||
if (isFixedRow(row)) return row
|
|
||||||
const rowSubtotal = sumNullableNumbers([row.investScale, row.landScale, row.workload, row.hourly])
|
|
||||||
const nextFinalFee = row.finalFee != null ? row.finalFee : (rowSubtotal != null ? roundTo(rowSubtotal, 2) : null)
|
|
||||||
return { ...row, finalFee: nextFinalFee }
|
|
||||||
})
|
|
||||||
// 再计算固定行汇总
|
|
||||||
const totalFinalFee = sumNullableNumbers(updatedRows.filter(r => !isFixedRow(r)).map(r => r.finalFee))
|
|
||||||
return updatedRows.map(row =>
|
|
||||||
isFixedRow(row)
|
isFixedRow(row)
|
||||||
? {
|
? {
|
||||||
...row,
|
...row,
|
||||||
@ -825,8 +799,7 @@ const applyFixedRowTotals = (rows: DetailRow[]) => {
|
|||||||
landScale: nextLandScale,
|
landScale: nextLandScale,
|
||||||
workload: nextWorkload,
|
workload: nextWorkload,
|
||||||
hourly: nextHourly,
|
hourly: nextHourly,
|
||||||
subtotal: sumNullableNumbers([nextInvestScale, nextLandScale, nextWorkload, nextHourly]),
|
subtotal: sumNullableNumbers([nextInvestScale, nextLandScale, nextWorkload, nextHourly])
|
||||||
finalFee: totalFinalFee != null ? roundTo(totalFinalFee, 2) : null
|
|
||||||
}
|
}
|
||||||
: row
|
: row
|
||||||
)
|
)
|
||||||
@ -883,14 +856,12 @@ const fillPricingTotalsForServiceIds = async (serviceIds: string[]) => {
|
|||||||
const totalsRaw = totalsByServiceId.get(String(row.id))
|
const totalsRaw = totalsByServiceId.get(String(row.id))
|
||||||
const totals = totalsRaw ? sanitizePricingTotalsByService(String(row.id), totalsRaw) : null
|
const totals = totalsRaw ? sanitizePricingTotalsByService(String(row.id), totalsRaw) : null
|
||||||
if (!totals) return row
|
if (!totals) return row
|
||||||
const newSubtotal = sumNullableNumbers([totals.investScale, totals.landScale, totals.workload, totals.hourly])
|
|
||||||
return {
|
return {
|
||||||
...row,
|
...row,
|
||||||
investScale: totals.investScale,
|
investScale: totals.investScale,
|
||||||
landScale: totals.landScale,
|
landScale: totals.landScale,
|
||||||
workload: totals.workload,
|
workload: totals.workload,
|
||||||
hourly: totals.hourly,
|
hourly: totals.hourly
|
||||||
finalFee: newSubtotal != null ? roundTo(newSubtotal, 2) : null
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -928,8 +899,7 @@ const applySelection = async (codes: string[]) => {
|
|||||||
investScale: nextValues.investScale,
|
investScale: nextValues.investScale,
|
||||||
landScale: nextValues.landScale,
|
landScale: nextValues.landScale,
|
||||||
workload: nextValues.workload,
|
workload: nextValues.workload,
|
||||||
hourly: nextValues.hourly,
|
hourly: nextValues.hourly
|
||||||
finalFee: typeof old?.finalFee === 'number' ? old.finalFee : null
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter((row): row is DetailRow => row !== null)
|
.filter((row): row is DetailRow => row !== null)
|
||||||
@ -948,7 +918,6 @@ const applySelection = async (codes: string[]) => {
|
|||||||
workload: typeof fixedOld?.workload === 'number' ? fixedOld.workload : null,
|
workload: typeof fixedOld?.workload === 'number' ? fixedOld.workload : null,
|
||||||
hourly: typeof fixedOld?.hourly === 'number' ? fixedOld.hourly : null,
|
hourly: typeof fixedOld?.hourly === 'number' ? fixedOld.hourly : null,
|
||||||
subtotal: null,
|
subtotal: null,
|
||||||
finalFee: null,
|
|
||||||
actions: null
|
actions: null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1123,7 +1092,6 @@ const initializeContractState = async () => {
|
|||||||
workload: null,
|
workload: null,
|
||||||
hourly: null,
|
hourly: null,
|
||||||
subtotal: null,
|
subtotal: null,
|
||||||
finalFee: null,
|
|
||||||
actions: null
|
actions: null
|
||||||
}])
|
}])
|
||||||
})
|
})
|
||||||
@ -1149,20 +1117,7 @@ watch(serviceIdSignature, () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleCellValueChanged = async (event: any) => {
|
const handleCellValueChanged = () => {}
|
||||||
if (event.colDef?.field !== 'finalFee') return
|
|
||||||
const row = event.data as DetailRow | undefined
|
|
||||||
if (!row || isFixedRow(row)) return
|
|
||||||
const newValue = event.newValue != null ? roundTo(Number(event.newValue), 2) : null
|
|
||||||
const currentState = getCurrentContractState()
|
|
||||||
const nextRows = currentState.detailRows.map(item =>
|
|
||||||
item.id === row.id ? { ...item, finalFee: newValue } : item
|
|
||||||
)
|
|
||||||
await setCurrentContractState({
|
|
||||||
...currentState,
|
|
||||||
detailRows: applyFixedRowTotals(nextRows)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadProjectIndustry()
|
await loadProjectIndustry()
|
||||||
|
|||||||
@ -918,7 +918,7 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
if (!params.node?.group && !params.node?.rowPinned && !params.value) return '点击输入'
|
if (!params.node?.group && !params.node?.rowPinned && !params.value) return '点击输入'
|
||||||
return params.value || ''
|
return params.value || ''
|
||||||
},
|
},
|
||||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? ' remark-wrap-cell' : ''),
|
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line remark-wrap-cell' : ''),
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
'editable-cell-empty': params =>
|
'editable-cell-empty': params =>
|
||||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||||
|
|||||||
@ -772,7 +772,7 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
if (!params.node?.group && !params.node?.rowPinned && !params.value) return '点击输入'
|
if (!params.node?.group && !params.node?.rowPinned && !params.value) return '点击输入'
|
||||||
return params.value || ''
|
return params.value || ''
|
||||||
},
|
},
|
||||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? ' remark-wrap-cell' : ''),
|
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line remark-wrap-cell' : ''),
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
'editable-cell-empty': params =>
|
'editable-cell-empty': params =>
|
||||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||||
|
|||||||
@ -386,7 +386,7 @@ const columnDefs: ColDef<DetailRow>[] = [
|
|||||||
|
|
||||||
return params.value || ''
|
return params.value || ''
|
||||||
},
|
},
|
||||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? ' remark-wrap-cell' : ''),
|
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line remark-wrap-cell' : ''),
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
'editable-cell-empty': params =>
|
'editable-cell-empty': params =>
|
||||||
!params.node?.group &&
|
!params.node?.group &&
|
||||||
|
|||||||
@ -413,7 +413,7 @@ const columnDefs: (ColDef<DetailRow> | ColGroupDef<DetailRow>)[] = [
|
|||||||
if (!params.node?.group && !params.node?.rowPinned && !params.value) return '点击输入'
|
if (!params.node?.group && !params.node?.rowPinned && !params.value) return '点击输入'
|
||||||
return params.value || ''
|
return params.value || ''
|
||||||
},
|
},
|
||||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? ' remark-wrap-cell' : ''),
|
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line remark-wrap-cell' : ''),
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
'editable-cell-empty': params =>
|
'editable-cell-empty': params =>
|
||||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||||
|
|||||||
@ -351,7 +351,7 @@ const columnDefs: ColDef<FeeRow>[] = [
|
|||||||
autoHeight: true,
|
autoHeight: true,
|
||||||
cellStyle: { whiteSpace: 'normal', lineHeight: '1.4' },
|
cellStyle: { whiteSpace: 'normal', lineHeight: '1.4' },
|
||||||
valueFormatter: formatEditableText,
|
valueFormatter: formatEditableText,
|
||||||
cellClass: ' remark-wrap-cell',
|
cellClass: 'editable-cell-line remark-wrap-cell',
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
'editable-cell-empty': params => params.value == null || params.value === ''
|
'editable-cell-empty': params => params.value == null || params.value === ''
|
||||||
}
|
}
|
||||||
|
|||||||
@ -205,11 +205,13 @@ const columnDefs: ColDef<WorkContentRow>[] = [
|
|||||||
minWidth: 320,
|
minWidth: 320,
|
||||||
flex: 2,
|
flex: 2,
|
||||||
editable: params => Boolean(params.data?.custom),
|
editable: params => Boolean(params.data?.custom),
|
||||||
|
cellClass: params => (params.data?.custom ? 'editable-cell-line' : ''),
|
||||||
|
cellClassRules: {
|
||||||
|
'editable-cell-empty': params => Boolean(params.data?.custom) && (params.value == null || params.value === '')
|
||||||
|
},
|
||||||
valueParser: params => String(params.newValue || '').trim(),
|
valueParser: params => String(params.newValue || '').trim(),
|
||||||
wrapText: true,
|
wrapText: true,
|
||||||
autoHeight: true,
|
autoHeight: true,
|
||||||
|
|
||||||
cellStyle: { whiteSpace: 'normal', lineHeight: '1.5' },
|
cellStyle: { whiteSpace: 'normal', lineHeight: '1.5' },
|
||||||
cellRenderer: contentCellRenderer
|
cellRenderer: contentCellRenderer
|
||||||
},
|
},
|
||||||
@ -231,7 +233,7 @@ const columnDefs: ColDef<WorkContentRow>[] = [
|
|||||||
wrapText: true,
|
wrapText: true,
|
||||||
autoHeight: true,
|
autoHeight: true,
|
||||||
cellStyle: { whiteSpace: 'normal', lineHeight: '1.4' },
|
cellStyle: { whiteSpace: 'normal', lineHeight: '1.4' },
|
||||||
cellClass: 'remark-wrap-cell',
|
cellClass: 'editable-cell-line remark-wrap-cell',
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
'editable-cell-empty': params => params.value == null || params.value === ''
|
'editable-cell-empty': params => params.value == null || params.value === ''
|
||||||
},
|
},
|
||||||
|
|||||||
@ -201,7 +201,7 @@ const columnDefs: ColDef<FactorRow>[] = [
|
|||||||
cellStyle: { whiteSpace: 'normal', lineHeight: '1.4' },
|
cellStyle: { whiteSpace: 'normal', lineHeight: '1.4' },
|
||||||
editable: true,
|
editable: true,
|
||||||
valueFormatter: params => params.value || '点击输入',
|
valueFormatter: params => params.value || '点击输入',
|
||||||
cellClass: ' remark-wrap-cell',
|
cellClass: 'editable-cell-line remark-wrap-cell',
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
'editable-cell-empty': params => params.value == null || params.value === ''
|
'editable-cell-empty': params => params.value == null || params.value === ''
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,11 +20,6 @@ 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 { PROJECT_TAB_ID, QUICK_TAB_ID, readWorkspaceMode } from '@/lib/workspace'
|
import { PROJECT_TAB_ID, QUICK_TAB_ID, readWorkspaceMode } from '@/lib/workspace'
|
||||||
@ -475,37 +470,6 @@ 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
|
||||||
|
|
||||||
const reportExportToastOpen = ref(false)
|
|
||||||
const reportExportProgress = ref(0)
|
|
||||||
const reportExportStatus = ref<'running' | 'success' | 'error'>('running')
|
|
||||||
const reportExportText = ref('')
|
|
||||||
let reportExportToastTimer: ReturnType<typeof setTimeout> | null = null
|
|
||||||
|
|
||||||
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 tabsModel = computed({
|
const tabsModel = computed({
|
||||||
get: () => tabStore.tabs,
|
get: () => tabStore.tabs,
|
||||||
set: (value) => {
|
set: (value) => {
|
||||||
@ -1611,15 +1575,11 @@ const exportReport = async () => {
|
|||||||
const now = new Date()
|
const now = new Date()
|
||||||
const payload = await buildExportReportPayload()
|
const payload = await buildExportReportPayload()
|
||||||
const fileName = `${sanitizeFileNamePart(payload.name)}-报表-${formatExportTimestamp(now)}`
|
const fileName = `${sanitizeFileNamePart(payload.name)}-报表-${formatExportTimestamp(now)}`
|
||||||
await exportFile(fileName, payload, () => {
|
console.log(payload)
|
||||||
showReportExportProgress(30, '正在生成报表文件...')
|
await exportFile(fileName, payload)
|
||||||
})
|
|
||||||
finishReportExportProgress(true, '报表导出完成')
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('export report failed:', error)
|
console.error('export report failed:', error)
|
||||||
if (reportExportToastOpen.value) {
|
// window.alert('导出报表失败,请重试。')
|
||||||
finishReportExportProgress(false, '报表导出失败,请重试')
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
dataMenuOpen.value = false
|
dataMenuOpen.value = false
|
||||||
}
|
}
|
||||||
@ -1800,7 +1760,6 @@ 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">
|
||||||
@ -2008,31 +1967,7 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ToastRoot
|
|
||||||
v-model: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 flex items-center gap-2">
|
|
||||||
<div class="h-1.5 flex-1 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>
|
|
||||||
<span class="shrink-0 text-[11px] tabular-nums text-muted-foreground">{{ reportExportProgress }}%</span>
|
|
||||||
</div>
|
|
||||||
</ToastRoot>
|
|
||||||
<ToastViewport class="fixed bottom-5 right-5 z-[85] flex w-[380px] max-w-[92vw] flex-col gap-2 outline-none" />
|
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</ToastProvider>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -198,7 +198,7 @@ useMotionValueEvent(
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div class="flex h-full w-full bg-background p-2">
|
<div class="flex h-full w-full bg-background">
|
||||||
<div class="w-[200px] shrink-0 border-r px-4 py-3 flex flex-col gap-6 relative">
|
<div class="w-[200px] shrink-0 border-r px-4 py-3 flex flex-col gap-6 relative">
|
||||||
<div v-if="props.title || props.subtitle || props.metaText" class="space-y-1">
|
<div v-if="props.title || props.subtitle || props.metaText" class="space-y-1">
|
||||||
<TooltipRoot>
|
<TooltipRoot>
|
||||||
@ -228,7 +228,7 @@ useMotionValueEvent(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div :class="['flex flex-col gap-6 relative', (props.title || props.subtitle || props.metaText) ? 'mt-4' : '']">
|
<div class="flex flex-col gap-5 relative">
|
||||||
<div class="absolute left-[9px] top-3 bottom-3 w-[1.5px] bg-border/60"></div>
|
<div class="absolute left-[9px] top-3 bottom-3 w-[1.5px] bg-border/60"></div>
|
||||||
|
|
||||||
<div v-for="item in props.categories" :key="item.key"
|
<div v-for="item in props.categories" :key="item.key"
|
||||||
@ -242,7 +242,7 @@ useMotionValueEvent(
|
|||||||
<div v-if="activeCategory === item.key" class="w-1.5 h-1.5 bg-background rounded-full"></div>
|
<div v-if="activeCategory === item.key" class="w-1.5 h-1.5 bg-background rounded-full"></div>
|
||||||
</div>
|
</div>
|
||||||
<span :class="[
|
<span :class="[
|
||||||
'text-[12px] leading-4 transition-colors duration-200',
|
'text-[13px] leading-5 transition-colors duration-200',
|
||||||
activeCategory === item.key
|
activeCategory === item.key
|
||||||
? 'font-semibold text-primary'
|
? 'font-semibold text-primary'
|
||||||
: 'text-muted-foreground group-hover:text-foreground'
|
: 'text-muted-foreground group-hover:text-foreground'
|
||||||
|
|||||||
@ -688,7 +688,7 @@ export function getBasicFeeFromScale(
|
|||||||
* 导出入口:生成 Excel 并触发保存(优先使用 File System Access API)。
|
* 导出入口:生成 Excel 并触发保存(优先使用 File System Access API)。
|
||||||
* @returns 导出流程完成后的 Promise
|
* @returns 导出流程完成后的 Promise
|
||||||
*/
|
*/
|
||||||
export async function exportFile(fileName: string, data: any, onSaveConfirmed?: () => void): Promise<void> {
|
export async function exportFile(fileName: string, data: any): Promise<void> {
|
||||||
if (window.showSaveFilePicker) {
|
if (window.showSaveFilePicker) {
|
||||||
const handle = await window.showSaveFilePicker({
|
const handle = await window.showSaveFilePicker({
|
||||||
suggestedName: fileName,
|
suggestedName: fileName,
|
||||||
@ -697,9 +697,10 @@ export async function exportFile(fileName: string, data: any, onSaveConfirmed?:
|
|||||||
accept: { "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [".xlsx"] }
|
accept: { "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [".xlsx"] }
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
// ecCom.WeaLoadingGlobal.start({
|
||||||
|
// tip: "下载中,结束前请勿打开文件...",
|
||||||
|
// });
|
||||||
try {
|
try {
|
||||||
onSaveConfirmed?.()
|
|
||||||
|
|
||||||
const workbook = await generateTemplate(data);
|
const workbook = await generateTemplate(data);
|
||||||
const buffer = await workbook.xlsx.writeBuffer();
|
const buffer = await workbook.xlsx.writeBuffer();
|
||||||
const writable = await handle.createWritable();
|
const writable = await handle.createWritable();
|
||||||
|
|||||||
@ -144,8 +144,7 @@ html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* When one column uses auto-height rows, keep other columns vertically centered. */
|
/* When one column uses auto-height rows, keep other columns vertically centered. */
|
||||||
.xmMx .ag-row .ag-cell-wrapper {
|
.xmMx .ag-row .ag-cell:not(.ag-cell-auto-height) .ag-cell-wrapper {
|
||||||
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user