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>
|
||||
+12
-4
@@ -485,7 +485,9 @@ const exportData = async () => {
|
||||
dataMenuOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const exportReport = async ()=>{
|
||||
|
||||
}
|
||||
const triggerImport = () => {
|
||||
importFileRef.value?.click()
|
||||
}
|
||||
@@ -629,17 +631,23 @@ watch(
|
||||
v-if="dataMenuOpen"
|
||||
class="absolute right-0 top-full mt-1 z-50 min-w-[108px] rounded-md border bg-background p-1 shadow-md"
|
||||
>
|
||||
<button
|
||||
class="w-full cursor-pointer rounded px-3 py-1.5 text-left text-sm hover:bg-muted"
|
||||
@click="triggerImport"
|
||||
>
|
||||
导入
|
||||
</button>
|
||||
<button
|
||||
class="w-full cursor-pointer rounded px-3 py-1.5 text-left text-sm hover:bg-muted"
|
||||
@click="exportData"
|
||||
>
|
||||
导出
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
class="w-full cursor-pointer rounded px-3 py-1.5 text-left text-sm hover:bg-muted"
|
||||
@click="triggerImport"
|
||||
@click="exportReport"
|
||||
>
|
||||
导入
|
||||
导出报表
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
|
||||
+196
-2
@@ -1,8 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, ref, watch, type Component } from 'vue'
|
||||
import { computed, onBeforeUnmount, ref, watch, type Component ,onMounted} from 'vue'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogRoot,
|
||||
DialogTitle,
|
||||
DialogTrigger,DialogDescription
|
||||
} from 'reka-ui'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
import { animate, AnimatePresence, Motion, useMotionValue, useMotionValueEvent, useTransform } from 'motion-v'
|
||||
interface TypeLineCategory {
|
||||
key: string
|
||||
label: string
|
||||
@@ -59,6 +70,7 @@ const activeComponent = computed(() => {
|
||||
})
|
||||
|
||||
const copyBtnText = ref('复制')
|
||||
const sheetOpen = ref(false)
|
||||
let copyBtnTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const handleCopySubtitle = async () => {
|
||||
@@ -81,7 +93,91 @@ const handleCopySubtitle = async () => {
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (copyBtnTimer) clearTimeout(copyBtnTimer)
|
||||
if (!root) return
|
||||
root.style.scale = ''
|
||||
root.style.translate = ''
|
||||
root.style.borderRadius = ''
|
||||
})
|
||||
|
||||
//
|
||||
|
||||
|
||||
const inertiaTransition = {
|
||||
type: 'inertia' as const,
|
||||
bounceStiffness: 300,
|
||||
bounceDamping: 40,
|
||||
timeConstant: 300,
|
||||
}
|
||||
|
||||
const staticTransition = {
|
||||
duration: 0.5,
|
||||
ease: [0.32, 0.72, 0, 1] as const,
|
||||
}
|
||||
|
||||
const SHEET_TOP_RATIO = 0.1
|
||||
const SHEET_RADIUS = 12
|
||||
const OFFICIAL_SITE_URL = 'http://www.zwgczx.com.cn/'
|
||||
|
||||
let root: HTMLElement | null = null
|
||||
|
||||
onMounted(() => {
|
||||
root = document.body.firstElementChild as HTMLElement | null
|
||||
})
|
||||
|
||||
const { height, width } = useWindowSize()
|
||||
|
||||
const sheetTop = computed(() => Math.round(height.value * SHEET_TOP_RATIO))
|
||||
const h = computed(() => Math.max(0, height.value - sheetTop.value))
|
||||
const y = useMotionValue(h.value)
|
||||
|
||||
watch(
|
||||
() => h.value,
|
||||
(nextHeight) => {
|
||||
if (!sheetOpen.value) y.jump(nextHeight)
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => sheetOpen.value,
|
||||
(isOpen) => {
|
||||
if (!isOpen) {
|
||||
y.jump(h.value)
|
||||
return
|
||||
}
|
||||
y.jump(h.value)
|
||||
animate(y, 0, staticTransition)
|
||||
}
|
||||
)
|
||||
|
||||
// Scale the body down and adjust the border radius when the sheet is open.
|
||||
const bodyScale = useTransform(
|
||||
y,
|
||||
[0, h.value],
|
||||
[(width.value - sheetTop.value) / width.value, 1],
|
||||
)
|
||||
const bodyTranslate = useTransform(y, [0, h.value], [sheetTop.value - SHEET_RADIUS, 0])
|
||||
const bodyBorderRadius = useTransform(y, [0, h.value], [SHEET_RADIUS, 0])
|
||||
|
||||
useMotionValueEvent(bodyScale, 'change', (v) => {
|
||||
if (!root) return
|
||||
root.style.scale = `${v}`
|
||||
})
|
||||
useMotionValueEvent(
|
||||
bodyTranslate,
|
||||
'change',
|
||||
(v) => {
|
||||
if (!root) return
|
||||
root.style.translate = `0 ${v}px`
|
||||
},
|
||||
)
|
||||
useMotionValueEvent(
|
||||
bodyBorderRadius,
|
||||
'change',
|
||||
(v) => {
|
||||
if (!root) return
|
||||
root.style.borderRadius = `${v}px`
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -140,6 +236,104 @@ onBeforeUnmount(() => {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogRoot v-model:open="sheetOpen">
|
||||
<DialogTrigger as-child>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute left-4 right-4 bottom-4 flex cursor-pointer flex-col items-center gap-1.5 rounded-lg border bg-muted/35 px-3 py-2 text-center text-[12px] leading-5 text-foreground/85 shadow-sm transition-colors hover:bg-muted/55 hover:text-foreground"
|
||||
>
|
||||
<img src="/favicon.ico" alt="众为咨询" class="h-5 w-5 shrink-0 rounded-sm" />
|
||||
<span>本网站由众为工程咨询有限公司提供免费技术支持</span>
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
<DialogPortal>
|
||||
<AnimatePresence
|
||||
multiple
|
||||
as="div"
|
||||
>
|
||||
<DialogOverlay as-child>
|
||||
<Motion
|
||||
class="fixed inset-0 z-10 bg-black/45 backdrop-blur-[2px]"
|
||||
:initial="{ opacity: 0 }"
|
||||
:animate="{ opacity: 1 }"
|
||||
:exit="{ opacity: 0 }"
|
||||
:transition="staticTransition"
|
||||
/>
|
||||
</DialogOverlay>
|
||||
|
||||
<DialogContent as-child>
|
||||
<Motion
|
||||
class="fixed inset-x-0 bottom-0 z-20 overflow-hidden rounded-t-2xl border border-border/60 bg-card/95 shadow-2xl backdrop-blur-xl will-change-transform"
|
||||
:style="{
|
||||
y,
|
||||
top: `${sheetTop}px`,
|
||||
}"
|
||||
drag="y"
|
||||
:drag-constraints="{ top: 0 }"
|
||||
@drag-end="(e, { offset, velocity }) => {
|
||||
if (offset.y > h * 0.35 || velocity.y > 10) {
|
||||
sheetOpen = false;
|
||||
}
|
||||
else {
|
||||
animate(y, 0, { ...inertiaTransition, min: 0, max: 0 });
|
||||
}
|
||||
}"
|
||||
>
|
||||
<div class="mx-auto mt-2 h-1.5 w-12 cursor-grab rounded-full bg-muted-foreground/35 active:cursor-grabbing" />
|
||||
<div class="mx-auto flex h-full w-full max-w-2xl flex-col px-4 pb-5 pt-3">
|
||||
<div class="mb-3">
|
||||
<div class="flex justify-end">
|
||||
<DialogClose class="inline-flex h-8 w-8 cursor-pointer items-center justify-center rounded-md border border-muted-foreground/30 text-muted-foreground transition-colors hover:border-foreground/40 hover:text-foreground">
|
||||
<Icon icon="lucide:x" class="h-4 w-4" />
|
||||
</DialogClose>
|
||||
</div>
|
||||
<DialogTitle class="mt-2">
|
||||
<div class="flex items-center gap-3">
|
||||
<img src="/favicon.ico" alt="众为咨询" class="h-7 w-7 shrink-0 rounded-sm" />
|
||||
<span class="text-2xl font-semibold leading-none">关于我们</span>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
</div>
|
||||
|
||||
<DialogDescription class="mb-4 text-base text-muted-foreground">
|
||||
<div class="flex items-center gap-2">
|
||||
<a
|
||||
:href="OFFICIAL_SITE_URL"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex cursor-pointer items-center font-medium text-foreground transition-colors hover:text-primary hover:underline"
|
||||
>
|
||||
众为工程咨询有限公司
|
||||
</a>
|
||||
<a
|
||||
:href="OFFICIAL_SITE_URL"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex h-7 w-7 cursor-pointer items-center justify-center rounded-md border border-muted-foreground/30 text-muted-foreground transition-colors hover:border-foreground/40 hover:text-foreground"
|
||||
aria-label="跳转到官网首页"
|
||||
title="官网首页"
|
||||
>
|
||||
<Icon icon="lucide:arrow-up-right" class="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
</DialogDescription>
|
||||
|
||||
<div class="space-y-4 overflow-y-auto pr-1 text-[15px] leading-7">
|
||||
<p>
|
||||
众为工程咨询有限公司 2009 年成立,专注工程造价与工程成本管控全过程咨询,是广东省政府审计入库优选单位。公司服务覆盖多领域、全类型客户,累计服务投资额超万亿元,深度参与港珠澳大桥、澳门大学横琴校区等国家级重点工程,参编三十余项国家及省市行业标准。
|
||||
</p>
|
||||
<p>
|
||||
公司立足大湾区,布局全球,设有澳门公司、斯里兰卡分公司,具备跨境与海外项目服务能力,以十五年专业沉淀、万亿级项目经验,为客户提供精准、可靠的工程咨询服务。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Motion>
|
||||
</DialogContent>
|
||||
</AnimatePresence>
|
||||
</DialogPortal>
|
||||
</DialogRoot>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="w-88/100 min-h-0 h-full flex flex-col">
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
|
||||
GridOptions,
|
||||
themeQuartz
|
||||
} from "ag-grid-community"
|
||||
import { GridOptions, themeQuartz } from "ag-grid-community"
|
||||
const borderConfig = {
|
||||
style: "solid", // 虚线改实线更简洁,也可保留 dotted 但建议用 solid
|
||||
width: 0.3, // 更细的边框,减少视觉干扰
|
||||
@@ -13,7 +9,7 @@ const borderConfig = {
|
||||
export const myTheme = themeQuartz.withParams({
|
||||
// 核心:移除外边框,减少视觉包裹感
|
||||
wrapperBorder: false,
|
||||
|
||||
|
||||
// 表头样式(柔和浅蓝,无加粗,更轻盈)
|
||||
headerBackgroundColor: "#f0f2f3", // 极浅的背景色,替代深一点的 #e7f3fc
|
||||
headerTextColor: "#374151", // 深灰色文字,比纯黑更柔和
|
||||
@@ -24,8 +20,7 @@ export const myTheme = themeQuartz.withParams({
|
||||
rowBorder: borderConfig,
|
||||
columnBorder: borderConfig,
|
||||
headerRowBorder: borderConfig,
|
||||
|
||||
|
||||
|
||||
// 可选:偶数行背景色(轻微区分,更清新)
|
||||
dataBackgroundColor: "#fefefe"
|
||||
});
|
||||
@@ -43,6 +38,8 @@ export const gridOptions: GridOptions<any> = {
|
||||
// },
|
||||
groupDefaultExpanded: -1,
|
||||
suppressFieldDotNotation: true,
|
||||
// Keep group expand/collapse state when rowData updates after edits/saves.
|
||||
getRowId: params => String(params.data?.id ?? params.data?.path?.join('/') ?? ''),
|
||||
getDataPath: data => data.path,
|
||||
getContextMenuItems: () => ['copy', 'paste', 'separator', 'export'],
|
||||
defaultColDef: {
|
||||
|
||||
+39
-14
@@ -1,4 +1,5 @@
|
||||
import localforage from 'localforage'
|
||||
import { majorList, serviceList } from '@/sql'
|
||||
|
||||
const CONSULT_CATEGORY_FACTOR_KEY = 'xm-consult-category-factor-v1'
|
||||
const MAJOR_FACTOR_KEY = 'xm-major-factor-v1'
|
||||
@@ -13,27 +14,51 @@ type XmFactorState = {
|
||||
detailRows?: XmFactorRow[]
|
||||
}
|
||||
|
||||
type FactorDictItem = {
|
||||
defCoe?: number | null
|
||||
}
|
||||
|
||||
type FactorDict = Record<string, FactorDictItem>
|
||||
|
||||
const toFiniteNumberOrNull = (value: unknown): number | null =>
|
||||
typeof value === 'number' && Number.isFinite(value) ? value : null
|
||||
|
||||
const resolveFactorValue = (row: Partial<XmFactorRow> | undefined): number | null => {
|
||||
if (!row) return null
|
||||
const budgetValue = toFiniteNumberOrNull(row.budgetValue)
|
||||
if (budgetValue != null) return budgetValue
|
||||
return toFiniteNumberOrNull(row.standardFactor)
|
||||
}
|
||||
|
||||
const loadFactorMap = async (storageKey: string): Promise<Map<string, number | null>> => {
|
||||
const data = await localforage.getItem<XmFactorState>(storageKey)
|
||||
const buildStandardFactorMap = (dict: FactorDict): Map<string, number | null> => {
|
||||
const map = new Map<string, number | null>()
|
||||
for (const row of data?.detailRows || []) {
|
||||
if (!row?.id) continue
|
||||
map.set(String(row.id), resolveFactorValue(row))
|
||||
for (const [id, item] of Object.entries(dict)) {
|
||||
map.set(String(id), toFiniteNumberOrNull(item?.defCoe))
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
export const loadConsultCategoryFactorMap = async () => loadFactorMap(CONSULT_CATEGORY_FACTOR_KEY)
|
||||
const resolveFactorValue = (
|
||||
row: Partial<XmFactorRow> | undefined,
|
||||
fallbackStandard: number | null
|
||||
): number | null => {
|
||||
if (!row) return null
|
||||
const budgetValue = toFiniteNumberOrNull(row.budgetValue)
|
||||
if (budgetValue != null) return budgetValue
|
||||
const standardFactor = toFiniteNumberOrNull(row.standardFactor)
|
||||
if (standardFactor != null) return standardFactor
|
||||
return fallbackStandard
|
||||
}
|
||||
|
||||
export const loadMajorFactorMap = async () => loadFactorMap(MAJOR_FACTOR_KEY)
|
||||
const loadFactorMap = async (
|
||||
storageKey: string,
|
||||
dict: FactorDict
|
||||
): Promise<Map<string, number | null>> => {
|
||||
const data = await localforage.getItem<XmFactorState>(storageKey)
|
||||
const map = buildStandardFactorMap(dict)
|
||||
for (const row of data?.detailRows || []) {
|
||||
if (!row?.id) continue
|
||||
const id = String(row.id)
|
||||
map.set(id, resolveFactorValue(row, map.get(id) ?? null))
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
export const loadConsultCategoryFactorMap = async () =>
|
||||
loadFactorMap(CONSULT_CATEGORY_FACTOR_KEY, serviceList as FactorDict)
|
||||
|
||||
export const loadMajorFactorMap = async () =>
|
||||
loadFactorMap(MAJOR_FACTOR_KEY, majorList as FactorDict)
|
||||
+817
-61
@@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
import { roundTo, toDecimal } from '@/lib/decimal'
|
||||
|
||||
export const majorList = {
|
||||
@@ -67,44 +68,51 @@ export const serviceList = {
|
||||
};
|
||||
//basicParam预算基数
|
||||
|
||||
export const taskList = {
|
||||
0: { serviceID: 15, code: 'C4-1', name: '工程造价日常顾问', basicParam: '服务月份数', required: true, unit: '万元/月', conversion: 10000, maxPrice: 0.5, minPrice: 0.3, defPrice: 0.4, desc: '' },
|
||||
1: { serviceID: 15, code: 'C4-2', name: '工程造价专项顾问', basicParam: '服务项目的造价金额', required: true, unit: '%', conversion: 0.01, maxPrice: null, minPrice: null, defPrice: 0.01, desc: '适用于涉及造价费用类的顾问' },
|
||||
2: { serviceID: 16, code: 'C5-1', name: '组织与调研工作', basicParam: '调研次数', required: true, unit: '万元/次', conversion: 10000, maxPrice: 2, minPrice: 1, defPrice: 1.5, desc: '' },
|
||||
3: { serviceID: 16, code: 'C5-2-1', name: '文件编写工作', basicParam: '文件份数', required: true, unit: '万元/份', conversion: 10000, maxPrice: 5, minPrice: 3, defPrice: 4, desc: '主编' },
|
||||
4: { serviceID: 16, code: 'C5-2-2', name: '文件编写工作', basicParam: '文件份数', required: true, unit: '万元/份', conversion: 10000, maxPrice: 3, minPrice: 1, defPrice: 2, desc: '参编' },
|
||||
5: { serviceID: 16, code: 'C5-3-1', name: '评审工作', basicParam: '评审次数', required: false, unit: '万元/次', conversion: 10000, maxPrice: 20, minPrice: 8, defPrice: 14, desc: '大型评审' },
|
||||
6: { serviceID: 16, code: 'C5-3-2', name: '评审工作', basicParam: '评审次数', required: false, unit: '万元/次', conversion: 10000, maxPrice: 10, minPrice: 5, defPrice: 7.5, desc: '中型评审' },
|
||||
7: { serviceID: 16, code: 'C5-3-3', name: '评审工作', basicParam: '评审次数', required: false, unit: '万元/次', conversion: 10000, maxPrice: 6, minPrice: 3, defPrice: 4.5, desc: '小型评审' },
|
||||
8: { serviceID: 17, code: 'C6-1', name: '组织与调研工作', basicParam: '调研次数', required: true, unit: '万元/次', conversion: 10000, maxPrice: 2, minPrice: 1, defPrice: 1.5, desc: '' },
|
||||
9: { serviceID: 17, code: 'C6-2-1', name: '研究及编写报告', basicParam: '文件份数', required: true, unit: '万元/份', conversion: 10000, maxPrice: 50, minPrice: 20, defPrice: 35, desc: '国家级' },
|
||||
10: { serviceID: 17, code: 'C6-2-2', name: '研究及编写报告', basicParam: '文件份数', required: true, unit: '万元/份', conversion: 10000, maxPrice: 20, minPrice: 10, defPrice: 15, desc: '省部级' },
|
||||
11: { serviceID: 17, code: 'C6-2-3', name: '研究及编写报告', basicParam: '文件份数', required: true, unit: '万元/份', conversion: 10000, maxPrice: 10, minPrice: 5, defPrice: 7.5, desc: '其他级' },
|
||||
12: { serviceID: 17, code: 'C6-3-1', name: '标准与技术性指导文件的编制', basicParam: '文件与标准的数量', required: true, unit: '万元/份', conversion: 10000, maxPrice: 80, minPrice: 50, defPrice: 65, desc: '复杂标准' },
|
||||
13: { serviceID: 17, code: 'C6-3-2', name: '标准与技术性指导文件的编制', basicParam: '文件与标准的数量', required: true, unit: '万元/份', conversion: 10000, maxPrice: 50, minPrice: 20, defPrice: 35, desc: '较复杂标准' },
|
||||
14: { serviceID: 17, code: 'C6-3-3', name: '标准与技术性指导文件的编制', basicParam: '文件与标准的数量', required: true, unit: '万元/份', conversion: 10000, maxPrice: 20, minPrice: 10, defPrice: 15, desc: '一般标准' },
|
||||
15: { serviceID: 17, code: 'C6-3-4', name: '标准与技术性指导文件的编制', basicParam: '文件与标准的数量', required: true, unit: '万元/份', conversion: 10000, maxPrice: 10, minPrice: 5, defPrice: 7.5, desc: '简单标准' },
|
||||
16: { serviceID: 17, code: 'C6-4-1', name: '评审与验收工作', basicParam: '评审与验收次数', required: false, unit: '万元/次', conversion: 10000, maxPrice: 20, minPrice: 8, defPrice: 14, desc: '大型评审' },
|
||||
17: { serviceID: 17, code: 'C6-4-2', name: '评审与验收工作', basicParam: '评审与验收次数', required: false, unit: '万元/次', conversion: 10000, maxPrice: 10, minPrice: 5, defPrice: 7.5, desc: '中型评审' },
|
||||
18: { serviceID: 17, code: 'C6-4-3', name: '评审与验收工作', basicParam: '评审与验收次数', required: false, unit: '万元/次', conversion: 10000, maxPrice: 6, minPrice: 3, defPrice: 4.5, desc: '小型评审' },
|
||||
19: { serviceID: 17, code: 'C6-5-1', name: '培训与宣贯工作', basicParam: '项目数量', required: false, unit: '万元/次', conversion: 10000, maxPrice: 3, minPrice: 1, defPrice: 2, desc: '培训与宣贯材料' },
|
||||
20: { serviceID: 17, code: 'C6-5-2', name: '培训与宣贯工作', basicParam: '培训与宣贯次数', required: false, unit: '万元/次', conversion: 10000, maxPrice: 1, minPrice: 0.5, defPrice: 0.75, desc: '组织培训与宣贯' },
|
||||
21: { serviceID: 17, code: 'C6-6', name: '测试与验证工作', basicParam: '', required: false, unit: '%', conversion: 0.01, maxPrice: 50, minPrice: 30, defPrice: 40, desc: '' },
|
||||
22: { serviceID: 18, code: 'C7-1', name: '组织与调研工作', basicParam: '调研次数', required: true, unit: '万元/次', conversion: 10000, maxPrice: 2, minPrice: 1, defPrice: 1.5, desc: '' },
|
||||
23: { serviceID: 18, code: 'C7-2', name: '编制大纲', basicParam: '项目数量', required: true, unit: '万元/个', conversion: 10000, maxPrice: 3, minPrice: 2, defPrice: 2.5, desc: '包括技术与定额子目研究' },
|
||||
24: { serviceID: 18, code: 'C7-3', name: '数据采集与测定', basicParam: '采集组数', required: true, unit: '万元/组', conversion: 10000, maxPrice: 0.8, minPrice: 0.2, defPrice: 0.5, desc: '现场采集方式时计' },
|
||||
25: { serviceID: 18, code: 'C7-4-1', name: '数据整理与分析', basicParam: '定额子目条数', required: true, unit: '万元/条', conversion: 10000, maxPrice: 0.3, minPrice: 0.1, defPrice: 0.2, desc: '简单定额' },
|
||||
26: { serviceID: 18, code: 'C7-4-2', name: '数据整理与分析', basicParam: '定额子目条数', required: true, unit: '万元/条', conversion: 10000, maxPrice: 3, minPrice: 0.3, defPrice: 1.65, desc: '复杂定额' },
|
||||
27: { serviceID: 18, code: 'C7-5', name: '编写定额测定报告', basicParam: '项目数量', required: true, unit: '万元/份', conversion: 10000, maxPrice: 5, minPrice: 2, defPrice: 3.5, desc: '' },
|
||||
28: { serviceID: 18, code: 'C7-6-1', name: '编制定额文本和释义', basicParam: '基本费用', required: true, unit: '万元/份', conversion: 10000, maxPrice: 1, minPrice: 0.5, defPrice: 0.75, desc: '20条定额子目内' },
|
||||
29: { serviceID: 18, code: 'C7-6-2', name: '编制定额文本和释义', basicParam: '定额子目条数', required: true, unit: '万元/条', conversion: 10000, maxPrice: 0.2, minPrice: 0.1, defPrice: 0.15, desc: '超过20条每增加1条' },
|
||||
30: { serviceID: 18, code: 'C7-7-1', name: '评审与验收工作', basicParam: '评审与验收次数', required: false, unit: '万元/次', conversion: 10000, maxPrice: 20, minPrice: 8, defPrice: 14, desc: '大型评审' },
|
||||
31: { serviceID: 18, code: 'C7-7-2', name: '评审与验收工作', basicParam: '评审与验收次数', required: false, unit: '万元/次', conversion: 10000, maxPrice: 10, minPrice: 5, defPrice: 7.5, desc: '中型评审' },
|
||||
32: { serviceID: 18, code: 'C7-7-3', name: '评审与验收工作', basicParam: '评审与验收次数', required: false, unit: '万元/次', conversion: 10000, maxPrice: 6, minPrice: 3, defPrice: 4.5, desc: '小型评审' },
|
||||
33: { serviceID: 18, code: 'C7-8-1', name: '培训与宣贯工作', basicParam: '项目数量', required: false, unit: '万元/次', conversion: 10000, maxPrice: 3, minPrice: 1, defPrice: 2, desc: '培训与宣贯材料' },
|
||||
34:{ serviceID :18 ,code :'C7-8-2' ,name :'培训与宣贯工作' ,basicParam :'培训与宣贯次数' ,required :false ,unit :'万元/次' ,conversion :10000 ,maxPrice :1 ,minPrice :0.5 ,defPrice :0.75 ,desc :'组织培训与宣贯'},
|
||||
35: { serviceID: 18, code: 'C7-9', name: '定额测试与验证', basicParam: '', required: false, unit: '%', conversion: 0.01, maxPrice: 50, minPrice: 30, defPrice: 40, desc: '' },
|
||||
export const taskList = {
|
||||
0: { serviceID: 15, ref: 'C4-1', name: '工程造价日常顾问', basicParam: '服务月份数', required: true, unit: '万元/月', conversion: 10000, maxPrice: 0.5, minPrice: 0.3, defPrice: 0.4, desc: '' },
|
||||
1: { serviceID: 15, ref: 'C4-2', name: '工程造价专项顾问', basicParam: '服务项目的造价金额', required: true, unit: '%', conversion: 0.01, maxPrice: null, minPrice: null, defPrice: 0.01, desc: '适用于涉及造价费用类的顾问' },
|
||||
2: { serviceID: 16, ref: 'C5-1', name: '组织与调研工作', basicParam: '调研次数', required: true, unit: '万元/次', conversion: 10000, maxPrice: 2, minPrice: 1, defPrice: 1.5, desc: '' },
|
||||
3: { serviceID: 16, ref: 'C5-2-1', name: '文件编写工作', basicParam: '文件份数', required: true, unit: '万元/份', conversion: 10000, maxPrice: 5, minPrice: 3, defPrice: 4, desc: '主编' },
|
||||
4: { serviceID: 16, ref: 'C5-2-2', name: '文件编写工作', basicParam: '文件份数', required: true, unit: '万元/份', conversion: 10000, maxPrice: 3, minPrice: 1, defPrice: 2, desc: '参编' },
|
||||
5: { serviceID: 16, ref: 'C5-3-1', name: '评审工作', basicParam: '评审次数', required: false, unit: '万元/次', conversion: 10000, maxPrice: 20, minPrice: 8, defPrice: 14, desc: '大型评审' },
|
||||
6: { serviceID: 16, ref: 'C5-3-2', name: '评审工作', basicParam: '评审次数', required: false, unit: '万元/次', conversion: 10000, maxPrice: 10, minPrice: 5, defPrice: 7.5, desc: '中型评审' },
|
||||
7: { serviceID: 16, ref: 'C5-3-3', name: '评审工作', basicParam: '评审次数', required: false, unit: '万元/次', conversion: 10000, maxPrice: 6, minPrice: 3, defPrice: 4.5, desc: '小型评审' },
|
||||
8: { serviceID: 17, ref: 'C6-1', name: '组织与调研工作', basicParam: '调研次数', required: true, unit: '万元/次', conversion: 10000, maxPrice: 2, minPrice: 1, defPrice: 1.5, desc: '' },
|
||||
9: { serviceID: 17, ref: 'C6-2-1', name: '研究及编写报告', basicParam: '文件份数', required: true, unit: '万元/份', conversion: 10000, maxPrice: 50, minPrice: 20, defPrice: 35, desc: '国家级' },
|
||||
10: { serviceID: 17, ref: 'C6-2-2', name: '研究及编写报告', basicParam: '文件份数', required: true, unit: '万元/份', conversion: 10000, maxPrice: 20, minPrice: 10, defPrice: 15, desc: '省部级' },
|
||||
11: { serviceID: 17, ref: 'C6-2-3', name: '研究及编写报告', basicParam: '文件份数', required: true, unit: '万元/份', conversion: 10000, maxPrice: 10, minPrice: 5, defPrice: 7.5, desc: '其他级' },
|
||||
12: { serviceID: 17, ref: 'C6-3-1', name: '标准与技术性指导文件的编制', basicParam: '文件与标准的数量', required: true, unit: '万元/份', conversion: 10000, maxPrice: 80, minPrice: 50, defPrice: 65, desc: '复杂标准' },
|
||||
13: { serviceID: 17, ref: 'C6-3-2', name: '标准与技术性指导文件的编制', basicParam: '文件与标准的数量', required: true, unit: '万元/份', conversion: 10000, maxPrice: 50, minPrice: 20, defPrice: 35, desc: '较复杂标准' },
|
||||
14: { serviceID: 17, ref: 'C6-3-3', name: '标准与技术性指导文件的编制', basicParam: '文件与标准的数量', required: true, unit: '万元/份', conversion: 10000, maxPrice: 20, minPrice: 10, defPrice: 15, desc: '一般标准' },
|
||||
15: { serviceID: 17, ref: 'C6-3-4', name: '标准与技术性指导文件的编制', basicParam: '文件与标准的数量', required: true, unit: '万元/份', conversion: 10000, maxPrice: 10, minPrice: 5, defPrice: 7.5, desc: '简单标准' },
|
||||
16: { serviceID: 17, ref: 'C6-4-1', name: '评审与验收工作', basicParam: '评审与验收次数', required: false, unit: '万元/次', conversion: 10000, maxPrice: 20, minPrice: 8, defPrice: 14, desc: '大型评审' },
|
||||
17: { serviceID: 17, ref: 'C6-4-2', name: '评审与验收工作', basicParam: '评审与验收次数', required: false, unit: '万元/次', conversion: 10000, maxPrice: 10, minPrice: 5, defPrice: 7.5, desc: '中型评审' },
|
||||
18: { serviceID: 17, ref: 'C6-4-3', name: '评审与验收工作', basicParam: '评审与验收次数', required: false, unit: '万元/次', conversion: 10000, maxPrice: 6, minPrice: 3, defPrice: 4.5, desc: '小型评审' },
|
||||
19: { serviceID: 17, ref: 'C6-5-1', name: '培训与宣贯工作', basicParam: '项目数量', required: false, unit: '万元/次', conversion: 10000, maxPrice: 3, minPrice: 1, defPrice: 2, desc: '培训与宣贯材料' },
|
||||
20: { serviceID: 17, ref: 'C6-5-2', name: '培训与宣贯工作', basicParam: '培训与宣贯次数', required: false, unit: '万元/次', conversion: 10000, maxPrice: 1, minPrice: 0.5, defPrice: 0.75, desc: '组织培训与宣贯' },
|
||||
21: { serviceID: 17, ref: 'C6-6', name: '测试与验证工作', basicParam: '', required: false, unit: '%', conversion: 0.01, maxPrice: 50, minPrice: 30, defPrice: 40, desc: '' },
|
||||
22: { serviceID: 18, ref: 'C7-1', name: '组织与调研工作', basicParam: '调研次数', required: true, unit: '万元/次', conversion: 10000, maxPrice: 2, minPrice: 1, defPrice: 1.5, desc: '' },
|
||||
23: { serviceID: 18, ref: 'C7-2', name: '编制大纲', basicParam: '项目数量', required: true, unit: '万元/个', conversion: 10000, maxPrice: 3, minPrice: 2, defPrice: 2.5, desc: '包括技术与定额子目研究' },
|
||||
24: { serviceID: 18, ref: 'C7-3', name: '数据采集与测定', basicParam: '采集组数', required: true, unit: '万元/组', conversion: 10000, maxPrice: 0.8, minPrice: 0.2, defPrice: 0.5, desc: '现场采集方式时计' },
|
||||
25: { serviceID: 18, ref: 'C7-4-1', name: '数据整理与分析', basicParam: '定额子目条数', required: true, unit: '万元/条', conversion: 10000, maxPrice: 0.3, minPrice: 0.1, defPrice: 0.2, desc: '简单定额' },
|
||||
26: { serviceID: 18, ref: 'C7-4-2', name: '数据整理与分析', basicParam: '定额子目条数', required: true, unit: '万元/条', conversion: 10000, maxPrice: 3, minPrice: 0.3, defPrice: 1.65, desc: '复杂定额' },
|
||||
27: { serviceID: 18, ref: 'C7-5', name: '编写定额测定报告', basicParam: '项目数量', required: true, unit: '万元/份', conversion: 10000, maxPrice: 5, minPrice: 2, defPrice: 3.5, desc: '' },
|
||||
28: { serviceID: 18, ref: 'C7-6-1', name: '编制定额文本和释义', basicParam: '基本费用', required: true, unit: '万元/份', conversion: 10000, maxPrice: 1, minPrice: 0.5, defPrice: 0.75, desc: '20条定额子目内' },
|
||||
29: { serviceID: 18, ref: 'C7-6-2', name: '编制定额文本和释义', basicParam: '定额子目条数', required: true, unit: '万元/条', conversion: 10000, maxPrice: 0.2, minPrice: 0.1, defPrice: 0.15, desc: '超过20条每增加1条' },
|
||||
30: { serviceID: 18, ref: 'C7-7-1', name: '评审与验收工作', basicParam: '评审与验收次数', required: false, unit: '万元/次', conversion: 10000, maxPrice: 20, minPrice: 8, defPrice: 14, desc: '大型评审' },
|
||||
31: { serviceID: 18, ref: 'C7-7-2', name: '评审与验收工作', basicParam: '评审与验收次数', required: false, unit: '万元/次', conversion: 10000, maxPrice: 10, minPrice: 5, defPrice: 7.5, desc: '中型评审' },
|
||||
32: { serviceID: 18, ref: 'C7-7-3', name: '评审与验收工作', basicParam: '评审与验收次数', required: false, unit: '万元/次', conversion: 10000, maxPrice: 6, minPrice: 3, defPrice: 4.5, desc: '小型评审' },
|
||||
33: { serviceID: 18, ref: 'C7-8-1', name: '培训与宣贯工作', basicParam: '项目数量', required: false, unit: '万元/次', conversion: 10000, maxPrice: 3, minPrice: 1, defPrice: 2, desc: '培训与宣贯材料' },
|
||||
34: { serviceID: 18, ref: 'C7-8-2', name: '培训与宣贯工作', basicParam: '培训与宣贯次数', required: false, unit: '万元/次', conversion: 10000, maxPrice: 1, minPrice: 0.5, defPrice: 0.75, desc: '组织培训与宣贯' },
|
||||
35: { serviceID: 18, ref: 'C7-9', name: '定额测试与验证', basicParam: '', required: false, unit: '%', conversion: 0.01, maxPrice: 50, minPrice: 30, defPrice: 40, desc: '' },
|
||||
36: { serviceID: 19, ref: 'C8-1', name: 'Q≤10条', basicParam: '价格信息数量', required: true, unit: '元/条', conversion: 1, maxPrice: null, minPrice: null, defPrice: 500, desc: '' },
|
||||
37: { serviceID: 19, ref: 'C8-2', name: '10条<Q≤30条', basicParam: '价格信息数量', required: true, unit: '元/条', conversion: 1, maxPrice: null, minPrice: null, defPrice: 400, desc: '' },
|
||||
38: { serviceID: 19, ref: 'C8-3', name: '30条<Q≤50条', basicParam: '价格信息数量', required: true, unit: '元/条', conversion: 1, maxPrice: null, minPrice: null, defPrice: 300, desc: '' },
|
||||
39: { serviceID: 19, ref: 'C8-4', name: '50条<Q≤100条', basicParam: '价格信息数量', required: true, unit: '元/条', conversion: 1, maxPrice: null, minPrice: null, defPrice: 200, desc: '' },
|
||||
40: { serviceID: 19, ref: 'C8-5', name: 'Q>100条', basicParam: '价格信息数量', required: true, unit: '元/条', conversion: 1, maxPrice: null, minPrice: null, defPrice: 100, desc: '' }
|
||||
|
||||
};
|
||||
|
||||
export const expertList = {
|
||||
0: { code: 'C9-1-1', name: '技术员及其他', maxPrice: 800, minPrice: 600, defPrice: 700, manageCoe: 2.3 },
|
||||
1: { code: 'C9-1-2', name: '助理工程师', maxPrice: 1000, minPrice: 800, defPrice: 900, manageCoe: 2.3 },
|
||||
@@ -145,51 +153,799 @@ export const expertList = {
|
||||
{ code: 'C2-6', staLine: 5000, endLine: null, basic: { staPrice: 346000, rate: 20 }, optional: { staPrice: 69200, rate: 4 } },
|
||||
];
|
||||
|
||||
// TODO: 补充信息服务规模计价区间后替换空数组
|
||||
const infoServiceScaleCal: Array<{ staLine: number; endLine: number | null; staPrice: number; rate: number }> = []
|
||||
|
||||
const inRange = (sv: number, staLine: number, endLine: number | null) =>
|
||||
sv >= staLine && (endLine == null || sv <= endLine)
|
||||
staLine < sv && (endLine == null || sv <= endLine)
|
||||
|
||||
export function getBasicFeeFromScale(scaleValue: unknown, scaleType: 'cost' | 'area' | 'amount') {
|
||||
const calcScaleFee = (params: {
|
||||
staPrice: number
|
||||
sv: number
|
||||
staLine: number
|
||||
rate: number
|
||||
multiplier?: number
|
||||
}) => {
|
||||
const multiplier = params.multiplier ?? 1
|
||||
return roundTo(
|
||||
toDecimal(params.staPrice).plus(
|
||||
toDecimal(params.sv)
|
||||
.minus(params.staLine)
|
||||
.mul(multiplier)
|
||||
.mul(params.rate)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export function getBasicFeeFromScale1(scaleValue: unknown, scaleType: 'cost' | 'area' | 'amount') {
|
||||
const sv = Number(scaleValue)
|
||||
if (!Number.isFinite(sv) || sv <= 0) return null
|
||||
|
||||
if (scaleType === 'cost') {// 造价规模
|
||||
if (scaleType === 'cost') {
|
||||
const targetRange = costScaleCal.find(f => inRange(sv, f.staLine, f.endLine))
|
||||
if (!targetRange) return null
|
||||
const delta = toDecimal(sv).minus(targetRange.staLine)
|
||||
return {
|
||||
basic: roundTo(
|
||||
toDecimal(targetRange.basic.staPrice).plus(delta.mul(10000).mul(targetRange.basic.rate))
|
||||
),
|
||||
optional: roundTo(
|
||||
toDecimal(targetRange.optional.staPrice).plus(delta.mul(10000).mul(targetRange.optional.rate))
|
||||
),
|
||||
basic: calcScaleFee({
|
||||
staPrice: targetRange.basic.staPrice,
|
||||
sv,
|
||||
staLine: targetRange.staLine,
|
||||
rate: targetRange.basic.rate,
|
||||
multiplier: 10000
|
||||
}),
|
||||
optional: calcScaleFee({
|
||||
staPrice: targetRange.optional.staPrice,
|
||||
sv,
|
||||
staLine: targetRange.staLine,
|
||||
rate: targetRange.optional.rate,
|
||||
multiplier: 10000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (scaleType === 'area') {//用地
|
||||
if (scaleType === 'area') {
|
||||
const targetRange = areaScaleCal.find(f => inRange(sv, f.staLine, f.endLine))
|
||||
if (!targetRange) return null
|
||||
const delta = toDecimal(sv).minus(targetRange.staLine)
|
||||
return {
|
||||
basic: roundTo(
|
||||
toDecimal(targetRange.basic.staPrice).plus(delta.mul(targetRange.basic.rate))
|
||||
),
|
||||
optional: roundTo(
|
||||
toDecimal(targetRange.optional.staPrice).plus(delta.mul(targetRange.optional.rate))
|
||||
),
|
||||
basic: calcScaleFee({
|
||||
staPrice: targetRange.basic.staPrice,
|
||||
sv,
|
||||
staLine: targetRange.staLine,
|
||||
rate: targetRange.basic.rate
|
||||
}),
|
||||
optional: calcScaleFee({
|
||||
staPrice: targetRange.optional.staPrice,
|
||||
sv,
|
||||
staLine: targetRange.staLine,
|
||||
rate: targetRange.optional.rate
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const targetRange = infoServiceScaleCal.find(f => inRange(sv, f.staLine, f.endLine))
|
||||
if (!targetRange) return null
|
||||
const delta = toDecimal(sv).minus(targetRange.staLine)
|
||||
return {
|
||||
basic: roundTo(
|
||||
toDecimal(targetRange.staPrice).plus(delta.mul(targetRange.rate))
|
||||
),
|
||||
optional: 0,
|
||||
basic: calcScaleFee({
|
||||
staPrice: targetRange.staPrice,
|
||||
sv,
|
||||
staLine: targetRange.staLine,
|
||||
rate: targetRange.rate
|
||||
}),
|
||||
optional: 0
|
||||
}
|
||||
}
|
||||
|
||||
export function getBasicFeeFromScale(scaleValue: unknown, scaleType: 'cost' | 'area' | 'amount') {
|
||||
return getBasicFeeFromScale1(scaleValue, scaleType)
|
||||
}
|
||||
|
||||
|
||||
async function exportFile(fileName, data) {
|
||||
if (window.showSaveFilePicker) {
|
||||
const handle = await window.showSaveFilePicker({
|
||||
suggestedName: fileName,
|
||||
types: [{
|
||||
description: "Excel 文件",
|
||||
accept: { "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [".xlsx"] }
|
||||
}]
|
||||
});
|
||||
// ecCom.WeaLoadingGlobal.start({
|
||||
// tip: "下载中,结束前请勿打开文件...",
|
||||
// });
|
||||
try {
|
||||
const workbook = await generateTemplate(data);
|
||||
const buffer = await workbook.xlsx.writeBuffer();
|
||||
const writable = await handle.createWritable();
|
||||
await writable.write(buffer);
|
||||
await writable.close();
|
||||
// ecCom.WeaLoadingGlobal.destroy();
|
||||
// antd.notification['success']({
|
||||
// message: '下载成功!',
|
||||
// });
|
||||
} catch (err) {
|
||||
console.log('err:' + err);
|
||||
// ecCom.WeaLoadingGlobal.destroy();
|
||||
// antd.notification['error']({
|
||||
// message: '下载失败!',
|
||||
// });
|
||||
}
|
||||
} else {
|
||||
const workbook = await generateTemplate(data);
|
||||
const buffer = await workbook.xlsx.writeBuffer();
|
||||
const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
||||
const url1 = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url1;
|
||||
a.download = `${fileName}.xlsx`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url1);
|
||||
}
|
||||
}
|
||||
|
||||
async function generateTemplate(data) {
|
||||
// 获取模板
|
||||
let templateExcel = 'template20260226001test009';
|
||||
let templateUrl = `https://oa.zwgczx.com/myExcelTemplate/${templateExcel}.xlsx`;
|
||||
let buf = await (await fetch(templateUrl)).arrayBuffer();
|
||||
let workbook = new ExcelJS.Workbook();
|
||||
await workbook.xlsx.load(buf);
|
||||
|
||||
// 生成表格
|
||||
let fm_sheet = workbook.getWorksheet('封面');
|
||||
let ml_sheet = workbook.getWorksheet('目录');
|
||||
let yz01_sheet = workbook.getWorksheet('预总01表');
|
||||
let f01_sheet = workbook.getWorksheet('辅01表');
|
||||
|
||||
let now = new Date();
|
||||
let year = now.getFullYear();
|
||||
let month = now.getMonth() + 1;
|
||||
let day = now.getDate();
|
||||
|
||||
fm_sheet.getRow(2).getCell(1).value = data.name;
|
||||
fm_sheet.getRow(11).getCell(4).value = `${year} 年 ${month} 月 ${day} 日`;
|
||||
|
||||
let yz01Mod = (data.contracts.length + 1) % 4;
|
||||
let yz01Num = (data.contracts.length + 1 - yz01Mod) / 4;
|
||||
switch (yz01Mod) {
|
||||
case 0:
|
||||
yz01_sheet.spliceColumns(8, 15);
|
||||
break;
|
||||
case 1:
|
||||
yz01_sheet.spliceColumns(8, 11);
|
||||
break;
|
||||
case 2:
|
||||
yz01_sheet.spliceColumns(19, 4);
|
||||
yz01_sheet.spliceColumns(8, 6);
|
||||
break;
|
||||
case 3:
|
||||
yz01_sheet.spliceColumns(14, 9);
|
||||
break;
|
||||
}
|
||||
if (yz01Num == 0) {
|
||||
yz01_sheet.spliceColumns(1, 7);
|
||||
} else {
|
||||
if (yz01Num > 1) {
|
||||
for (let i = 0; i < yz01Num - 1; i++) {
|
||||
insertAndCopyColumn(7 * (i + 1) + 1, [1, 2, 3, 4, 5, 6, 7], yz01_sheet);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < yz01Num; i++) {
|
||||
yz01_sheet.mergeCells(6, i * 7 + 2, 6, i * 7 + 7);
|
||||
}
|
||||
}
|
||||
if (yz01Mod > 0) {
|
||||
yz01_sheet.mergeCells(6, yz01Num * 7 + 2, 6, yz01Num * 7 + 3 + yz01Mod);
|
||||
}
|
||||
|
||||
let f01Mod = (data.contracts.length) % 3;
|
||||
let f01Num = (data.contracts.length - f01Mod) / 3;
|
||||
switch (f01Mod) {
|
||||
case 0:
|
||||
f01_sheet.spliceColumns(11, 14);
|
||||
break;
|
||||
case 1:
|
||||
f01_sheet.spliceColumns(11, 8);
|
||||
break;
|
||||
case 2:
|
||||
f01_sheet.spliceColumns(19, 6);
|
||||
break;
|
||||
}
|
||||
if (f01Num == 0) {
|
||||
f01_sheet.spliceColumns(1, 10);
|
||||
} else {
|
||||
if (f01Num > 1) {
|
||||
for (let i = 0; i < f01Num - 1; i++) {
|
||||
insertAndCopyColumn(10 * (i + 1) + 1, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], f01_sheet);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < f01Num; i++) {
|
||||
f01_sheet.mergeCells(1, i * 10 + 1, 2, i * 10 + 1);
|
||||
f01_sheet.mergeCells(1, i * 10 + 2, 2, i * 10 + 2);
|
||||
f01_sheet.mergeCells(1, i * 10 + 3, 2, i * 10 + 3);
|
||||
f01_sheet.mergeCells(1, i * 10 + 10, 2, i * 10 + 10);
|
||||
f01_sheet.mergeCells(1, i * 10 + 4, 1, i * 10 + 5);
|
||||
f01_sheet.mergeCells(1, i * 10 + 6, 1, i * 10 + 7);
|
||||
f01_sheet.mergeCells(1, i * 10 + 8, 1, i * 10 + 9);
|
||||
}
|
||||
}
|
||||
if (f01Mod > 0) {
|
||||
f01_sheet.mergeCells(1, f01Num * 10 + 1, 2, f01Num * 10 + 1);
|
||||
f01_sheet.mergeCells(1, f01Num * 10 + 2, 2, f01Num * 10 + 2);
|
||||
f01_sheet.mergeCells(1, f01Num * 10 + 3, 2, f01Num * 10 + 3);
|
||||
f01_sheet.mergeCells(1, f01Num * 10 + 2 * f01Mod + 4, 2, f01Num * 10 + 2 * f01Mod + 4);
|
||||
f01_sheet.mergeCells(1, f01Num * 10 + 4, 1, f01Num * 10 + 5);
|
||||
if (f01Mod == 2) f01_sheet.mergeCells(1, f01Num * 10 + 6, 1, f01Num * 10 + 7);
|
||||
}
|
||||
|
||||
let ml_slotRow = 13;
|
||||
let ml_number = 1;
|
||||
let allServices = [];
|
||||
data.contracts.forEach((ci, index) => {
|
||||
ci.method1 = [];
|
||||
ci.method2 = [];
|
||||
ci.method3 = [];
|
||||
ci.method4 = [];
|
||||
ci.services.forEach(si => {
|
||||
if (si.method1) {
|
||||
ci.method1.push(si.id);
|
||||
}
|
||||
if (si.method2) {
|
||||
ci.method2.push(si.id);
|
||||
}
|
||||
if (si.method3) ci.method3.push(si.id);
|
||||
if (si.method4) ci.method4.push(si.id);
|
||||
});
|
||||
|
||||
let ml_sourceRows = [ml_sheet.getRow(6)];
|
||||
let sheet_1 = copyWorksheet(workbook, '预i-1表', `预${index + 1}-1表`);
|
||||
let sheet_2;
|
||||
let sheet_2_1;
|
||||
let sheet_2_2;
|
||||
let sheet_3;
|
||||
let sheet_4;
|
||||
let sheet_4_1;
|
||||
if (ci.method1.length || ci.method2.length) {
|
||||
ml_sourceRows.push(ml_sheet.getRow(7));
|
||||
sheet_2 = copyWorksheet(workbook, '预i-2表', `预${index + 1}-2表`);
|
||||
if (ci.method1.length) {
|
||||
ml_sourceRows.push(ml_sheet.getRow(8));
|
||||
sheet_2_1 = copyWorksheet(workbook, '预i-2-1表', `预${index + 1}-2-1表`);
|
||||
}
|
||||
if (ci.method2.length) {
|
||||
ml_sourceRows.push(ml_sheet.getRow(9));
|
||||
sheet_2_2 = copyWorksheet(workbook, '预i-2-2表', `预${index + 1}-2-2表`);
|
||||
}
|
||||
}
|
||||
if (ci.method3.length) {
|
||||
ml_sourceRows.push(ml_sheet.getRow(10));
|
||||
sheet_3 = copyWorksheet(workbook, '预i-3表', `预${index + 1}-3表`);
|
||||
}
|
||||
if (ci.method4.length) {
|
||||
ml_sourceRows.push(ml_sheet.getRow(11));
|
||||
ml_sourceRows.push(ml_sheet.getRow(12));
|
||||
sheet_4 = copyWorksheet(workbook, '预i-4表', `预${index + 1}-4表`);
|
||||
sheet_4_1 = copyWorksheet(workbook, '预i-4-1表', `预${index + 1}-4-1表`);
|
||||
}
|
||||
|
||||
cusInsertRowFunc(ml_slotRow, ml_sourceRows, ml_sheet, () => ml_slotRow++, (targetCell, sourceCell, colNumber) => {
|
||||
if (colNumber == 1) {
|
||||
targetCell.value = ml_number++;
|
||||
} else if (colNumber == 2) {
|
||||
targetCell.value = sourceCell.value.replaceAll('第i合同', ci.name);
|
||||
} else if (colNumber == 3) {
|
||||
targetCell.value = sourceCell.value.replaceAll('i', index + 1);
|
||||
} else {
|
||||
targetCell.value = sourceCell.value;
|
||||
}
|
||||
});
|
||||
|
||||
let num_2 = 1;
|
||||
let num_2_1 = 1;
|
||||
let num_2_2 = 1;
|
||||
let num_3 = 1;
|
||||
let num_4 = 1;
|
||||
let num_4_1 = 1;
|
||||
ci.services.forEach((sobj, sindex) => {
|
||||
let allServicesX = allServices.find(s => s.id == sobj.id);
|
||||
if (allServicesX) {
|
||||
allServicesX.contracts[index] = sobj.fee;
|
||||
} else {
|
||||
allServices.push({
|
||||
id: sobj.id,
|
||||
contracts: {
|
||||
[index]: sobj.fee,
|
||||
},
|
||||
});
|
||||
}
|
||||
let serviceX = serviceList[sobj.id];
|
||||
cusInsertRowFunc(4 + sindex, [sheet_1.getRow(3)], sheet_1, (targetRow) => {
|
||||
targetRow.getCell(1).value = sindex + 1;
|
||||
targetRow.getCell(2).value = serviceX.ref;
|
||||
targetRow.getCell(3).value = serviceX.name;
|
||||
targetRow.getCell(4).value = sobj.method1 ? sobj.method1.fee : '';
|
||||
targetRow.getCell(5).value = sobj.method2 ? sobj.method2.fee : '';
|
||||
targetRow.getCell(6).value = sobj.method3 ? sobj.method3.fee : '';
|
||||
targetRow.getCell(7).value = sobj.method4 ? sobj.method4.fee : '';
|
||||
targetRow.getCell(8).value = sobj.fee;
|
||||
});
|
||||
if (sobj.method1 || sobj.method2) {
|
||||
let det1 = sobj.method1 ? sobj.method1.det.map(m => m.major) : [];
|
||||
let det2 = sobj.method2 ? sobj.method2.det.map(m => m.major) : [];
|
||||
let allDet = [...(new Set([...det1, ...det2]))].sort((a, b) => a - b).map(m => {
|
||||
return {
|
||||
major: m,
|
||||
mth1: det1.includes(m) ? sobj.method1.det[det1.indexOf(m)] : null,
|
||||
mth2: det2.includes(m) ? sobj.method2.det[det2.indexOf(m)] : null,
|
||||
};
|
||||
});
|
||||
|
||||
cusInsertRowFunc(4 + num_2, [sheet_2.getRow(4)], sheet_2, (targetRow) => {
|
||||
targetRow.getCell(1).value = num_2++;
|
||||
targetRow.getCell(2).value = serviceX.ref;
|
||||
targetRow.getCell(3).value = serviceX.name;
|
||||
targetRow.getCell(4).value = '/';
|
||||
targetRow.getCell(5).value = '/';
|
||||
if (sobj.method1) {
|
||||
targetRow.getCell(6).value = sobj.method1.basicFee;
|
||||
targetRow.getCell(7).value = sobj.method1.fee;
|
||||
cusInsertRowFunc(4 + num_2_1, [sheet_2_1.getRow(4)], sheet_2_1, (targetRow) => {
|
||||
targetRow.getCell(1).value = num_2_1++;
|
||||
targetRow.getCell(2).value = serviceX.ref;
|
||||
targetRow.getCell(3).value = serviceX.name;
|
||||
targetRow.getCell(4).value = sobj.method1.cost;
|
||||
targetRow.getCell(5).value = '/';
|
||||
targetRow.getCell(6).value = sobj.method1.basicFee_basic;
|
||||
targetRow.getCell(7).value = '/';
|
||||
targetRow.getCell(8).value = sobj.method1.basicFee_optional;
|
||||
targetRow.getCell(9).value = sobj.method1.basicFee;
|
||||
});
|
||||
}
|
||||
if (sobj.method2) {
|
||||
targetRow.getCell(8).value = sobj.method2.basicFee;
|
||||
targetRow.getCell(9).value = sobj.method2.fee;
|
||||
cusInsertRowFunc(4 + num_2_2, [sheet_2_2.getRow(4)], sheet_2_2, (targetRow) => {
|
||||
targetRow.getCell(1).value = num_2_2++;
|
||||
targetRow.getCell(2).value = serviceX.ref;
|
||||
targetRow.getCell(3).value = serviceX.name;
|
||||
targetRow.getCell(4).value = sobj.method2.area;
|
||||
targetRow.getCell(5).value = '/';
|
||||
targetRow.getCell(6).value = sobj.method2.basicFee_basic;
|
||||
targetRow.getCell(7).value = '/';
|
||||
targetRow.getCell(8).value = sobj.method2.basicFee_optional;
|
||||
targetRow.getCell(9).value = sobj.method2.basicFee;
|
||||
});
|
||||
}
|
||||
});
|
||||
allDet.forEach((m, mindex) => {
|
||||
let majorX = majorList[m.major];
|
||||
cusInsertRowFunc(4 + num_2, [sheet_2.getRow(4)], sheet_2, (targetRow) => {
|
||||
targetRow.getCell(1).value = num_2++;
|
||||
targetRow.getCell(2).value = serviceX.ref + '-' + (mindex + 1);
|
||||
targetRow.getCell(3).value = majorX.name;
|
||||
if (m.mth1) {
|
||||
targetRow.getCell(4).value = m.mth1.serviceCoe;
|
||||
targetRow.getCell(5).value = m.mth1.majorCoe;
|
||||
targetRow.getCell(6).value = m.mth1.basicFee;
|
||||
targetRow.getCell(7).value = m.mth1.fee;
|
||||
targetRow.getCell(8).value = 0;
|
||||
targetRow.getCell(9).value = 0;
|
||||
cusInsertRowFunc(4 + num_2_1, [sheet_2_1.getRow(4)], sheet_2_1, (targetRow) => {
|
||||
targetRow.getCell(1).value = num_2_1++;
|
||||
targetRow.getCell(2).value = serviceX.ref + '-' + (mindex + 1);
|
||||
targetRow.getCell(3).value = majorX.name;
|
||||
targetRow.getCell(4).value = m.mth1.cost;
|
||||
targetRow.getCell(5).value = m.mth1.basicFormula;
|
||||
targetRow.getCell(6).value = m.mth1.basicFee_basic;
|
||||
targetRow.getCell(7).value = m.mth1.optionalFormula;
|
||||
targetRow.getCell(8).value = m.mth1.basicFee_optional;
|
||||
targetRow.getCell(9).value = m.mth1.basicFee;
|
||||
});
|
||||
} else {
|
||||
targetRow.getCell(4).value = m.mth2.serviceCoe;
|
||||
targetRow.getCell(5).value = m.mth2.majorCoe;
|
||||
targetRow.getCell(6).value = 0;
|
||||
targetRow.getCell(7).value = 0;
|
||||
targetRow.getCell(8).value = m.mth2.basicFee;
|
||||
targetRow.getCell(9).value = m.mth2.fee;
|
||||
cusInsertRowFunc(4 + num_2_2, [sheet_2_2.getRow(4)], sheet_2_2, (targetRow) => {
|
||||
targetRow.getCell(1).value = num_2_2++;
|
||||
targetRow.getCell(2).value = serviceX.ref + '-' + (mindex + 1);
|
||||
targetRow.getCell(3).value = majorX.name;
|
||||
targetRow.getCell(4).value = m.mth2.area;
|
||||
targetRow.getCell(5).value = m.mth2.basicFormula;
|
||||
targetRow.getCell(6).value = m.mth2.basicFee_basic;
|
||||
targetRow.getCell(7).value = m.mth2.optionalFormula;
|
||||
targetRow.getCell(8).value = m.mth2.basicFee_optional;
|
||||
targetRow.getCell(9).value = m.mth2.basicFee;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
if (sobj.method3) {
|
||||
cusInsertRowFunc(3 + num_3, [sheet_3.getRow(3)], sheet_3, (targetRow) => {
|
||||
targetRow.getCell(1).value = num_3++;
|
||||
targetRow.getCell(2).value = serviceX.ref;
|
||||
targetRow.getCell(3).value = serviceX.name;
|
||||
targetRow.getCell(4).value = '/';
|
||||
targetRow.getCell(5).value = '/';
|
||||
targetRow.getCell(6).value = '/';
|
||||
targetRow.getCell(7).value = sobj.method3.basicFee;
|
||||
targetRow.getCell(8).value = '/';
|
||||
targetRow.getCell(9).value = sobj.method3.fee;
|
||||
});
|
||||
sobj.method3.det.forEach((tobj, tindex) => {
|
||||
const taskX = taskList[tobj.task];
|
||||
cusInsertRowFunc(3 + num_3, [sheet_3.getRow(3)], sheet_3, (targetRow) => {
|
||||
targetRow.getCell(1).value = num_3++;
|
||||
targetRow.getCell(2).value = taskX.ref;
|
||||
targetRow.getCell(3).value = taskX.name + (taskX.desc ? `(${taskX.desc})` : '');
|
||||
targetRow.getCell(4).value = taskX.basicParam;
|
||||
targetRow.getCell(5).value = tobj.price;
|
||||
targetRow.getCell(6).value = tobj.amount;
|
||||
targetRow.getCell(7).value = tobj.basicFee;
|
||||
targetRow.getCell(8).value = tobj.serviceCoe;
|
||||
targetRow.getCell(9).value = tobj.fee;
|
||||
});
|
||||
});
|
||||
}
|
||||
if (sobj.method4) {
|
||||
cusInsertRowFunc(4 + num_4, [sheet_4.getRow(4)], sheet_4, (targetRow) => {
|
||||
targetRow.getCell(1).value = num_4++;
|
||||
targetRow.getCell(2).value = serviceX.ref;
|
||||
targetRow.getCell(3).value = serviceX.name;
|
||||
targetRow.getCell(4).value = sobj.method4.person_num;
|
||||
targetRow.getCell(5).value = sobj.method4.work_day;
|
||||
targetRow.getCell(6).value = sobj.method4.fee;
|
||||
});
|
||||
cusInsertRowFunc(5 + num_4_1, [sheet_4_1.getRow(5)], sheet_4_1, (targetRow) => {
|
||||
targetRow.getCell(1).value = num_4_1++;
|
||||
targetRow.getCell(2).value = serviceX.ref;
|
||||
targetRow.getCell(3).value = serviceX.name;
|
||||
targetRow.getCell(4).value = '/';
|
||||
targetRow.getCell(5).value = '/';
|
||||
targetRow.getCell(6).value = '/';
|
||||
targetRow.getCell(7).value = sobj.method4.person_num;
|
||||
targetRow.getCell(8).value = sobj.method4.work_day;
|
||||
targetRow.getCell(9).value = sobj.method4.fee;
|
||||
});
|
||||
sobj.method4.det.forEach((eobj, eindex) => {
|
||||
const expertX = expertList[eobj.expert];
|
||||
cusInsertRowFunc(5 + num_4_1, [sheet_4_1.getRow(5)], sheet_4_1, (targetRow) => {
|
||||
targetRow.getCell(1).value = num_4_1++;
|
||||
targetRow.getCell(2).value = expertX.ref;
|
||||
targetRow.getCell(3).value = expertX.name;
|
||||
targetRow.getCell(4).value = `${expertX.minPrice}~${expertX.maxPrice}`;
|
||||
targetRow.getCell(5).value = `${Math.round(expertX.minPrice * expertX.manageCoe)}~${Math.round(expertX.maxPrice * expertX.manageCoe)}`;
|
||||
targetRow.getCell(6).value = eobj.price;
|
||||
targetRow.getCell(7).value = eobj.person_num;
|
||||
targetRow.getCell(8).value = eobj.work_day;
|
||||
targetRow.getCell(9).value = eobj.fee;
|
||||
targetRow.getCell(10).value = eobj.desc;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
allServices.sort((a, b) => a.id - b.id);
|
||||
allServices.forEach((s, sindex) => {
|
||||
const serviceX = serviceList[s.id];
|
||||
cusInsertRowFunc(3 + sindex, [yz01_sheet.getRow(2)], yz01_sheet, (targetRow) => {
|
||||
const sumCol = data.contracts.length;
|
||||
let siSum = 0;
|
||||
for (let i = 0; i < yz01Num; i++) {
|
||||
targetRow.getCell(i * 7 + 1).value = sindex + 1;
|
||||
targetRow.getCell(i * 7 + 2).value = serviceX.ref;
|
||||
targetRow.getCell(i * 7 + 3).value = serviceX.name;
|
||||
targetRow.getCell(i * 7 + 4).value = s.contracts[i * 4];
|
||||
targetRow.getCell(i * 7 + 5).value = s.contracts[i * 4 + 1];
|
||||
targetRow.getCell(i * 7 + 6).value = s.contracts[i * 4 + 2];
|
||||
siSum = siSum + (Number(s.contracts[i * 4]) || 0 + Number(s.contracts[i * 4 + 1]) || 0 + Number(s.contracts[i * 4 + 2]) || 0);
|
||||
if (i * 4 + 3 == sumCol) {
|
||||
targetRow.getCell(i * 7 + 7).value = numberFormatter(siSum, 2);
|
||||
} else {
|
||||
targetRow.getCell(i * 7 + 7).value = s.contracts[i * 4 + 3];
|
||||
siSum = siSum + (Number(s.contracts[i * 4 + 3]) || 0);
|
||||
}
|
||||
}
|
||||
if (yz01Mod) {
|
||||
targetRow.getCell(yz01Num * 7 + 1).value = sindex + 1;
|
||||
targetRow.getCell(yz01Num * 7 + 2).value = serviceX.ref;
|
||||
targetRow.getCell(yz01Num * 7 + 3).value = serviceX.name;
|
||||
if (yz01Mod == 1) {
|
||||
targetRow.getCell(yz01Num * 7 + 4).value = numberFormatter(siSum, 2);
|
||||
} else if (yz01Mod == 2) {
|
||||
targetRow.getCell(yz01Num * 7 + 4).value = s.contracts[yz01Num * 4];
|
||||
siSum = siSum + (Number(s.contracts[yz01Num * 4]) || 0);
|
||||
targetRow.getCell(yz01Num * 7 + 5).value = numberFormatter(siSum, 2);
|
||||
} else {
|
||||
targetRow.getCell(yz01Num * 7 + 4).value = s.contracts[yz01Num * 4];
|
||||
targetRow.getCell(yz01Num * 7 + 5).value = s.contracts[yz01Num * 4 + 1];
|
||||
siSum = siSum + (Number(s.contracts[yz01Num * 4]) || 0 + Number(s.contracts[yz01Num * 4 + 1]) || 0);
|
||||
targetRow.getCell(yz01Num * 7 + 6).value = numberFormatter(siSum, 2);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ml_sheet.spliceRows(6, 6);
|
||||
ml_sheet.spliceRows(6, 1);
|
||||
ml_sheet.mergeCells(ml_slotRow - 7, 1, ml_slotRow - 7, 4);
|
||||
|
||||
workbook.removeWorksheet('预i-1表');
|
||||
workbook.removeWorksheet('预i-2表');
|
||||
workbook.removeWorksheet('预i-2-1表');
|
||||
workbook.removeWorksheet('预i-2-2表');
|
||||
workbook.removeWorksheet('预i-3表');
|
||||
workbook.removeWorksheet('预i-4表');
|
||||
workbook.removeWorksheet('预i-4-1表');
|
||||
workbook.getWorksheet('辅01表').orderNo = ml_number + 2 + 10;
|
||||
workbook.getWorksheet('辅02表').orderNo = ml_number + 3 + 10;
|
||||
workbook.getWorksheet('辅03表').orderNo = ml_number + 4 + 10;
|
||||
|
||||
// workbook._worksheets.forEach(sheet => {
|
||||
// if (sheet) {
|
||||
// if (sheet.headerFooter.oddHeader) sheet.headerFooter.oddHeader = sheet.headerFooter.oddHeader.replace(/&([CLR])&/g, '&$1&"宋体"&');
|
||||
// if (sheet.headerFooter.oddFooter) sheet.headerFooter.oddFooter = sheet.headerFooter.oddFooter.replace(/&([CLR])&/g, '&$1&"宋体"&');
|
||||
// }
|
||||
// });
|
||||
|
||||
window.workbook = workbook;
|
||||
|
||||
return workbook;
|
||||
}
|
||||
|
||||
function cusInsertRowFunc(insertRowNum, sourceRows, worksheet, RowFun, cellFun) {
|
||||
// 插入行
|
||||
let newRows = [];
|
||||
for (let i = 0; i < sourceRows.length; i++) {
|
||||
newRows.push([]);
|
||||
}
|
||||
worksheet.insertRows(insertRowNum, newRows);
|
||||
|
||||
for (let i = 0; i < sourceRows.length; i++) {
|
||||
const sourceRow = sourceRows[i];
|
||||
const targetRowNum = insertRowNum + i;
|
||||
const targetRow = worksheet.getRow(targetRowNum);
|
||||
targetRow.height = undefined;
|
||||
sourceRow.eachCell({ includeEmpty: true }, (cell, colNumber) => {
|
||||
const targetCell = targetRow.getCell(colNumber);
|
||||
// targetCell.value = cell.value; // 复制内容
|
||||
// 复制样式
|
||||
if (cell.style) {
|
||||
targetCell.style = { ...cell.style };
|
||||
}
|
||||
|
||||
if (cellFun) {
|
||||
cellFun(targetCell, cell, colNumber, targetRow, sourceRow);
|
||||
}
|
||||
});
|
||||
if (RowFun) {
|
||||
RowFun(targetRow, sourceRow, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function copyWorksheet(workbook, sourceName, targetName) {
|
||||
const source = workbook.getWorksheet(sourceName);
|
||||
if (!source) throw new Error("Source sheet not found");
|
||||
|
||||
const target = workbook.addWorksheet(targetName, {
|
||||
properties: { ...source.properties },
|
||||
pageSetup: { ...source.pageSetup },
|
||||
views: source.views ? JSON.parse(JSON.stringify(source.views)) : [],
|
||||
});
|
||||
|
||||
/* 复制页眉页脚(关键补充) */
|
||||
if (source.headerFooter) {
|
||||
target.headerFooter = JSON.parse(
|
||||
JSON.stringify(source.headerFooter)
|
||||
);
|
||||
}
|
||||
|
||||
/* 复制合并单元格 */
|
||||
Object.keys(source._merges).forEach(range => {
|
||||
let rangeModel = source._merges[range].model;
|
||||
target.mergeCells(rangeModel.top, rangeModel.left, rangeModel.bottom, rangeModel.right);
|
||||
});
|
||||
|
||||
/* 复制列(宽度、样式) */
|
||||
source.columns.forEach((col, i) => {
|
||||
const targetCol = target.getColumn(i + 1);
|
||||
targetCol.width = col.width;
|
||||
targetCol.style = JSON.parse(JSON.stringify(col.style || {}));
|
||||
});
|
||||
|
||||
/* 复制行 & 单元格 */
|
||||
source.eachRow({ includeEmpty: true }, (row, rowNumber) => {
|
||||
const targetRow = target.getRow(rowNumber);
|
||||
targetRow.height = undefined;
|
||||
|
||||
row.eachCell({ includeEmpty: true }, (cell, colNumber) => {
|
||||
const targetCell = targetRow.getCell(colNumber);
|
||||
|
||||
// value(富文本/公式安全)
|
||||
targetCell.value = cloneCellValue(cell.value);
|
||||
|
||||
// style(必须深拷贝)
|
||||
targetCell.style = JSON.parse(JSON.stringify(cell.style || {}));
|
||||
});
|
||||
});
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
function insertAndCopyColumn(insertAt, cols, ws) {
|
||||
let insertAti = insertAt;
|
||||
cols.forEach((col, index) => {
|
||||
// 在 insertAti 位置插入空列
|
||||
ws.spliceColumns(insertAti, 0, []);
|
||||
|
||||
// 插入后,col 可能需要偏移
|
||||
const srcColIndex = col >= insertAti ? col + 1 + index : col;
|
||||
|
||||
// 复制列
|
||||
copyColumn(insertAti, srcColIndex, ws);
|
||||
|
||||
insertAti++;
|
||||
});
|
||||
}
|
||||
|
||||
function copyColumn(toCol, fromCol, ws) {
|
||||
const srcCol = ws.getColumn(fromCol);
|
||||
const dstCol = ws.getColumn(toCol);
|
||||
|
||||
// 列级属性
|
||||
dstCol.width = srcCol.width;
|
||||
dstCol.hidden = srcCol.hidden;
|
||||
dstCol.style = JSON.parse(JSON.stringify(srcCol.style || {}));
|
||||
|
||||
// 单元格
|
||||
srcCol.eachCell({ includeEmpty: true }, (cell, rowNumber) => {
|
||||
const dstCell = ws.getRow(rowNumber).getCell(toCol);
|
||||
dstCell.value = cloneCellValue(cell.value);
|
||||
dstCell.style = JSON.parse(JSON.stringify(cell.style || {}));
|
||||
});
|
||||
}
|
||||
|
||||
function cloneCellValue(value) {
|
||||
if (value == null) return value;
|
||||
|
||||
if (typeof value === "object") {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
|
||||
let data1 = {
|
||||
name: 'test001',
|
||||
fee: 10000,
|
||||
scale: [
|
||||
{
|
||||
major: 0,
|
||||
cost: 100000,
|
||||
area: 200,
|
||||
},
|
||||
{
|
||||
major: 1,
|
||||
cost: 100000,
|
||||
area: 200,
|
||||
},
|
||||
],
|
||||
contracts: [
|
||||
{
|
||||
name: 'A合同段',
|
||||
fee: 10000,
|
||||
scale: [
|
||||
{
|
||||
major: 0,
|
||||
cost: 100000,
|
||||
area: 200,
|
||||
},
|
||||
{
|
||||
major: 1,
|
||||
cost: 100000,
|
||||
area: 200,
|
||||
},
|
||||
],
|
||||
services: [
|
||||
{
|
||||
id: 0,
|
||||
fee: 100000,
|
||||
method1: { // 投资规模法
|
||||
cost: 100000,
|
||||
basicFee: 200,
|
||||
basicFee_basic: 200,
|
||||
basicFee_optional: 0,
|
||||
fee: 250000,
|
||||
det: [
|
||||
{
|
||||
major: 0,
|
||||
cost: 100000,
|
||||
basicFee: 200,
|
||||
basicFormula: '856,000+(1,000,000,000-500,000,000)×1‰',
|
||||
basicFee_basic: 200,
|
||||
optionalFormula: '171,200+(1,000,000,000-500,000,000)×0.2‰',
|
||||
basicFee_optional: 0,
|
||||
serviceCoe: 1.1,
|
||||
majorCoe: 1.2,
|
||||
fee: 100000,
|
||||
desc: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
method2: { // 用地规模法
|
||||
area: 1200,
|
||||
basicFee: 200,
|
||||
basicFee_basic: 200,
|
||||
basicFee_optional: 0,
|
||||
fee: 250000,
|
||||
det: [
|
||||
{
|
||||
major: 0,
|
||||
area: 1200,
|
||||
basicFee: 200,
|
||||
basicFormula: '106,000+(1,200-1,000)×60',
|
||||
basicFee_basic: 200,
|
||||
optionalFormula: '21,200+(1,200-1,000)×12',
|
||||
basicFee_optional: 0,
|
||||
serviceCoe: 1.1,
|
||||
majorCoe: 1.2,
|
||||
fee: 100000,
|
||||
desc: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
method3: { // 工作量法
|
||||
basicFee: 200,
|
||||
fee: 250000,
|
||||
det: [
|
||||
{
|
||||
task: 0,
|
||||
price: 100000,
|
||||
amount: 10,
|
||||
basicFee: 200,
|
||||
serviceCoe: 1.1,
|
||||
fee: 100000,
|
||||
desc: '',
|
||||
},
|
||||
{
|
||||
task: 1,
|
||||
price: 100000,
|
||||
amount: 10,
|
||||
basicFee: 200,
|
||||
serviceCoe: 1.1,
|
||||
fee: 100000,
|
||||
desc: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
method4: { // 工时法
|
||||
person_num: 10,
|
||||
work_day: 10,
|
||||
fee: 250000,
|
||||
det: [
|
||||
{
|
||||
expert: 0,
|
||||
price: 100000,
|
||||
person_num: 10,
|
||||
work_day: 3,
|
||||
fee: 100000,
|
||||
desc: '',
|
||||
},
|
||||
{
|
||||
expert: 1,
|
||||
price: 100000,
|
||||
person_num: 10,
|
||||
work_day: 3,
|
||||
fee: 100000,
|
||||
desc: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -231,5 +231,6 @@
|
||||
.xmMx .ag-cell.editable-cell-empty .ag-cell-value * {
|
||||
--ag-data-color: #94a3b8;
|
||||
color: #94a3b8 !important;
|
||||
height:100%;
|
||||
font-style: italic;
|
||||
}
|
||||
Reference in new issue
Block a user