fix
This commit is contained in:
1 parent
757de9a43f
commit
3950057707
17 files changed
+2681
-202
No files matched your search
@@ -10,6 +10,14 @@ import { useTabStore } from '@/pinia/tab'
|
||||
import { ArrowUp, Edit3, GripVertical, MoreHorizontal, Plus, Trash2, X } from 'lucide-vue-next'
|
||||
import { decodeZwArchive, encodeZwArchive } from '@/lib/zwArchive'
|
||||
import {
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogPortal,
|
||||
AlertDialogRoot,
|
||||
AlertDialogTitle,
|
||||
ToastAction,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
@@ -64,6 +72,8 @@ const editingContractId = ref<string | null>(null)
|
||||
const toastOpen = ref(false)
|
||||
const toastTitle = ref('操作成功')
|
||||
const toastText = ref('')
|
||||
const deleteConfirmOpen = ref(false)
|
||||
const pendingDeleteContractId = ref<string | null>(null)
|
||||
const modalOffset = ref({ x: 0, y: 0 })
|
||||
let dragStartX = 0
|
||||
let dragStartY = 0
|
||||
@@ -125,6 +135,30 @@ const notify = (text: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
const pendingDeleteContractName = computed(() => {
|
||||
if (!pendingDeleteContractId.value) return ''
|
||||
const target = contracts.value.find(item => item.id === pendingDeleteContractId.value)
|
||||
return target?.name || pendingDeleteContractId.value
|
||||
})
|
||||
|
||||
const handleDeleteConfirmOpenChange = (open: boolean) => {
|
||||
deleteConfirmOpen.value = open
|
||||
if (!open) pendingDeleteContractId.value = null
|
||||
}
|
||||
|
||||
const requestDeleteContract = (id: string) => {
|
||||
pendingDeleteContractId.value = id
|
||||
deleteConfirmOpen.value = true
|
||||
}
|
||||
|
||||
const confirmDeleteContract = async () => {
|
||||
const id = pendingDeleteContractId.value
|
||||
if (!id) return
|
||||
await deleteContract(id)
|
||||
deleteConfirmOpen.value = false
|
||||
pendingDeleteContractId.value = null
|
||||
}
|
||||
|
||||
const closeContractDataMenu = () => {
|
||||
contractDataMenuOpen.value = false
|
||||
}
|
||||
@@ -945,7 +979,7 @@ onBeforeUnmount(() => {
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
:class="[isListLayout ? 'h-6 w-6' : 'h-7 w-7', 'text-destructive']"
|
||||
@click.stop="deleteContract(element.id)"
|
||||
@click.stop="requestDeleteContract(element.id)"
|
||||
>
|
||||
<Trash2 :class="isListLayout ? 'h-3.5 w-3.5' : 'h-4 w-4'" />
|
||||
</Button>
|
||||
@@ -1063,7 +1097,7 @@ onBeforeUnmount(() => {
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
:class="[isListLayout ? 'h-6 w-6' : 'h-7 w-7', 'text-destructive']"
|
||||
@click.stop="deleteContract(element.id)"
|
||||
@click.stop="requestDeleteContract(element.id)"
|
||||
>
|
||||
<Trash2 :class="isListLayout ? 'h-3.5 w-3.5' : 'h-4 w-4'" />
|
||||
</Button>
|
||||
@@ -1150,6 +1184,25 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AlertDialogRoot :open="deleteConfirmOpen" @update:open="handleDeleteConfirmOpenChange">
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" />
|
||||
<AlertDialogContent class="fixed left-1/2 top-1/2 z-[70] w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl">
|
||||
<AlertDialogTitle class="text-base font-semibold">确认删除合同段</AlertDialogTitle>
|
||||
<AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
|
||||
即将删除“{{ pendingDeleteContractName }}”及其关联咨询服务和计价数据,是否继续?
|
||||
</AlertDialogDescription>
|
||||
<div class="mt-4 flex items-center justify-end gap-2">
|
||||
<AlertDialogCancel as-child>
|
||||
<Button variant="outline">取消</Button>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction as-child>
|
||||
<Button variant="destructive" @click="confirmDeleteContract">确认删除</Button>
|
||||
</AlertDialogAction>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</AlertDialogRoot>
|
||||
<ToastRoot
|
||||
v-model:open="toastOpen"
|
||||
:duration="1800"
|
||||
|
||||
@@ -40,13 +40,27 @@ const DB_KEY = computed(() => `hourlyPricing-${props.contractId}-${props.service
|
||||
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
|
||||
const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
|
||||
const pricingPaneReloadStore = usePricingPaneReloadStore()
|
||||
const paneInstanceCreatedAt = Date.now()
|
||||
|
||||
const shouldSkipPersist = () => {
|
||||
const storageKey = `${PRICING_CLEAR_SKIP_PREFIX}${DB_KEY.value}`
|
||||
const raw = sessionStorage.getItem(storageKey)
|
||||
if (!raw) return false
|
||||
const now = Date.now()
|
||||
|
||||
if (raw.includes(':')) {
|
||||
const [issuedRaw, untilRaw] = raw.split(':')
|
||||
const issuedAt = Number(issuedRaw)
|
||||
const skipUntil = Number(untilRaw)
|
||||
if (Number.isFinite(issuedAt) && Number.isFinite(skipUntil) && now <= skipUntil) {
|
||||
return paneInstanceCreatedAt <= issuedAt
|
||||
}
|
||||
sessionStorage.removeItem(storageKey)
|
||||
return false
|
||||
}
|
||||
|
||||
const skipUntil = Number(raw)
|
||||
if (Number.isFinite(skipUntil) && Date.now() <= skipUntil) return true
|
||||
if (Number.isFinite(skipUntil) && now <= skipUntil) return true
|
||||
sessionStorage.removeItem(storageKey)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -10,6 +10,18 @@ import { formatThousands } from '@/lib/numberFormat'
|
||||
import { syncPricingTotalToZxFw, ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
|
||||
import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
||||
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogPortal,
|
||||
AlertDialogRoot,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from 'reka-ui'
|
||||
|
||||
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale';
|
||||
// 精简的边框配置(细线条+浅灰色,弱化分割线视觉)
|
||||
@@ -59,14 +71,14 @@ const pricingPaneReloadStore = usePricingPaneReloadStore()
|
||||
const consultCategoryFactorMap = ref<Map<string, number | null>>(new Map())
|
||||
const majorFactorMap = ref<Map<string, number | null>>(new Map())
|
||||
let factorDefaultsLoaded = false
|
||||
const paneInstanceCreatedAt = Date.now()
|
||||
|
||||
const getDefaultConsultCategoryFactor = () =>
|
||||
consultCategoryFactorMap.value.get(String(props.serviceId)) ?? null
|
||||
|
||||
const getDefaultMajorFactorById = (id: string): number | null => majorFactorMap.value.get(id) ?? null
|
||||
|
||||
const ensureFactorDefaultsLoaded = async () => {
|
||||
if (factorDefaultsLoaded) return
|
||||
const loadFactorDefaults = async () => {
|
||||
const [consultMap, majorMap] = await Promise.all([
|
||||
loadConsultCategoryFactorMap(),
|
||||
loadMajorFactorMap()
|
||||
@@ -76,12 +88,30 @@ const ensureFactorDefaultsLoaded = async () => {
|
||||
factorDefaultsLoaded = true
|
||||
}
|
||||
|
||||
const ensureFactorDefaultsLoaded = async () => {
|
||||
if (factorDefaultsLoaded) return
|
||||
await loadFactorDefaults()
|
||||
}
|
||||
|
||||
const shouldSkipPersist = () => {
|
||||
const storageKey = `${PRICING_CLEAR_SKIP_PREFIX}${DB_KEY.value}`
|
||||
const raw = sessionStorage.getItem(storageKey)
|
||||
if (!raw) return false
|
||||
const now = Date.now()
|
||||
|
||||
if (raw.includes(':')) {
|
||||
const [issuedRaw, untilRaw] = raw.split(':')
|
||||
const issuedAt = Number(issuedRaw)
|
||||
const skipUntil = Number(untilRaw)
|
||||
if (Number.isFinite(issuedAt) && Number.isFinite(skipUntil) && now <= skipUntil) {
|
||||
return paneInstanceCreatedAt <= issuedAt
|
||||
}
|
||||
sessionStorage.removeItem(storageKey)
|
||||
return false
|
||||
}
|
||||
|
||||
const skipUntil = Number(raw)
|
||||
if (Number.isFinite(skipUntil) && Date.now() <= skipUntil) return true
|
||||
if (Number.isFinite(skipUntil) && now <= skipUntil) return true
|
||||
sessionStorage.removeItem(storageKey)
|
||||
return false
|
||||
}
|
||||
@@ -165,8 +195,8 @@ const buildDefaultRows = (): DetailRow[] => {
|
||||
majorName: child.name,
|
||||
amount: null,
|
||||
benchmarkBudget: null,
|
||||
consultCategoryFactor: getDefaultConsultCategoryFactor(),
|
||||
majorFactor: getDefaultMajorFactorById(child.id),
|
||||
consultCategoryFactor: null,
|
||||
majorFactor: null,
|
||||
budgetFee: null,
|
||||
remark: '',
|
||||
path: [group.id, child.id]
|
||||
@@ -177,7 +207,12 @@ const buildDefaultRows = (): DetailRow[] => {
|
||||
}
|
||||
|
||||
type SourceRow = Pick<DetailRow, 'id'> & Partial<Pick<DetailRow, 'amount' | 'benchmarkBudget' | 'consultCategoryFactor' | 'majorFactor' | 'budgetFee' | 'remark'>>
|
||||
const mergeWithDictRows = (rowsFromDb: SourceRow[] | undefined): DetailRow[] => {
|
||||
const mergeWithDictRows = (
|
||||
rowsFromDb: SourceRow[] | undefined,
|
||||
options?: { includeAmount?: boolean; includeFactorValues?: boolean }
|
||||
): DetailRow[] => {
|
||||
const includeAmount = options?.includeAmount ?? true
|
||||
const includeFactorValues = options?.includeFactorValues ?? true
|
||||
const dbValueMap = new Map<string, SourceRow>()
|
||||
for (const row of rowsFromDb || []) {
|
||||
dbValueMap.set(row.id, row)
|
||||
@@ -191,20 +226,24 @@ const mergeWithDictRows = (rowsFromDb: SourceRow[] | undefined): DetailRow[] =>
|
||||
|
||||
return {
|
||||
...row,
|
||||
amount: typeof fromDb.amount === 'number' ? fromDb.amount : null,
|
||||
amount: includeAmount && typeof fromDb.amount === 'number' ? fromDb.amount : null,
|
||||
benchmarkBudget: typeof fromDb.benchmarkBudget === 'number' ? fromDb.benchmarkBudget : null,
|
||||
consultCategoryFactor:
|
||||
typeof fromDb.consultCategoryFactor === 'number'
|
||||
? fromDb.consultCategoryFactor
|
||||
: hasConsultCategoryFactor
|
||||
? null
|
||||
: getDefaultConsultCategoryFactor(),
|
||||
!includeFactorValues
|
||||
? null
|
||||
: typeof fromDb.consultCategoryFactor === 'number'
|
||||
? fromDb.consultCategoryFactor
|
||||
: hasConsultCategoryFactor
|
||||
? null
|
||||
: getDefaultConsultCategoryFactor(),
|
||||
majorFactor:
|
||||
typeof fromDb.majorFactor === 'number'
|
||||
? fromDb.majorFactor
|
||||
: hasMajorFactor
|
||||
? null
|
||||
: getDefaultMajorFactorById(row.id),
|
||||
!includeFactorValues
|
||||
? null
|
||||
: typeof fromDb.majorFactor === 'number'
|
||||
? fromDb.majorFactor
|
||||
: hasMajorFactor
|
||||
? null
|
||||
: getDefaultMajorFactorById(row.id),
|
||||
budgetFee: typeof fromDb.budgetFee === 'number' ? fromDb.budgetFee : null,
|
||||
remark: typeof fromDb.remark === 'string' ? fromDb.remark : ''
|
||||
}
|
||||
@@ -226,21 +265,10 @@ const formatEditableNumber = (params: any) => {
|
||||
}
|
||||
|
||||
const formatConsultCategoryFactor = (params: any) => {
|
||||
if (params.node?.group) {
|
||||
const v = getDefaultConsultCategoryFactor()
|
||||
if (v == null) return ''
|
||||
return Number(v).toFixed(2)
|
||||
}
|
||||
return formatEditableNumber(params)
|
||||
}
|
||||
|
||||
const formatMajorFactor = (params: any) => {
|
||||
if (params.node?.group) {
|
||||
const groupId = String(params.node?.key || '')
|
||||
const v = getDefaultMajorFactorById(groupId)
|
||||
if (v == null) return ''
|
||||
return Number(v).toFixed(2)
|
||||
}
|
||||
return formatEditableNumber(params)
|
||||
}
|
||||
|
||||
@@ -438,10 +466,13 @@ const saveToIndexedDB = async () => {
|
||||
|
||||
const loadFromIndexedDB = async () => {
|
||||
try {
|
||||
|
||||
await ensureFactorDefaultsLoaded()
|
||||
if (shouldForceDefaultLoad()) {
|
||||
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
||||
detailRows.value = htData?.detailRows ? mergeWithDictRows(htData.detailRows) : buildDefaultRows()
|
||||
detailRows.value = htData?.detailRows
|
||||
? mergeWithDictRows(htData.detailRows, { includeAmount: false, includeFactorValues: false })
|
||||
: buildDefaultRows()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -453,7 +484,7 @@ const loadFromIndexedDB = async () => {
|
||||
|
||||
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
||||
if (htData?.detailRows) {
|
||||
detailRows.value = mergeWithDictRows(htData.detailRows)
|
||||
detailRows.value = mergeWithDictRows(htData.detailRows, { includeAmount: false, includeFactorValues: false })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -464,6 +495,25 @@ const loadFromIndexedDB = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const importContractData = async () => {
|
||||
try {
|
||||
// 使用默认数据时,强制读取最新的项目系数(预算取值优先,空值回退标准系数)
|
||||
await loadFactorDefaults()
|
||||
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
||||
const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
|
||||
detailRows.value = hasContractRows
|
||||
? mergeWithDictRows(htData!.detailRows, { includeFactorValues: true })
|
||||
: buildDefaultRows().map(row => ({
|
||||
...row,
|
||||
consultCategoryFactor: getDefaultConsultCategoryFactor(),
|
||||
majorFactor: getDefaultMajorFactorById(row.id)
|
||||
}))
|
||||
await saveToIndexedDB()
|
||||
} catch (error) {
|
||||
console.error('importContractData failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => pricingPaneReloadStore.getReloadVersion(props.contractId, props.serviceId),
|
||||
(nextVersion, prevVersion) => {
|
||||
@@ -521,7 +571,28 @@ const processCellFromClipboard = (params: any) => {
|
||||
<div class="rounded-lg border bg-card xmMx flex min-h-0 flex-1 flex-col">
|
||||
<div class="flex items-center justify-between border-b px-4 py-3">
|
||||
<h3 class="text-sm font-semibold text-foreground">投资规模明细</h3>
|
||||
<div class="text-xs text-muted-foreground">导入导出</div>
|
||||
<AlertDialogRoot>
|
||||
<AlertDialogTrigger as-child>
|
||||
<Button type="button" variant="outline" size="sm">使用默认数据</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" />
|
||||
<AlertDialogContent class="fixed left-1/2 top-1/2 z-50 w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl">
|
||||
<AlertDialogTitle class="text-base font-semibold">确认覆盖当前明细</AlertDialogTitle>
|
||||
<AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
|
||||
将使用合同默认数据覆盖当前投资规模明细,是否继续?
|
||||
</AlertDialogDescription>
|
||||
<div class="mt-4 flex items-center justify-end gap-2">
|
||||
<AlertDialogCancel as-child>
|
||||
<Button variant="outline">取消</Button>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction as-child>
|
||||
<Button @click="importContractData">确认覆盖</Button>
|
||||
</AlertDialogAction>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</AlertDialogRoot>
|
||||
</div>
|
||||
|
||||
<div class="ag-theme-quartz h-full min-h-0 w-full flex-1">
|
||||
|
||||
@@ -10,6 +10,18 @@ import { formatThousands } from '@/lib/numberFormat'
|
||||
import { syncPricingTotalToZxFw, ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
|
||||
import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
||||
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogPortal,
|
||||
AlertDialogRoot,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from 'reka-ui'
|
||||
|
||||
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale';
|
||||
// 精简的边框配置(细线条+浅灰色,弱化分割线视觉)
|
||||
@@ -60,6 +72,7 @@ const pricingPaneReloadStore = usePricingPaneReloadStore()
|
||||
const consultCategoryFactorMap = ref<Map<string, number | null>>(new Map())
|
||||
const majorFactorMap = ref<Map<string, number | null>>(new Map())
|
||||
let factorDefaultsLoaded = false
|
||||
const paneInstanceCreatedAt = Date.now()
|
||||
|
||||
const detailRows = ref<DetailRow[]>([])
|
||||
const getDefaultConsultCategoryFactor = () =>
|
||||
@@ -67,8 +80,7 @@ const getDefaultConsultCategoryFactor = () =>
|
||||
|
||||
const getDefaultMajorFactorById = (id: string): number | null => majorFactorMap.value.get(id) ?? null
|
||||
|
||||
const ensureFactorDefaultsLoaded = async () => {
|
||||
if (factorDefaultsLoaded) return
|
||||
const loadFactorDefaults = async () => {
|
||||
const [consultMap, majorMap] = await Promise.all([
|
||||
loadConsultCategoryFactorMap(),
|
||||
loadMajorFactorMap()
|
||||
@@ -78,6 +90,11 @@ const ensureFactorDefaultsLoaded = async () => {
|
||||
factorDefaultsLoaded = true
|
||||
}
|
||||
|
||||
const ensureFactorDefaultsLoaded = async () => {
|
||||
if (factorDefaultsLoaded) return
|
||||
await loadFactorDefaults()
|
||||
}
|
||||
|
||||
const shouldForceDefaultLoad = () => {
|
||||
const storageKey = `${PRICING_FORCE_DEFAULT_PREFIX}${DB_KEY.value}`
|
||||
const raw = sessionStorage.getItem(storageKey)
|
||||
@@ -91,8 +108,21 @@ const shouldSkipPersist = () => {
|
||||
const storageKey = `${PRICING_CLEAR_SKIP_PREFIX}${DB_KEY.value}`
|
||||
const raw = sessionStorage.getItem(storageKey)
|
||||
if (!raw) return false
|
||||
const now = Date.now()
|
||||
|
||||
if (raw.includes(':')) {
|
||||
const [issuedRaw, untilRaw] = raw.split(':')
|
||||
const issuedAt = Number(issuedRaw)
|
||||
const skipUntil = Number(untilRaw)
|
||||
if (Number.isFinite(issuedAt) && Number.isFinite(skipUntil) && now <= skipUntil) {
|
||||
return paneInstanceCreatedAt <= issuedAt
|
||||
}
|
||||
sessionStorage.removeItem(storageKey)
|
||||
return false
|
||||
}
|
||||
|
||||
const skipUntil = Number(raw)
|
||||
if (Number.isFinite(skipUntil) && Date.now() <= skipUntil) return true
|
||||
if (Number.isFinite(skipUntil) && now <= skipUntil) return true
|
||||
sessionStorage.removeItem(storageKey)
|
||||
return false
|
||||
}
|
||||
@@ -167,8 +197,8 @@ const buildDefaultRows = (): DetailRow[] => {
|
||||
amount: null,
|
||||
landArea: null,
|
||||
benchmarkBudget: null,
|
||||
consultCategoryFactor: getDefaultConsultCategoryFactor(),
|
||||
majorFactor: getDefaultMajorFactorById(child.id),
|
||||
consultCategoryFactor: null,
|
||||
majorFactor: null,
|
||||
budgetFee: null,
|
||||
remark: '',
|
||||
path: [group.id, child.id]
|
||||
@@ -179,7 +209,12 @@ const buildDefaultRows = (): DetailRow[] => {
|
||||
}
|
||||
|
||||
type SourceRow = Pick<DetailRow, 'id'> & Partial<Pick<DetailRow, 'amount' | 'landArea' | 'benchmarkBudget' | 'consultCategoryFactor' | 'majorFactor' | 'budgetFee' | 'remark'>>
|
||||
const mergeWithDictRows = (rowsFromDb: SourceRow[] | undefined): DetailRow[] => {
|
||||
const mergeWithDictRows = (
|
||||
rowsFromDb: SourceRow[] | undefined,
|
||||
options?: { includeScaleValues?: boolean; includeFactorValues?: boolean }
|
||||
): DetailRow[] => {
|
||||
const includeScaleValues = options?.includeScaleValues ?? true
|
||||
const includeFactorValues = options?.includeFactorValues ?? true
|
||||
const dbValueMap = new Map<string, SourceRow>()
|
||||
for (const row of rowsFromDb || []) {
|
||||
dbValueMap.set(row.id, row)
|
||||
@@ -193,21 +228,25 @@ const mergeWithDictRows = (rowsFromDb: SourceRow[] | undefined): DetailRow[] =>
|
||||
|
||||
return {
|
||||
...row,
|
||||
amount: typeof fromDb.amount === 'number' ? fromDb.amount : null,
|
||||
landArea: typeof fromDb.landArea === 'number' ? fromDb.landArea : null,
|
||||
amount: includeScaleValues && typeof fromDb.amount === 'number' ? fromDb.amount : null,
|
||||
landArea: includeScaleValues && typeof fromDb.landArea === 'number' ? fromDb.landArea : null,
|
||||
benchmarkBudget: typeof fromDb.benchmarkBudget === 'number' ? fromDb.benchmarkBudget : null,
|
||||
consultCategoryFactor:
|
||||
typeof fromDb.consultCategoryFactor === 'number'
|
||||
? fromDb.consultCategoryFactor
|
||||
: hasConsultCategoryFactor
|
||||
? null
|
||||
: getDefaultConsultCategoryFactor(),
|
||||
!includeFactorValues
|
||||
? null
|
||||
: typeof fromDb.consultCategoryFactor === 'number'
|
||||
? fromDb.consultCategoryFactor
|
||||
: hasConsultCategoryFactor
|
||||
? null
|
||||
: getDefaultConsultCategoryFactor(),
|
||||
majorFactor:
|
||||
typeof fromDb.majorFactor === 'number'
|
||||
? fromDb.majorFactor
|
||||
: hasMajorFactor
|
||||
? null
|
||||
: getDefaultMajorFactorById(row.id),
|
||||
!includeFactorValues
|
||||
? null
|
||||
: typeof fromDb.majorFactor === 'number'
|
||||
? fromDb.majorFactor
|
||||
: hasMajorFactor
|
||||
? null
|
||||
: getDefaultMajorFactorById(row.id),
|
||||
budgetFee: typeof fromDb.budgetFee === 'number' ? fromDb.budgetFee : null,
|
||||
remark: typeof fromDb.remark === 'string' ? fromDb.remark : ''
|
||||
}
|
||||
@@ -229,21 +268,10 @@ const formatEditableNumber = (params: any) => {
|
||||
}
|
||||
|
||||
const formatConsultCategoryFactor = (params: any) => {
|
||||
if (params.node?.group) {
|
||||
const v = getDefaultConsultCategoryFactor()
|
||||
if (v == null) return ''
|
||||
return Number(v).toFixed(2)
|
||||
}
|
||||
return formatEditableNumber(params)
|
||||
}
|
||||
|
||||
const formatMajorFactor = (params: any) => {
|
||||
if (params.node?.group) {
|
||||
const groupId = String(params.node?.key || '')
|
||||
const v = getDefaultMajorFactorById(groupId)
|
||||
if (v == null) return ''
|
||||
return Number(v).toFixed(2)
|
||||
}
|
||||
return formatEditableNumber(params)
|
||||
}
|
||||
|
||||
@@ -444,7 +472,9 @@ const loadFromIndexedDB = async () => {
|
||||
await ensureFactorDefaultsLoaded()
|
||||
if (shouldForceDefaultLoad()) {
|
||||
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
||||
detailRows.value = htData?.detailRows ? mergeWithDictRows(htData.detailRows) : buildDefaultRows()
|
||||
detailRows.value = htData?.detailRows
|
||||
? mergeWithDictRows(htData.detailRows, { includeScaleValues: false, includeFactorValues: false })
|
||||
: buildDefaultRows()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -456,7 +486,7 @@ const loadFromIndexedDB = async () => {
|
||||
|
||||
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
||||
if (htData?.detailRows) {
|
||||
detailRows.value = mergeWithDictRows(htData.detailRows)
|
||||
detailRows.value = mergeWithDictRows(htData.detailRows, { includeScaleValues: false, includeFactorValues: false })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -467,6 +497,25 @@ const loadFromIndexedDB = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const importContractData = async () => {
|
||||
try {
|
||||
// 使用默认数据时,强制读取最新的项目系数(预算取值优先,空值回退标准系数)
|
||||
await loadFactorDefaults()
|
||||
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
||||
const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
|
||||
detailRows.value = hasContractRows
|
||||
? mergeWithDictRows(htData!.detailRows, { includeFactorValues: true })
|
||||
: buildDefaultRows().map(row => ({
|
||||
...row,
|
||||
consultCategoryFactor: getDefaultConsultCategoryFactor(),
|
||||
majorFactor: getDefaultMajorFactorById(row.id)
|
||||
}))
|
||||
await saveToIndexedDB()
|
||||
} catch (error) {
|
||||
console.error('importContractData failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => pricingPaneReloadStore.getReloadVersion(props.contractId, props.serviceId),
|
||||
(nextVersion, prevVersion) => {
|
||||
@@ -524,7 +573,28 @@ const processCellFromClipboard = (params: any) => {
|
||||
<div class="rounded-lg border bg-card xmMx flex min-h-0 flex-1 flex-col">
|
||||
<div class="flex items-center justify-between border-b px-4 py-3">
|
||||
<h3 class="text-sm font-semibold text-foreground">用地规模明细</h3>
|
||||
<div class="text-xs text-muted-foreground">导入导出</div>
|
||||
<AlertDialogRoot>
|
||||
<AlertDialogTrigger as-child>
|
||||
<Button type="button" variant="outline" size="sm">使用默认数据</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" />
|
||||
<AlertDialogContent class="fixed left-1/2 top-1/2 z-50 w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl">
|
||||
<AlertDialogTitle class="text-base font-semibold">确认覆盖当前明细</AlertDialogTitle>
|
||||
<AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
|
||||
将使用合同默认数据覆盖当前用地规模明细,是否继续?
|
||||
</AlertDialogDescription>
|
||||
<div class="mt-4 flex items-center justify-end gap-2">
|
||||
<AlertDialogCancel as-child>
|
||||
<Button variant="outline">取消</Button>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction as-child>
|
||||
<Button @click="importContractData">确认覆盖</Button>
|
||||
</AlertDialogAction>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</AlertDialogRoot>
|
||||
</div>
|
||||
|
||||
<div class="ag-theme-quartz h-full min-h-0 w-full flex-1">
|
||||
|
||||
@@ -45,6 +45,7 @@ const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
|
||||
const pricingPaneReloadStore = usePricingPaneReloadStore()
|
||||
const consultCategoryFactorMap = ref<Map<string, number | null>>(new Map())
|
||||
let factorDefaultsLoaded = false
|
||||
const paneInstanceCreatedAt = Date.now()
|
||||
|
||||
const getDefaultConsultCategoryFactor = () =>
|
||||
consultCategoryFactorMap.value.get(String(props.serviceId)) ?? null
|
||||
@@ -59,8 +60,21 @@ const shouldSkipPersist = () => {
|
||||
const storageKey = `${PRICING_CLEAR_SKIP_PREFIX}${DB_KEY.value}`
|
||||
const raw = sessionStorage.getItem(storageKey)
|
||||
if (!raw) return false
|
||||
const now = Date.now()
|
||||
|
||||
if (raw.includes(':')) {
|
||||
const [issuedRaw, untilRaw] = raw.split(':')
|
||||
const issuedAt = Number(issuedRaw)
|
||||
const skipUntil = Number(untilRaw)
|
||||
if (Number.isFinite(issuedAt) && Number.isFinite(skipUntil) && now <= skipUntil) {
|
||||
return paneInstanceCreatedAt <= issuedAt
|
||||
}
|
||||
sessionStorage.removeItem(storageKey)
|
||||
return false
|
||||
}
|
||||
|
||||
const skipUntil = Number(raw)
|
||||
if (Number.isFinite(skipUntil) && Date.now() <= skipUntil) return true
|
||||
if (Number.isFinite(skipUntil) && now <= skipUntil) return true
|
||||
sessionStorage.removeItem(storageKey)
|
||||
return false
|
||||
}
|
||||
@@ -78,7 +92,8 @@ const detailRows = ref<DetailRow[]>([])
|
||||
|
||||
type taskLite = {
|
||||
serviceID: number
|
||||
code: string
|
||||
code?: string
|
||||
ref?: string
|
||||
name: string
|
||||
basicParam: string
|
||||
unit: string
|
||||
@@ -116,11 +131,12 @@ const buildDefaultRows = (): DetailRow[] => {
|
||||
|
||||
for (const [order, taskId] of sourceTaskIds.entries()) {
|
||||
const task = (taskList as Record<string, taskLite | undefined>)[String(taskId)]
|
||||
if (!task?.code || !task?.name) continue
|
||||
const taskCode = task?.code || task?.ref || ''
|
||||
if (!taskCode || !task?.name) continue
|
||||
const rowId = `task-${taskId}-${order}`
|
||||
rows.push({
|
||||
id: rowId,
|
||||
taskCode: task.code,
|
||||
taskCode,
|
||||
taskName: task.name,
|
||||
unit: task.unit || '',
|
||||
conversion: typeof task.conversion === 'number' && Number.isFinite(task.conversion) ? task.conversion : null,
|
||||
|
||||
+192
-43
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import type { ComponentPublicInstance } from 'vue'
|
||||
import { computed, defineComponent, h, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import type { ComponentPublicInstance, PropType } from 'vue'
|
||||
import { AgGridVue } from 'ag-grid-vue3'
|
||||
import type { ColDef, GridOptions, ICellRendererParams } from 'ag-grid-community'
|
||||
import localforage from 'localforage'
|
||||
@@ -8,10 +8,18 @@ import { myTheme ,gridOptions} from '@/lib/diyAgGridOptions'
|
||||
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
|
||||
import { addNumbers } from '@/lib/decimal'
|
||||
import { formatThousands, formatThousandsFlexible } from '@/lib/numberFormat'
|
||||
import { getPricingMethodTotalsForService, getPricingMethodTotalsForServices } from '@/lib/pricingMethodTotals'
|
||||
import { getPricingMethodTotalsForServices } from '@/lib/pricingMethodTotals'
|
||||
import { ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
|
||||
import { Search } from 'lucide-vue-next'
|
||||
import {
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogPortal,
|
||||
AlertDialogRoot,
|
||||
AlertDialogTitle,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
@@ -22,6 +30,7 @@ import {
|
||||
DialogTrigger
|
||||
} from 'reka-ui'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { serviceList } from '@/sql'
|
||||
import { useTabStore } from '@/pinia/tab'
|
||||
import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
||||
@@ -153,6 +162,8 @@ function handleSnapHostScroll() {
|
||||
const pickerOpen = ref(false)
|
||||
const pickerTempIds = ref<string[]>([])
|
||||
const pickerSearch = ref('')
|
||||
const clearConfirmOpen = ref(false)
|
||||
const pendingClearServiceId = ref<string | null>(null)
|
||||
const dragSelecting = ref(false)
|
||||
const dragMoved = ref(false)
|
||||
let dragSelectChecked = false
|
||||
@@ -170,6 +181,39 @@ const selectedServiceText = computed(() => {
|
||||
return `${names.slice(0, 2).join('、')} 等 ${names.length} 项`
|
||||
})
|
||||
|
||||
const pendingClearServiceName = computed(() => {
|
||||
if (!pendingClearServiceId.value) return ''
|
||||
const row = detailRows.value.find(item => item.id === pendingClearServiceId.value)
|
||||
if (row) return `${row.code}${row.name}`
|
||||
const dict = serviceById.get(pendingClearServiceId.value)
|
||||
if (dict) return `${dict.code}${dict.name}`
|
||||
return pendingClearServiceId.value
|
||||
})
|
||||
|
||||
const handleClearConfirmOpenChange = (open: boolean) => {
|
||||
clearConfirmOpen.value = open
|
||||
}
|
||||
|
||||
const requestClearRow = (row: DetailRow) => {
|
||||
if (isFixedRow(row)) return
|
||||
pendingClearServiceId.value = row.id
|
||||
clearConfirmOpen.value = true
|
||||
}
|
||||
|
||||
const confirmClearRow = async () => {
|
||||
const id = pendingClearServiceId.value
|
||||
if (!id) return
|
||||
const row = detailRows.value.find(item => item.id === id)
|
||||
if (!row || isFixedRow(row)) {
|
||||
clearConfirmOpen.value = false
|
||||
pendingClearServiceId.value = null
|
||||
return
|
||||
}
|
||||
await clearRowValues(row)
|
||||
clearConfirmOpen.value = false
|
||||
pendingClearServiceId.value = null
|
||||
}
|
||||
|
||||
const filteredServiceDict = computed(() => {
|
||||
const keyword = pickerSearch.value.trim()
|
||||
if (!keyword) return serviceDict
|
||||
@@ -198,6 +242,26 @@ const numericParser = (newValue: any): number | null => {
|
||||
|
||||
const valueOrZero = (v: number | null | undefined) => (typeof v === 'number' ? v : 0)
|
||||
|
||||
const getMethodTotalFromRows = (
|
||||
rows: DetailRow[],
|
||||
field: 'investScale' | 'landScale' | 'workload' | 'hourly'
|
||||
) =>
|
||||
rows.reduce((sum, row) => {
|
||||
if (isFixedRow(row)) return sum
|
||||
return addNumbers(sum, valueOrZero(row[field]))
|
||||
}, 0)
|
||||
|
||||
const getMethodTotal = (field: 'investScale' | 'landScale' | 'workload' | 'hourly') =>
|
||||
getMethodTotalFromRows(detailRows.value, field)
|
||||
|
||||
const getFixedRowSubtotal = () =>
|
||||
addNumbers(
|
||||
getMethodTotal('investScale'),
|
||||
getMethodTotal('landScale'),
|
||||
getMethodTotal('workload'),
|
||||
getMethodTotal('hourly')
|
||||
)
|
||||
|
||||
const getPricingPaneStorageKeys = (serviceId: string) => [
|
||||
`tzGMF-${props.contractId}-${serviceId}`,
|
||||
`ydGMF-${props.contractId}-${serviceId}`,
|
||||
@@ -207,9 +271,11 @@ const getPricingPaneStorageKeys = (serviceId: string) => [
|
||||
|
||||
const clearPricingPaneValues = async (serviceId: string) => {
|
||||
const keys = getPricingPaneStorageKeys(serviceId)
|
||||
const skipUntil = Date.now() + PRICING_CLEAR_SKIP_TTL_MS
|
||||
const clearIssuedAt = Date.now()
|
||||
const skipUntil = clearIssuedAt + PRICING_CLEAR_SKIP_TTL_MS
|
||||
const skipToken = `${clearIssuedAt}:${skipUntil}`
|
||||
for (const key of keys) {
|
||||
sessionStorage.setItem(`${PRICING_CLEAR_SKIP_PREFIX}${key}`, String(skipUntil))
|
||||
sessionStorage.setItem(`${PRICING_CLEAR_SKIP_PREFIX}${key}`, skipToken)
|
||||
sessionStorage.setItem(`${PRICING_FORCE_DEFAULT_PREFIX}${key}`, String(skipUntil))
|
||||
}
|
||||
await Promise.all(keys.map(key => localforage.removeItem(key)))
|
||||
@@ -222,21 +288,37 @@ const clearRowValues = async (row: DetailRow) => {
|
||||
// 若该服务编辑页已打开,先关闭,避免子页面卸载时把旧数据写回缓? tabStore.removeTab(`zxfw-edit-${props.contractId}-${row.id}`)
|
||||
await nextTick()
|
||||
await clearPricingPaneValues(row.id)
|
||||
const totals = await getPricingMethodTotalsForService({
|
||||
contractId: props.contractId,
|
||||
serviceId: row.id
|
||||
})
|
||||
detailRows.value = detailRows.value.map(item =>
|
||||
// const totals = await getPricingMethodTotalsForService({
|
||||
// contractId: props.contractId,
|
||||
// serviceId: row.id
|
||||
// })
|
||||
const clearedRows = detailRows.value.map(item =>
|
||||
item.id !== row.id
|
||||
? item
|
||||
: {
|
||||
...item,
|
||||
investScale: totals.investScale,
|
||||
landScale: totals.landScale,
|
||||
workload: totals.workload,
|
||||
hourly: totals.hourly
|
||||
investScale: null,
|
||||
landScale: null,
|
||||
workload: null,
|
||||
hourly: null
|
||||
}
|
||||
)
|
||||
const nextInvestScale = getMethodTotalFromRows(clearedRows, 'investScale')
|
||||
const nextLandScale = getMethodTotalFromRows(clearedRows, 'landScale')
|
||||
const nextWorkload = getMethodTotalFromRows(clearedRows, 'workload')
|
||||
const nextHourly = getMethodTotalFromRows(clearedRows, 'hourly')
|
||||
detailRows.value = clearedRows.map(item =>
|
||||
isFixedRow(item)
|
||||
? {
|
||||
...item,
|
||||
investScale: nextInvestScale,
|
||||
landScale: nextLandScale,
|
||||
workload: nextWorkload,
|
||||
hourly: nextHourly,
|
||||
subtotal: addNumbers(nextInvestScale, nextLandScale, nextWorkload, nextHourly)
|
||||
}
|
||||
: item
|
||||
)
|
||||
await saveToIndexedDB()
|
||||
}
|
||||
|
||||
@@ -249,6 +331,41 @@ const openEditTab = (row: DetailRow) => {
|
||||
})
|
||||
}
|
||||
|
||||
const ActionCellRenderer = defineComponent({
|
||||
name: 'ActionCellRenderer',
|
||||
props: {
|
||||
params: {
|
||||
type: Object as PropType<ICellRendererParams<DetailRow>>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
return () => {
|
||||
if (isFixedRow(props.params.data)) return null
|
||||
return h('div', { class: 'zxfw-action-wrap' }, [
|
||||
h(TooltipRoot, null, {
|
||||
default: () => [
|
||||
h(TooltipTrigger, { asChild: true }, {
|
||||
default: () =>
|
||||
h('button', { class: 'zxfw-action-btn', 'data-action': 'edit' }, '✏️')
|
||||
}),
|
||||
h(TooltipContent, { side: 'top' }, { default: () => '编辑' })
|
||||
]
|
||||
}),
|
||||
h(TooltipRoot, null, {
|
||||
default: () => [
|
||||
h(TooltipTrigger, { asChild: true }, {
|
||||
default: () =>
|
||||
h('button', { class: 'zxfw-action-btn', 'data-action': 'clear' }, '🧹')
|
||||
}),
|
||||
h(TooltipContent, { side: 'top' }, { default: () => '清空' })
|
||||
]
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const columnDefs: ColDef<DetailRow>[] = [
|
||||
{ headerName: '编码', field: 'code', minWidth: 50, maxWidth: 100 },
|
||||
{ headerName: '名称', field: 'name', minWidth: 250, flex: 3, tooltipField: 'name' },
|
||||
@@ -261,6 +378,11 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
editable: false,
|
||||
|
||||
valueGetter: params => {
|
||||
if (!params.data) return null
|
||||
if (isFixedRow(params.data)) return getMethodTotal('investScale')
|
||||
return params.data.investScale
|
||||
},
|
||||
valueParser: params => numericParser(params.newValue),
|
||||
valueFormatter: params => (params.value == null ? '' : formatThousands(params.value))
|
||||
},
|
||||
@@ -273,6 +395,11 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
editable: false,
|
||||
|
||||
valueGetter: params => {
|
||||
if (!params.data) return null
|
||||
if (isFixedRow(params.data)) return getMethodTotal('landScale')
|
||||
return params.data.landScale
|
||||
},
|
||||
valueParser: params => numericParser(params.newValue),
|
||||
valueFormatter: params => (params.value == null ? '' : formatThousandsFlexible(params.value, 2))
|
||||
},
|
||||
@@ -285,6 +412,11 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
editable: false,
|
||||
|
||||
valueGetter: params => {
|
||||
if (!params.data) return null
|
||||
if (isFixedRow(params.data)) return getMethodTotal('workload')
|
||||
return params.data.workload
|
||||
},
|
||||
// editable: params => !params.node?.rowPinned && !isFixedRow(params.data),
|
||||
valueParser: params => numericParser(params.newValue),
|
||||
valueFormatter: params => (params.value == null ? '' : formatThousands(params.value))
|
||||
@@ -298,6 +430,11 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
editable: false,
|
||||
|
||||
valueGetter: params => {
|
||||
if (!params.data) return null
|
||||
if (isFixedRow(params.data)) return getMethodTotal('hourly')
|
||||
return params.data.hourly
|
||||
},
|
||||
// editable: params => !params.node?.rowPinned && !isFixedRow(params.data),
|
||||
valueParser: params => numericParser(params.newValue),
|
||||
valueFormatter: params => (params.value == null ? '' : formatThousands(params.value))
|
||||
@@ -313,6 +450,7 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
editable: false,
|
||||
valueGetter: params => {
|
||||
if (!params.data) return null
|
||||
if (isFixedRow(params.data)) return getFixedRowSubtotal()
|
||||
return addNumbers(
|
||||
valueOrZero(params.data.investScale),
|
||||
valueOrZero(params.data.landScale),
|
||||
@@ -332,14 +470,7 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
sortable: false,
|
||||
filter: false,
|
||||
suppressMovable: true,
|
||||
cellRenderer: (params: ICellRendererParams<DetailRow>) =>
|
||||
isFixedRow(params.data)
|
||||
? ''
|
||||
: `<div class="zxfw-action-wrap">
|
||||
<button class="zxfw-action-btn" data-action="edit" title="编辑">✏️</button>
|
||||
|
||||
<button class="zxfw-action-btn" data-action="clear" title="清空(重置到默认状态)">🧹</button>
|
||||
</div>`
|
||||
cellRenderer: ActionCellRenderer
|
||||
}
|
||||
]
|
||||
|
||||
@@ -353,7 +484,7 @@ const detailGridOptions: GridOptions<DetailRow> = {
|
||||
const btn = target?.closest('button[data-action]') as HTMLButtonElement | null
|
||||
const action = btn?.dataset.action
|
||||
if (action === 'clear') {
|
||||
await clearRowValues(params.data)
|
||||
requestClearRow(params.data)
|
||||
return
|
||||
}
|
||||
if (action === 'edit') {
|
||||
@@ -457,7 +588,7 @@ const handlePickerOpenChange = (open: boolean) => {
|
||||
const confirmPicker = async () => {
|
||||
applySelection(pickerTempIds.value)
|
||||
try {
|
||||
await fillPricingTotalsForSelectedRows()
|
||||
// await fillPricingTotalsForSelectedRows()
|
||||
await saveToIndexedDB()
|
||||
} catch (error) {
|
||||
console.error('confirmPicker failed:', error)
|
||||
@@ -469,15 +600,7 @@ const clearPickerSelection = () => {
|
||||
pickerTempIds.value = []
|
||||
}
|
||||
|
||||
const toggleServiceCode = (code: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
if (!pickerTempIds.value.includes(code)) {
|
||||
pickerTempIds.value = [...pickerTempIds.value, code]
|
||||
}
|
||||
return
|
||||
}
|
||||
pickerTempIds.value = pickerTempIds.value.filter(item => item !== code)
|
||||
}
|
||||
|
||||
|
||||
const applyTempChecked = (code: string, checked: boolean) => {
|
||||
const exists = pickerTempIds.value.includes(code)
|
||||
@@ -608,7 +731,7 @@ const loadFromIndexedDB = async () => {
|
||||
})
|
||||
|
||||
try {
|
||||
await fillPricingTotalsForSelectedRows()
|
||||
// await fillPricingTotalsForSelectedRows()
|
||||
} catch (error) {
|
||||
console.error('fillPricingTotalsForSelectedRows failed:', error)
|
||||
}
|
||||
@@ -659,7 +782,8 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="rootRef" class="space-y-6">
|
||||
<TooltipProvider>
|
||||
<div ref="rootRef" class="space-y-6">
|
||||
<DialogRoot v-model:open="pickerOpen" @update:open="handlePickerOpenChange">
|
||||
<div class="rounded-lg border bg-card p-4 shadow-sm shrink-0">
|
||||
<div class="mb-2 flex items-center justify-between gap-3">
|
||||
@@ -668,13 +792,17 @@ onBeforeUnmount(() => {
|
||||
<div class="flex items-center gap-2">
|
||||
<input :value="selectedServiceText" readonly placeholder="请点击右侧“浏览”选择服务"
|
||||
class="h-10 w-full rounded-md border bg-background px-3 text-sm text-foreground outline-none" />
|
||||
<DialogTrigger as-child>
|
||||
<button type="button"
|
||||
class="inline-flex h-10 w-10 items-center justify-center rounded-md border text-sm hover:bg-accent cursor-pointer"
|
||||
title="浏览服务词典">
|
||||
<Search class="h-4 w-4" />
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
<TooltipRoot>
|
||||
<TooltipTrigger as-child>
|
||||
<DialogTrigger as-child>
|
||||
<button type="button"
|
||||
class="inline-flex h-10 w-10 items-center justify-center rounded-md border text-sm hover:bg-accent cursor-pointer">
|
||||
<Search class="h-4 w-4" />
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">浏览服务词典</TooltipContent>
|
||||
</TooltipRoot>
|
||||
</div>
|
||||
</div>
|
||||
<DialogPortal>
|
||||
@@ -754,5 +882,26 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<AlertDialogRoot :open="clearConfirmOpen" @update:open="handleClearConfirmOpenChange">
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" />
|
||||
<AlertDialogContent class="fixed left-1/2 top-1/2 z-[70] w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl">
|
||||
<AlertDialogTitle class="text-base font-semibold">确认清空服务数据</AlertDialogTitle>
|
||||
<AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
|
||||
将清空“{{ pendingClearServiceName }}”的计价数据是否继续?
|
||||
</AlertDialogDescription>
|
||||
<div class="mt-4 flex items-center justify-end gap-2">
|
||||
<AlertDialogCancel as-child>
|
||||
<Button variant="outline">取消</Button>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction as-child>
|
||||
<Button variant="destructive" @click="confirmClearRow">确认清空</Button>
|
||||
</AlertDialogAction>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</AlertDialogRoot>
|
||||
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</template>
|
||||
Reference in new issue
Block a user