Compare commits

..

2 Commits

Author SHA1 Message Date
55e133852e fix toast Style issue 2026-03-18 10:42:42 +08:00
083c8f1886 fix 2026-03-18 10:17:41 +08:00
14 changed files with 175 additions and 64 deletions

View File

@ -11,7 +11,10 @@
"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)"
] ]
} }
} }

View File

@ -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-slate-800/90 bg-slate-900 px-4 py-3 text-white shadow-xl" 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"
> >
<div class="grid gap-1"> <div class="grid gap-1">
<ToastTitle class="text-sm font-semibold text-white">{{ toastTitle }}</ToastTitle> <ToastTitle class="text-sm font-semibold text-foreground">{{ toastTitle }}</ToastTitle>
<ToastDescription class="text-xs text-slate-100">{{ toastText }}</ToastDescription> <ToastDescription class="text-xs text-muted-foreground">{{ toastText }}</ToastDescription>
</div> </div>
<ToastAction <ToastAction
alt-text="知道了" alt-text="知道了"
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" 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"
@click="toastOpen = false" @click="toastOpen = false"
> >
知道了 知道了
</ToastAction> </ToastAction>
</ToastRoot> </ToastRoot>
<ToastViewport class="fixed bottom-5 right-5 z-[80] flex w-[380px] max-w-[92vw] flex-col gap-2 outline-none" /> <ToastViewport class="fixed bottom-5 right-5 z-[85] flex w-[380px] max-w-[92vw] flex-col gap-2 outline-none" />
</TooltipProvider> </TooltipProvider>
</ToastProvider> </ToastProvider>
</template> </template>

View File

@ -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 } from '@/lib/decimal' import { addNumbers, roundTo } from '@/lib/decimal'
import { parseNumberOrNull } from '@/lib/number' import { parseNumberOrNull } from '@/lib/number'
import { formatThousandsFlexible } from '@/lib/numberFormat' import { formatThousands, formatThousandsFlexible } from '@/lib/numberFormat'
import { import {
ensurePricingMethodDetailRowsForServices, ensurePricingMethodDetailRowsForServices,
getPricingMethodTotalsForService, getPricingMethodTotalsForService,
@ -51,6 +51,7 @@ 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
} }
@ -154,6 +155,7 @@ 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
})) }))
) )
@ -174,6 +176,7 @@ 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
})) }))
} }
@ -457,36 +460,21 @@ 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 => {
item.id !== row.id if (item.id !== row.id) return item
? item const newSubtotal = sumNullableNumbers([sanitizedTotals.investScale, sanitizedTotals.landScale, sanitizedTotals.workload, sanitizedTotals.hourly])
: { 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: nextRows detailRows: applyFixedRowTotals(clearedRows)
}) })
} }
@ -728,6 +716,35 @@ 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',
@ -791,7 +808,16 @@ 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')
return rows.map(row => // finalFee null
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,
@ -799,7 +825,8 @@ 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
) )
@ -856,12 +883,14 @@ 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
} }
}) })
@ -899,7 +928,8 @@ 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)
@ -918,6 +948,7 @@ 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
} }
@ -1092,6 +1123,7 @@ const initializeContractState = async () => {
workload: null, workload: null,
hourly: null, hourly: null,
subtotal: null, subtotal: null,
finalFee: null,
actions: null actions: null
}]) }])
}) })
@ -1117,7 +1149,20 @@ watch(serviceIdSignature, () => {
} }
}) })
const handleCellValueChanged = () => {} const handleCellValueChanged = async (event: any) => {
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()

View File

@ -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 ? 'editable-cell-line remark-wrap-cell' : ''), cellClass: params => (!params.node?.group && !params.node?.rowPinned ? ' 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 === '')

View File

@ -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 ? 'editable-cell-line remark-wrap-cell' : ''), cellClass: params => (!params.node?.group && !params.node?.rowPinned ? ' 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 === '')

View File

@ -386,7 +386,7 @@ const columnDefs: ColDef<DetailRow>[] = [
return params.value || '' return params.value || ''
}, },
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line remark-wrap-cell' : ''), cellClass: params => (!params.node?.group && !params.node?.rowPinned ? ' remark-wrap-cell' : ''),
cellClassRules: { cellClassRules: {
'editable-cell-empty': params => 'editable-cell-empty': params =>
!params.node?.group && !params.node?.group &&

View File

@ -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 ? 'editable-cell-line remark-wrap-cell' : ''), cellClass: params => (!params.node?.group && !params.node?.rowPinned ? ' 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 === '')

View File

@ -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: 'editable-cell-line remark-wrap-cell', cellClass: ' remark-wrap-cell',
cellClassRules: { cellClassRules: {
'editable-cell-empty': params => params.value == null || params.value === '' 'editable-cell-empty': params => params.value == null || params.value === ''
} }

View File

@ -205,13 +205,11 @@ 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
}, },
@ -233,7 +231,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: 'editable-cell-line remark-wrap-cell', cellClass: 'remark-wrap-cell',
cellClassRules: { cellClassRules: {
'editable-cell-empty': params => params.value == null || params.value === '' 'editable-cell-empty': params => params.value == null || params.value === ''
}, },

View File

@ -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: 'editable-cell-line remark-wrap-cell', cellClass: ' remark-wrap-cell',
cellClassRules: { cellClassRules: {
'editable-cell-empty': params => params.value == null || params.value === '' 'editable-cell-empty': params => params.value == null || params.value === ''
} }

View File

@ -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 { PROJECT_TAB_ID, QUICK_TAB_ID, readWorkspaceMode } from '@/lib/workspace' import { PROJECT_TAB_ID, QUICK_TAB_ID, readWorkspaceMode } from '@/lib/workspace'
@ -470,6 +475,37 @@ 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) => {
@ -1575,11 +1611,15 @@ 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)}`
console.log(payload) await exportFile(fileName, payload, () => {
await exportFile(fileName, payload) showReportExportProgress(30, '正在生成报表文件...')
})
finishReportExportProgress(true, '报表导出完成')
} catch (error) { } catch (error) {
console.error('export report failed:', error) console.error('export report failed:', error)
// window.alert('') if (reportExportToastOpen.value) {
finishReportExportProgress(false, '报表导出失败,请重试')
}
} finally { } finally {
dataMenuOpen.value = false dataMenuOpen.value = false
} }
@ -1760,6 +1800,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">
@ -1967,7 +2008,31 @@ 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>

View File

@ -198,7 +198,7 @@ useMotionValueEvent(
<template> <template>
<TooltipProvider> <TooltipProvider>
<div class="flex h-full w-full bg-background"> <div class="flex h-full w-full bg-background p-2">
<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-5 relative"> <div :class="['flex flex-col gap-6 relative', (props.title || props.subtitle || props.metaText) ? 'mt-4' : '']">
<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-[13px] leading-5 transition-colors duration-200', 'text-[12px] leading-4 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'

View File

@ -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): Promise<void> { export async function exportFile(fileName: string, data: any, onSaveConfirmed?: () => void): Promise<void> {
if (window.showSaveFilePicker) { if (window.showSaveFilePicker) {
const handle = await window.showSaveFilePicker({ const handle = await window.showSaveFilePicker({
suggestedName: fileName, suggestedName: fileName,
@ -697,10 +697,9 @@ export async function exportFile(fileName: string, data: any): Promise<void> {
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();

View File

@ -144,7 +144,8 @@ 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:not(.ag-cell-auto-height) .ag-cell-wrapper { .xmMx .ag-row .ag-cell-wrapper {
height: 100%; height: 100%;
} }