fix
This commit is contained in:
parent
57a2029847
commit
badf131dde
3
bun.lock
3
bun.lock
@ -12,6 +12,7 @@
|
||||
"ag-grid-vue3": "^35.1.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"decimal.js": "^10.6.0",
|
||||
"localforage": "^1.10.0",
|
||||
"lucide-vue-next": "^0.563.0",
|
||||
"pinia": "^3.0.4",
|
||||
@ -225,6 +226,8 @@
|
||||
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
|
||||
|
||||
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
1870
package-lock.json
generated
1870
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -18,6 +18,7 @@
|
||||
"ag-grid-vue3": "^35.1.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"decimal.js": "^10.6.0",
|
||||
"localforage": "^1.10.0",
|
||||
"lucide-vue-next": "^0.563.0",
|
||||
"pinia": "^3.0.4",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { computed, nextTick, onActivated, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import draggable from 'vuedraggable'
|
||||
import localforage from 'localforage'
|
||||
import { Card, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
@ -52,8 +52,15 @@ let baseOffsetY = 0
|
||||
const contractListScrollWrapRef = ref<HTMLElement | null>(null)
|
||||
const contractListViewportRef = ref<HTMLElement | null>(null)
|
||||
const isDraggingContracts = ref(false)
|
||||
const cardMotionState = ref<'enter' | 'ready'>('ready')
|
||||
let contractAutoScrollRaf = 0
|
||||
let dragPointerClientY: number | null = null
|
||||
let cardEnterTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const CARD_ENTER_STEP_MS = 58
|
||||
const CARD_ENTER_DURATION_MS = 560
|
||||
const CARD_ENTER_MAX_INDEX = 24
|
||||
const CARD_ENTER_TOTAL_MS = CARD_ENTER_DURATION_MS + CARD_ENTER_STEP_MS * CARD_ENTER_MAX_INDEX + 80
|
||||
|
||||
const buildDefaultContracts = (): ContractItem[] => [
|
||||
|
||||
@ -111,6 +118,22 @@ const scrollContractsToBottom = (behavior: ScrollBehavior = 'smooth') => {
|
||||
})
|
||||
}
|
||||
|
||||
const triggerCardEnterAnimation = () => {
|
||||
if (cardEnterTimer) {
|
||||
clearTimeout(cardEnterTimer)
|
||||
cardEnterTimer = null
|
||||
}
|
||||
cardMotionState.value = 'enter'
|
||||
cardEnterTimer = setTimeout(() => {
|
||||
cardMotionState.value = 'ready'
|
||||
cardEnterTimer = null
|
||||
}, CARD_ENTER_TOTAL_MS)
|
||||
}
|
||||
|
||||
const getCardEnterStyle = (index: number) => ({
|
||||
'--ht-card-enter-delay': `${Math.min(index, CARD_ENTER_MAX_INDEX) * CARD_ENTER_STEP_MS}ms`
|
||||
})
|
||||
|
||||
const saveContracts = async () => {
|
||||
try {
|
||||
contracts.value = normalizeOrder(contracts.value)
|
||||
@ -297,6 +320,11 @@ const contractAutoScrollTick = () => {
|
||||
}
|
||||
|
||||
const startContractAutoScroll = () => {
|
||||
if (cardEnterTimer) {
|
||||
clearTimeout(cardEnterTimer)
|
||||
cardEnterTimer = null
|
||||
}
|
||||
cardMotionState.value = 'ready'
|
||||
getContractListViewport()
|
||||
isDraggingContracts.value = true
|
||||
dragPointerClientY = null
|
||||
@ -349,13 +377,22 @@ const startDrag = (event: MouseEvent) => {
|
||||
|
||||
onMounted(async () => {
|
||||
await loadContracts()
|
||||
triggerCardEnterAnimation()
|
||||
await nextTick()
|
||||
getContractListViewport()
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
triggerCardEnterAnimation()
|
||||
void nextTick(() => {
|
||||
getContractListViewport()
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopDrag()
|
||||
stopContractAutoScroll()
|
||||
if (cardEnterTimer) clearTimeout(cardEnterTimer)
|
||||
void saveContracts()
|
||||
})
|
||||
</script>
|
||||
@ -432,12 +469,14 @@ onBeforeUnmount(() => {
|
||||
@start="startContractAutoScroll"
|
||||
@end="handleDragEnd"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<template #item="{ element, index }">
|
||||
<Card
|
||||
:class="[
|
||||
'group cursor-pointer snap-start snap-always transition-colors hover:border-primary',
|
||||
'group cursor-pointer snap-start snap-always transition-colors hover:border-primary ht-contract-card',
|
||||
cardMotionState === 'enter' && !isDraggingContracts ? 'ht-contract-card--enter' : 'ht-contract-card--ready',
|
||||
isListLayout && 'gap-0 py-0'
|
||||
]"
|
||||
:style="cardMotionState === 'enter' ? getCardEnterStyle(index) : undefined"
|
||||
@click="handleCardClick(element)"
|
||||
>
|
||||
<CardHeader
|
||||
@ -530,12 +569,14 @@ onBeforeUnmount(() => {
|
||||
]"
|
||||
>
|
||||
<Card
|
||||
v-for="element in filteredContracts"
|
||||
v-for="(element, index) in filteredContracts"
|
||||
:key="element.id"
|
||||
:class="[
|
||||
'group cursor-pointer snap-start snap-always transition-colors hover:border-primary',
|
||||
'group cursor-pointer snap-start snap-always transition-colors hover:border-primary ht-contract-card',
|
||||
cardMotionState === 'enter' && !isDraggingContracts ? 'ht-contract-card--enter' : 'ht-contract-card--ready',
|
||||
isListLayout && 'gap-0 py-0'
|
||||
]"
|
||||
:style="cardMotionState === 'enter' ? getCardEnterStyle(index) : undefined"
|
||||
@click="handleCardClick(element)"
|
||||
>
|
||||
<CardHeader
|
||||
@ -705,4 +746,37 @@ onBeforeUnmount(() => {
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.ht-contract-card {
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
.ht-contract-card--ready {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
.ht-contract-card--enter {
|
||||
animation: ht-card-slide-in 560ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
|
||||
animation-delay: var(--ht-card-enter-delay, 0ms);
|
||||
}
|
||||
|
||||
@keyframes ht-card-slide-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate3d(44px, 0, 0);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ht-contract-card--enter {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { AgGridVue } from 'ag-grid-vue3'
|
||||
import type { ColDef, GridOptions } from 'ag-grid-community'
|
||||
import type { ColDef } from 'ag-grid-community'
|
||||
import localforage from 'localforage'
|
||||
import { majorList } from '@/sql'
|
||||
import 'ag-grid-enterprise'
|
||||
import { myTheme } from '@/lib/diyAgGridTheme'
|
||||
import { myTheme ,gridOptions} from '@/lib/diyAgGridOptions'
|
||||
import { decimalAggSum, sumByNumber } from '@/lib/decimal'
|
||||
|
||||
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale';
|
||||
// 精简的边框配置(细线条+浅灰色,弱化分割线视觉)
|
||||
@ -155,7 +155,7 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
aggFunc: 'sum',
|
||||
aggFunc: decimalAggSum,
|
||||
valueParser: params => {
|
||||
if (params.newValue === '' || params.newValue == null) return null
|
||||
const v = Number(params.newValue)
|
||||
@ -181,7 +181,7 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
aggFunc: 'sum',
|
||||
aggFunc: decimalAggSum,
|
||||
valueParser: params => {
|
||||
if (params.newValue === '' || params.newValue == null) return null
|
||||
const v = Number(params.newValue)
|
||||
@ -215,30 +215,9 @@ const autoGroupColumnDef: ColDef = {
|
||||
}
|
||||
}
|
||||
|
||||
const gridOptions: GridOptions<DetailRow> = {
|
||||
treeData: true,
|
||||
animateRows: true,
|
||||
singleClickEdit: true,
|
||||
suppressClickEdit: false,
|
||||
suppressContextMenu: false,
|
||||
groupDefaultExpanded: -1,
|
||||
suppressFieldDotNotation: true,
|
||||
getDataPath: data => data.path,
|
||||
getContextMenuItems: () => ['copy', 'paste', 'separator', 'export'],
|
||||
defaultColDef: {
|
||||
resizable: true,
|
||||
sortable: false,
|
||||
filter: false
|
||||
}
|
||||
}
|
||||
const totalAmount = computed(() => sumByNumber(detailRows.value, row => row.amount))
|
||||
|
||||
const totalAmount = computed(() =>
|
||||
detailRows.value.reduce((sum, row) => sum + (row.amount || 0), 0)
|
||||
)
|
||||
|
||||
const totalLandArea = computed(() =>
|
||||
detailRows.value.reduce((sum, row) => sum + (row.landArea || 0), 0)
|
||||
)
|
||||
const totalLandArea = computed(() => sumByNumber(detailRows.value, row => row.landArea))
|
||||
const pinnedTopRowData = computed(() => [
|
||||
{
|
||||
id: 'pinned-total-row',
|
||||
@ -366,32 +345,3 @@ const processCellFromClipboard = (params:any) => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style >
|
||||
.ag-floating-top{
|
||||
overflow-y:auto !important
|
||||
}
|
||||
|
||||
.xmMx .editable-cell-line .ag-cell-value {
|
||||
display: inline-block;
|
||||
min-width: 84%;
|
||||
padding: 2px 4px;
|
||||
border-bottom: 1px solid #cbd5e1;
|
||||
}
|
||||
|
||||
.xmMx .editable-cell-line.ag-cell-focus .ag-cell-value,
|
||||
.xmMx .editable-cell-line:hover .ag-cell-value {
|
||||
border-bottom-color: #2563eb;
|
||||
}
|
||||
|
||||
.xmMx .editable-cell-empty .ag-cell-value {
|
||||
color: #94a3b8 !important;
|
||||
font-style: italic;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.xmMx .ag-cell.editable-cell-empty,
|
||||
.xmMx .ag-cell.editable-cell-empty .ag-cell-value {
|
||||
color: #94a3b8 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { AgGridVue } from 'ag-grid-vue3'
|
||||
import type { ColDef, ColGroupDef, GridOptions } from 'ag-grid-community'
|
||||
import type { ColDef, ColGroupDef } from 'ag-grid-community'
|
||||
import localforage from 'localforage'
|
||||
import { majorList } from '@/sql'
|
||||
import 'ag-grid-enterprise'
|
||||
import { myTheme } from '@/lib/diyAgGridTheme'
|
||||
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
||||
import { decimalAggSum, sumByNumber } from '@/lib/decimal'
|
||||
import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
||||
|
||||
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale';
|
||||
|
||||
@ -49,87 +49,40 @@ const props = defineProps<{
|
||||
serviceId: string | number
|
||||
}>()
|
||||
const DB_KEY = computed(() => `hourlyPricing-${props.contractId}-${props.serviceId}`)
|
||||
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
|
||||
const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
|
||||
const pricingPaneReloadStore = usePricingPaneReloadStore()
|
||||
|
||||
const shouldSkipPersist = () => {
|
||||
const storageKey = `${PRICING_CLEAR_SKIP_PREFIX}${DB_KEY.value}`
|
||||
const raw = sessionStorage.getItem(storageKey)
|
||||
if (!raw) return false
|
||||
const skipUntil = Number(raw)
|
||||
if (Number.isFinite(skipUntil) && Date.now() <= skipUntil) return true
|
||||
sessionStorage.removeItem(storageKey)
|
||||
return false
|
||||
}
|
||||
|
||||
const shouldForceDefaultLoad = () => {
|
||||
const storageKey = `${PRICING_FORCE_DEFAULT_PREFIX}${DB_KEY.value}`
|
||||
const raw = sessionStorage.getItem(storageKey)
|
||||
if (!raw) return false
|
||||
const forceUntil = Number(raw)
|
||||
sessionStorage.removeItem(storageKey)
|
||||
return Number.isFinite(forceUntil) && Date.now() <= forceUntil
|
||||
}
|
||||
|
||||
const detailRows = ref<DetailRow[]>([])
|
||||
|
||||
type majorLite = { code: string; name: string }
|
||||
const serviceEntries = Object.entries(majorList as Record<string, majorLite>)
|
||||
.sort((a, b) => Number(a[0]) - Number(b[0]))
|
||||
.filter((entry): entry is [string, majorLite] => {
|
||||
const item = entry[1]
|
||||
return Boolean(item?.code && item?.name)
|
||||
})
|
||||
|
||||
const detailDict: DictGroup[] = (() => {
|
||||
const groupMap = new Map<string, DictGroup>()
|
||||
const groupOrder: string[] = []
|
||||
const codeLookup = new Map(serviceEntries.map(([key, item]) => [item.code, { id: key, code: item.code, name: item.name }]))
|
||||
|
||||
for (const [key, item] of serviceEntries) {
|
||||
const code = item.code
|
||||
const isGroup = !code.includes('-')
|
||||
if (isGroup) {
|
||||
if (!groupMap.has(code)) groupOrder.push(code)
|
||||
groupMap.set(code, {
|
||||
id: key,
|
||||
code,
|
||||
name: item.name,
|
||||
children: []
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
const parentCode = code.split('-')[0]
|
||||
if (!groupMap.has(parentCode)) {
|
||||
const parent = codeLookup.get(parentCode)
|
||||
if (!groupOrder.includes(parentCode)) groupOrder.push(parentCode)
|
||||
groupMap.set(parentCode, {
|
||||
id: parent?.id || `group-${parentCode}`,
|
||||
code: parentCode,
|
||||
name: parent?.name || parentCode,
|
||||
children: []
|
||||
})
|
||||
}
|
||||
|
||||
groupMap.get(parentCode)!.children.push({
|
||||
id: key,
|
||||
code,
|
||||
name: item.name
|
||||
})
|
||||
}
|
||||
|
||||
return groupOrder.map(code => groupMap.get(code)).filter((group): group is DictGroup => Boolean(group))
|
||||
})()
|
||||
|
||||
const idLabelMap = new Map<string, string>()
|
||||
for (const group of detailDict) {
|
||||
idLabelMap.set(group.id, `${group.code} ${group.name}`)
|
||||
for (const child of group.children) {
|
||||
idLabelMap.set(child.id, `${child.code} ${child.name}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const buildDefaultRows = (): DetailRow[] => {
|
||||
const rows: DetailRow[] = []
|
||||
for (const group of detailDict) {
|
||||
for (const child of group.children) {
|
||||
rows.push({
|
||||
id: child.id,
|
||||
groupCode: group.code,
|
||||
groupName: group.name,
|
||||
majorCode: child.code,
|
||||
majorName: child.name,
|
||||
laborBudgetUnitPrice: null,
|
||||
compositeBudgetUnitPrice: null,
|
||||
adoptedBudgetUnitPrice: null,
|
||||
personnelCount: null,
|
||||
workdayCount: null,
|
||||
serviceBudget: null,
|
||||
remark: '',
|
||||
path: [group.id, child.id]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
@ -213,20 +166,25 @@ const columnDefs: (ColDef<DetailRow> | ColGroupDef<DetailRow>)[] = [
|
||||
]
|
||||
},
|
||||
editableNumberCol('adoptedBudgetUnitPrice', '预算采用单价(元/工日)'),
|
||||
editableNumberCol('personnelCount', '人员数量(人)', { aggFunc: 'sum' }),
|
||||
editableNumberCol('workdayCount', '工日数量(工日)', { aggFunc: 'sum' }),
|
||||
editableNumberCol('serviceBudget', '服务预算(元)', { aggFunc: 'sum' }),
|
||||
editableNumberCol('personnelCount', '人员数量(人)', { aggFunc: decimalAggSum }),
|
||||
editableNumberCol('workdayCount', '工日数量(工日)', { aggFunc: decimalAggSum }),
|
||||
editableNumberCol('serviceBudget', '服务预算(元)', { aggFunc: decimalAggSum }),
|
||||
{
|
||||
headerName: '说明',
|
||||
field: 'remark',
|
||||
minWidth: 180,
|
||||
flex: 1.2,
|
||||
cellEditor: 'agLargeTextCellEditor',
|
||||
wrapText: true,
|
||||
autoHeight: true,
|
||||
cellStyle: { whiteSpace: 'normal', lineHeight: '1.4' },
|
||||
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
valueFormatter: params => {
|
||||
if (!params.node?.group && !params.node?.rowPinned && !params.value) return '点击输入'
|
||||
return params.value || ''
|
||||
},
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line remark-wrap-cell' : ''),
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
@ -253,34 +211,11 @@ const autoGroupColumnDef: ColDef = {
|
||||
}
|
||||
}
|
||||
|
||||
const gridOptions: GridOptions<DetailRow> = {
|
||||
treeData: true,
|
||||
animateRows: true,
|
||||
singleClickEdit: true,
|
||||
suppressClickEdit: false,
|
||||
suppressContextMenu: false,
|
||||
groupDefaultExpanded: -1,
|
||||
suppressFieldDotNotation: true,
|
||||
getDataPath: data => data.path,
|
||||
getContextMenuItems: () => ['copy', 'paste', 'separator', 'export'],
|
||||
defaultColDef: {
|
||||
resizable: true,
|
||||
sortable: false,
|
||||
filter: false
|
||||
}
|
||||
}
|
||||
const totalPersonnelCount = computed(() => sumByNumber(detailRows.value, row => row.personnelCount))
|
||||
|
||||
const totalPersonnelCount = computed(() =>
|
||||
detailRows.value.reduce((sum, row) => sum + (row.personnelCount || 0), 0)
|
||||
)
|
||||
const totalWorkdayCount = computed(() => sumByNumber(detailRows.value, row => row.workdayCount))
|
||||
|
||||
const totalWorkdayCount = computed(() =>
|
||||
detailRows.value.reduce((sum, row) => sum + (row.workdayCount || 0), 0)
|
||||
)
|
||||
|
||||
const totalServiceBudget = computed(() =>
|
||||
detailRows.value.reduce((sum, row) => sum + (row.serviceBudget || 0), 0)
|
||||
)
|
||||
const totalServiceBudget = computed(() => sumByNumber(detailRows.value, row => row.serviceBudget))
|
||||
const pinnedTopRowData = computed(() => [
|
||||
{
|
||||
id: 'pinned-total-row',
|
||||
@ -302,6 +237,7 @@ const pinnedTopRowData = computed(() => [
|
||||
|
||||
|
||||
const saveToIndexedDB = async () => {
|
||||
if (shouldSkipPersist()) return
|
||||
try {
|
||||
const payload = {
|
||||
detailRows: JSON.parse(JSON.stringify(detailRows.value))
|
||||
@ -315,6 +251,11 @@ const saveToIndexedDB = async () => {
|
||||
|
||||
const loadFromIndexedDB = async () => {
|
||||
try {
|
||||
if (shouldForceDefaultLoad()) {
|
||||
detailRows.value = buildDefaultRows()
|
||||
return
|
||||
}
|
||||
|
||||
const data = await localforage.getItem<XmInfoState>(DB_KEY.value)
|
||||
if (data) {
|
||||
detailRows.value = mergeWithDictRows(data.detailRows)
|
||||
@ -328,6 +269,14 @@ const loadFromIndexedDB = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => pricingPaneReloadStore.getReloadVersion(props.contractId, props.serviceId),
|
||||
(nextVersion, prevVersion) => {
|
||||
if (nextVersion === prevVersion || nextVersion === 0) return
|
||||
void loadFromIndexedDB()
|
||||
}
|
||||
)
|
||||
|
||||
let persistTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
|
||||
@ -341,7 +290,6 @@ const handleCellValueChanged = () => {
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
await loadFromIndexedDB()
|
||||
})
|
||||
@ -380,58 +328,16 @@ const processCellFromClipboard = (params:any) => {
|
||||
</div>
|
||||
|
||||
<div class="ag-theme-quartz h-full min-h-0 w-full flex-1">
|
||||
<AgGridVue
|
||||
:style="{ height: '100%' }"
|
||||
:rowData="detailRows"
|
||||
:pinnedTopRowData="pinnedTopRowData"
|
||||
:columnDefs="columnDefs"
|
||||
:autoGroupColumnDef="autoGroupColumnDef"
|
||||
:gridOptions="gridOptions"
|
||||
:theme="myTheme"
|
||||
@cell-value-changed="handleCellValueChanged"
|
||||
:suppressColumnVirtualisation="true"
|
||||
:suppressRowVirtualisation="true"
|
||||
:cellSelection="{ handle: { mode: 'range' } }"
|
||||
:enableClipboard="true"
|
||||
:localeText="AG_GRID_LOCALE_CN"
|
||||
:tooltipShowDelay="500"
|
||||
:headerHeight="50"
|
||||
:processCellForClipboard="processCellForClipboard"
|
||||
:processCellFromClipboard="processCellFromClipboard"
|
||||
:undoRedoCellEditing="true"
|
||||
:undoRedoCellEditingLimit="20"
|
||||
|
||||
/>
|
||||
<AgGridVue :style="{ height: '100%' }" :rowData="detailRows" :pinnedTopRowData="pinnedTopRowData"
|
||||
:columnDefs="columnDefs" :autoGroupColumnDef="autoGroupColumnDef" :gridOptions="gridOptions" :theme="myTheme"
|
||||
@cell-value-changed="handleCellValueChanged" :suppressColumnVirtualisation="true"
|
||||
:suppressRowVirtualisation="true" :cellSelection="{ handle: { mode: 'range' } }" :enableClipboard="true"
|
||||
:localeText="AG_GRID_LOCALE_CN" :tooltipShowDelay="500" :headerHeight="50"
|
||||
:processCellForClipboard="processCellForClipboard" :processCellFromClipboard="processCellFromClipboard"
|
||||
:undoRedoCellEditing="true" :undoRedoCellEditingLimit="20" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style >
|
||||
.ag-floating-top{
|
||||
overflow-y:auto !important
|
||||
}
|
||||
|
||||
.xmMx .editable-cell-line .ag-cell-value {
|
||||
display: inline-block;
|
||||
min-width: 84%;
|
||||
padding: 2px 4px;
|
||||
border-bottom: 1px solid #cbd5e1;
|
||||
}
|
||||
|
||||
.xmMx .editable-cell-line.ag-cell-focus .ag-cell-value,
|
||||
.xmMx .editable-cell-line:hover .ag-cell-value {
|
||||
border-bottom-color: #2563eb;
|
||||
}
|
||||
|
||||
.xmMx .editable-cell-empty .ag-cell-value {
|
||||
color: #94a3b8 !important;
|
||||
font-style: italic;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.xmMx .ag-cell.editable-cell-empty,
|
||||
.xmMx .ag-cell.editable-cell-empty .ag-cell-value {
|
||||
color: #94a3b8 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { AgGridVue } from 'ag-grid-vue3'
|
||||
import type { ColDef, GridOptions } from 'ag-grid-community'
|
||||
import type { ColDef } from 'ag-grid-community'
|
||||
import localforage from 'localforage'
|
||||
import { majorList } from '@/sql'
|
||||
import 'ag-grid-enterprise'
|
||||
import { myTheme } from '@/lib/diyAgGridTheme'
|
||||
import { getBasicFeeFromScale, majorList, serviceList } from '@/sql'
|
||||
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
||||
import { addNumbers, decimalAggSum, sumByNumber } from '@/lib/decimal'
|
||||
import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
||||
|
||||
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale';
|
||||
// 精简的边框配置(细线条+浅灰色,弱化分割线视觉)
|
||||
@ -48,10 +49,38 @@ const props = defineProps<{
|
||||
serviceId: string | number
|
||||
}>()
|
||||
const DB_KEY = computed(() => `tzGMF-${props.contractId}-${props.serviceId}`)
|
||||
const HT_DB_KEY = computed(() => `ht-info-v3-${props.contractId}`)
|
||||
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
|
||||
const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
|
||||
const pricingPaneReloadStore = usePricingPaneReloadStore()
|
||||
|
||||
const shouldSkipPersist = () => {
|
||||
const storageKey = `${PRICING_CLEAR_SKIP_PREFIX}${DB_KEY.value}`
|
||||
const raw = sessionStorage.getItem(storageKey)
|
||||
if (!raw) return false
|
||||
const skipUntil = Number(raw)
|
||||
if (Number.isFinite(skipUntil) && Date.now() <= skipUntil) return true
|
||||
sessionStorage.removeItem(storageKey)
|
||||
return false
|
||||
}
|
||||
|
||||
const shouldForceDefaultLoad = () => {
|
||||
const storageKey = `${PRICING_FORCE_DEFAULT_PREFIX}${DB_KEY.value}`
|
||||
const raw = sessionStorage.getItem(storageKey)
|
||||
if (!raw) return false
|
||||
const forceUntil = Number(raw)
|
||||
sessionStorage.removeItem(storageKey)
|
||||
return Number.isFinite(forceUntil) && Date.now() <= forceUntil
|
||||
}
|
||||
|
||||
const detailRows = ref<DetailRow[]>([])
|
||||
type serviceLite = { defCoe: number | null }
|
||||
const defaultConsultCategoryFactor = computed<number | null>(() => {
|
||||
const service = (serviceList as Record<string, serviceLite | undefined>)[String(props.serviceId)]
|
||||
return typeof service?.defCoe === 'number' && Number.isFinite(service.defCoe) ? service.defCoe : null
|
||||
})
|
||||
|
||||
type majorLite = { code: string; name: string }
|
||||
type majorLite = { code: string; name: string; defCoe: number | null }
|
||||
const serviceEntries = Object.entries(majorList as Record<string, majorLite>)
|
||||
.sort((a, b) => Number(a[0]) - Number(b[0]))
|
||||
.filter((entry): entry is [string, majorLite] => {
|
||||
@ -59,6 +88,11 @@ const serviceEntries = Object.entries(majorList as Record<string, majorLite>)
|
||||
return Boolean(item?.code && item?.name)
|
||||
})
|
||||
|
||||
const getDefaultMajorFactorById = (id: string): number | null => {
|
||||
const major = (majorList as Record<string, majorLite | undefined>)[id]
|
||||
return typeof major?.defCoe === 'number' && Number.isFinite(major.defCoe) ? major.defCoe : null
|
||||
}
|
||||
|
||||
const detailDict: DictGroup[] = (() => {
|
||||
const groupMap = new Map<string, DictGroup>()
|
||||
const groupOrder: string[] = []
|
||||
@ -120,8 +154,8 @@ const buildDefaultRows = (): DetailRow[] => {
|
||||
majorName: child.name,
|
||||
amount: null,
|
||||
benchmarkBudget: null,
|
||||
consultCategoryFactor: null,
|
||||
majorFactor: null,
|
||||
consultCategoryFactor: defaultConsultCategoryFactor.value,
|
||||
majorFactor: getDefaultMajorFactorById(child.id),
|
||||
budgetFee: null,
|
||||
remark: '',
|
||||
path: [group.id, child.id]
|
||||
@ -131,8 +165,9 @@ const buildDefaultRows = (): DetailRow[] => {
|
||||
return rows
|
||||
}
|
||||
|
||||
const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] => {
|
||||
const dbValueMap = new Map<string, DetailRow>()
|
||||
type SourceRow = Pick<DetailRow, 'id'> & Partial<Pick<DetailRow, 'amount' | 'benchmarkBudget' | 'consultCategoryFactor' | 'majorFactor' | 'budgetFee' | 'remark'>>
|
||||
const mergeWithDictRows = (rowsFromDb: SourceRow[] | undefined): DetailRow[] => {
|
||||
const dbValueMap = new Map<string, SourceRow>()
|
||||
for (const row of rowsFromDb || []) {
|
||||
dbValueMap.set(row.id, row)
|
||||
}
|
||||
@ -140,14 +175,25 @@ const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] =>
|
||||
return buildDefaultRows().map(row => {
|
||||
const fromDb = dbValueMap.get(row.id)
|
||||
if (!fromDb) return row
|
||||
const hasConsultCategoryFactor = Object.prototype.hasOwnProperty.call(fromDb, 'consultCategoryFactor')
|
||||
const hasMajorFactor = Object.prototype.hasOwnProperty.call(fromDb, 'majorFactor')
|
||||
|
||||
return {
|
||||
...row,
|
||||
amount: typeof fromDb.amount === 'number' ? fromDb.amount : null,
|
||||
benchmarkBudget: typeof fromDb.benchmarkBudget === 'number' ? fromDb.benchmarkBudget : null,
|
||||
consultCategoryFactor:
|
||||
typeof fromDb.consultCategoryFactor === 'number' ? fromDb.consultCategoryFactor : null,
|
||||
majorFactor: typeof fromDb.majorFactor === 'number' ? fromDb.majorFactor : null,
|
||||
typeof fromDb.consultCategoryFactor === 'number'
|
||||
? fromDb.consultCategoryFactor
|
||||
: hasConsultCategoryFactor
|
||||
? null
|
||||
: defaultConsultCategoryFactor.value,
|
||||
majorFactor:
|
||||
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 : ''
|
||||
}
|
||||
@ -168,17 +214,41 @@ const formatEditableNumber = (params: any) => {
|
||||
return Number(params.value).toFixed(2)
|
||||
}
|
||||
|
||||
const columnDefs: ColDef<DetailRow>[] = [
|
||||
{
|
||||
headerName: '工程专业名称',
|
||||
minWidth: 220,
|
||||
width: 240,
|
||||
pinned: 'left',
|
||||
valueGetter: params => {
|
||||
if (params.node?.rowPinned) return ''
|
||||
return params.node?.group ? params.data?.groupName || '' : params.data?.majorName || ''
|
||||
const formatConsultCategoryFactor = (params: any) => {
|
||||
if (params.node?.group) {
|
||||
if (defaultConsultCategoryFactor.value == null) return ''
|
||||
return Number(defaultConsultCategoryFactor.value).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)
|
||||
}
|
||||
|
||||
const formatReadonlyNumber = (params: any) => {
|
||||
if (params.value == null || params.value === '') return ''
|
||||
return Number(params.value).toFixed(2)
|
||||
}
|
||||
|
||||
const getBenchmarkBudgetByAmount = (row?: Pick<DetailRow, 'amount'>) => {
|
||||
const result = getBasicFeeFromScale(row?.amount, 'cost')
|
||||
return result ? addNumbers(result.basic, result.optional) : null
|
||||
}
|
||||
|
||||
const getBudgetFee = (row?: Pick<DetailRow, 'amount' | 'majorFactor' | 'consultCategoryFactor'>) => {
|
||||
const benchmarkBudget = getBenchmarkBudgetByAmount(row)
|
||||
if (benchmarkBudget == null || row?.majorFactor == null || row?.consultCategoryFactor == null) return null
|
||||
return benchmarkBudget * row.majorFactor * row.consultCategoryFactor
|
||||
}
|
||||
|
||||
const columnDefs: ColDef<DetailRow>[] = [
|
||||
{
|
||||
headerName: '造价金额(万元)',
|
||||
field: 'amount',
|
||||
@ -191,7 +261,7 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
aggFunc: 'sum',
|
||||
aggFunc: decimalAggSum,
|
||||
valueParser: params => parseNumberOrNull(params.newValue),
|
||||
valueFormatter: formatEditableNumber
|
||||
},
|
||||
@ -200,16 +270,10 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
field: 'benchmarkBudget',
|
||||
minWidth: 170,
|
||||
flex: 1,
|
||||
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
aggFunc: 'sum',
|
||||
aggFunc: decimalAggSum,
|
||||
valueGetter: params => getBenchmarkBudgetByAmount(params.data),
|
||||
valueParser: params => parseNumberOrNull(params.newValue),
|
||||
valueFormatter: formatEditableNumber
|
||||
valueFormatter: formatReadonlyNumber
|
||||
},
|
||||
{
|
||||
headerName: '咨询分类系数',
|
||||
@ -223,7 +287,7 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
valueParser: params => parseNumberOrNull(params.newValue),
|
||||
valueFormatter: formatEditableNumber
|
||||
valueFormatter: formatConsultCategoryFactor
|
||||
},
|
||||
{
|
||||
headerName: '专业系数',
|
||||
@ -237,34 +301,34 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
valueParser: params => parseNumberOrNull(params.newValue),
|
||||
valueFormatter: formatEditableNumber
|
||||
valueFormatter: formatMajorFactor
|
||||
},
|
||||
{
|
||||
headerName: '预算费用',
|
||||
field: 'budgetFee',
|
||||
minWidth: 150,
|
||||
flex: 1,
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
aggFunc: 'sum',
|
||||
aggFunc: decimalAggSum,
|
||||
valueGetter: params => (params.node?.rowPinned ? params.data?.budgetFee ?? null : getBudgetFee(params.data)),
|
||||
valueParser: params => parseNumberOrNull(params.newValue),
|
||||
valueFormatter: formatEditableNumber
|
||||
valueFormatter: formatReadonlyNumber
|
||||
},
|
||||
{
|
||||
headerName: '说明',
|
||||
field: 'remark',
|
||||
minWidth: 180,
|
||||
flex: 1.2,
|
||||
cellEditor: 'agLargeTextCellEditor',
|
||||
wrapText: true,
|
||||
autoHeight: true,
|
||||
cellStyle: { whiteSpace: 'normal', lineHeight: '1.4' },
|
||||
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
valueFormatter: params => {
|
||||
if (!params.node?.group && !params.node?.rowPinned && !params.value) return '点击输入'
|
||||
return params.value || ''
|
||||
},
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line remark-wrap-cell' : ''),
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
@ -273,10 +337,10 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
]
|
||||
|
||||
const autoGroupColumnDef: ColDef = {
|
||||
headerName: '专业编码',
|
||||
minWidth: 160,
|
||||
headerName: '专业编码以及工程专业名称',
|
||||
minWidth: 320,
|
||||
pinned: 'left',
|
||||
width: 170,
|
||||
flex: 2,
|
||||
|
||||
cellRendererParams: {
|
||||
suppressCount: true
|
||||
@ -286,39 +350,16 @@ const autoGroupColumnDef: ColDef = {
|
||||
return '总合计'
|
||||
}
|
||||
const nodeId = String(params.value || '')
|
||||
const label = idLabelMap.get(nodeId) || nodeId
|
||||
return label.includes(' ') ? label.split(' ')[0] : label
|
||||
return idLabelMap.get(nodeId) || nodeId
|
||||
}
|
||||
}
|
||||
|
||||
const gridOptions: GridOptions<DetailRow> = {
|
||||
treeData: true,
|
||||
animateRows: true,
|
||||
singleClickEdit: true,
|
||||
suppressClickEdit: false,
|
||||
suppressContextMenu: false,
|
||||
groupDefaultExpanded: -1,
|
||||
suppressFieldDotNotation: true,
|
||||
getDataPath: data => data.path,
|
||||
getContextMenuItems: () => ['copy', 'paste', 'separator', 'export'],
|
||||
defaultColDef: {
|
||||
resizable: true,
|
||||
sortable: false,
|
||||
filter: false
|
||||
}
|
||||
}
|
||||
|
||||
const totalAmount = computed(() =>
|
||||
detailRows.value.reduce((sum, row) => sum + (row.amount || 0), 0)
|
||||
)
|
||||
const totalAmount = computed(() => sumByNumber(detailRows.value, row => row.amount))
|
||||
|
||||
const totalBenchmarkBudget = computed(() =>
|
||||
detailRows.value.reduce((sum, row) => sum + (row.benchmarkBudget || 0), 0)
|
||||
)
|
||||
const totalBenchmarkBudget = computed(() => sumByNumber(detailRows.value, row => getBenchmarkBudgetByAmount(row)))
|
||||
|
||||
const totalBudgetFee = computed(() =>
|
||||
detailRows.value.reduce((sum, row) => sum + (row.budgetFee || 0), 0)
|
||||
)
|
||||
const totalBudgetFee = computed(() => sumByNumber(detailRows.value, row => getBudgetFee(row)))
|
||||
const pinnedTopRowData = computed(() => [
|
||||
{
|
||||
id: 'pinned-total-row',
|
||||
@ -339,6 +380,7 @@ const pinnedTopRowData = computed(() => [
|
||||
|
||||
|
||||
const saveToIndexedDB = async () => {
|
||||
if (shouldSkipPersist()) return
|
||||
try {
|
||||
const payload = {
|
||||
detailRows: JSON.parse(JSON.stringify(detailRows.value))
|
||||
@ -352,12 +394,24 @@ const saveToIndexedDB = async () => {
|
||||
|
||||
const loadFromIndexedDB = async () => {
|
||||
try {
|
||||
if (shouldForceDefaultLoad()) {
|
||||
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
||||
detailRows.value = htData?.detailRows ? mergeWithDictRows(htData.detailRows) : buildDefaultRows()
|
||||
return
|
||||
}
|
||||
|
||||
const data = await localforage.getItem<XmInfoState>(DB_KEY.value)
|
||||
if (data) {
|
||||
detailRows.value = mergeWithDictRows(data.detailRows)
|
||||
return
|
||||
}
|
||||
|
||||
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
||||
if (htData?.detailRows) {
|
||||
detailRows.value = mergeWithDictRows(htData.detailRows)
|
||||
return
|
||||
}
|
||||
|
||||
detailRows.value = buildDefaultRows()
|
||||
} catch (error) {
|
||||
console.error('loadFromIndexedDB failed:', error)
|
||||
@ -365,6 +419,14 @@ const loadFromIndexedDB = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => pricingPaneReloadStore.getReloadVersion(props.contractId, props.serviceId),
|
||||
(nextVersion, prevVersion) => {
|
||||
if (nextVersion === prevVersion || nextVersion === 0) return
|
||||
void loadFromIndexedDB()
|
||||
}
|
||||
)
|
||||
|
||||
let persistTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
|
||||
@ -378,7 +440,6 @@ const handleCellValueChanged = () => {
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
await loadFromIndexedDB()
|
||||
})
|
||||
@ -417,58 +478,16 @@ const processCellFromClipboard = (params:any) => {
|
||||
</div>
|
||||
|
||||
<div class="ag-theme-quartz h-full min-h-0 w-full flex-1">
|
||||
<AgGridVue
|
||||
:style="{ height: '100%' }"
|
||||
:rowData="detailRows"
|
||||
:pinnedTopRowData="pinnedTopRowData"
|
||||
:columnDefs="columnDefs"
|
||||
:autoGroupColumnDef="autoGroupColumnDef"
|
||||
:gridOptions="gridOptions"
|
||||
:theme="myTheme"
|
||||
@cell-value-changed="handleCellValueChanged"
|
||||
:suppressColumnVirtualisation="true"
|
||||
:suppressRowVirtualisation="true"
|
||||
:cellSelection="{ handle: { mode: 'range' } }"
|
||||
:enableClipboard="true"
|
||||
:localeText="AG_GRID_LOCALE_CN"
|
||||
:tooltipShowDelay="500"
|
||||
:headerHeight="50"
|
||||
:processCellForClipboard="processCellForClipboard"
|
||||
:processCellFromClipboard="processCellFromClipboard"
|
||||
:undoRedoCellEditing="true"
|
||||
:undoRedoCellEditingLimit="20"
|
||||
|
||||
/>
|
||||
<AgGridVue :style="{ height: '100%' }" :rowData="detailRows" :pinnedTopRowData="pinnedTopRowData"
|
||||
:columnDefs="columnDefs" :autoGroupColumnDef="autoGroupColumnDef" :gridOptions="gridOptions" :theme="myTheme"
|
||||
@cell-value-changed="handleCellValueChanged" :suppressColumnVirtualisation="true"
|
||||
:suppressRowVirtualisation="true" :cellSelection="{ handle: { mode: 'range' } }" :enableClipboard="true"
|
||||
:localeText="AG_GRID_LOCALE_CN" :tooltipShowDelay="500" :headerHeight="50"
|
||||
:processCellForClipboard="processCellForClipboard" :processCellFromClipboard="processCellFromClipboard"
|
||||
:undoRedoCellEditing="true" :undoRedoCellEditingLimit="20" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style >
|
||||
.ag-floating-top{
|
||||
overflow-y:auto !important
|
||||
}
|
||||
|
||||
.xmMx .editable-cell-line .ag-cell-value {
|
||||
display: inline-block;
|
||||
min-width: 84%;
|
||||
padding: 2px 4px;
|
||||
border-bottom: 1px solid #cbd5e1;
|
||||
}
|
||||
|
||||
.xmMx .editable-cell-line.ag-cell-focus .ag-cell-value,
|
||||
.xmMx .editable-cell-line:hover .ag-cell-value {
|
||||
border-bottom-color: #2563eb;
|
||||
}
|
||||
|
||||
.xmMx .editable-cell-empty .ag-cell-value {
|
||||
color: #94a3b8 !important;
|
||||
font-style: italic;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.xmMx .ag-cell.editable-cell-empty,
|
||||
.xmMx .ag-cell.editable-cell-empty .ag-cell-value {
|
||||
color: #94a3b8 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { AgGridVue } from 'ag-grid-vue3'
|
||||
import type { ColDef, GridOptions } from 'ag-grid-community'
|
||||
import type { ColDef } from 'ag-grid-community'
|
||||
import localforage from 'localforage'
|
||||
import { majorList } from '@/sql'
|
||||
import 'ag-grid-enterprise'
|
||||
import { myTheme } from '@/lib/diyAgGridTheme'
|
||||
import { getBasicFeeFromScale, majorList, serviceList } from '@/sql'
|
||||
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
||||
import { addNumbers, decimalAggSum, sumByNumber } from '@/lib/decimal'
|
||||
import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
||||
|
||||
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale';
|
||||
// 精简的边框配置(细线条+浅灰色,弱化分割线视觉)
|
||||
@ -49,10 +50,38 @@ const props = defineProps<{
|
||||
serviceId: string | number
|
||||
}>()
|
||||
const DB_KEY = computed(() => `ydGMF-${props.contractId}-${props.serviceId}`)
|
||||
const HT_DB_KEY = computed(() => `ht-info-v3-${props.contractId}`)
|
||||
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
|
||||
const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
|
||||
const pricingPaneReloadStore = usePricingPaneReloadStore()
|
||||
|
||||
const detailRows = ref<DetailRow[]>([])
|
||||
type serviceLite = { defCoe: number | null }
|
||||
const defaultConsultCategoryFactor = computed<number | null>(() => {
|
||||
const service = (serviceList as Record<string, serviceLite | undefined>)[String(props.serviceId)]
|
||||
return typeof service?.defCoe === 'number' && Number.isFinite(service.defCoe) ? service.defCoe : null
|
||||
})
|
||||
|
||||
type majorLite = { code: string; name: string }
|
||||
const shouldForceDefaultLoad = () => {
|
||||
const storageKey = `${PRICING_FORCE_DEFAULT_PREFIX}${DB_KEY.value}`
|
||||
const raw = sessionStorage.getItem(storageKey)
|
||||
if (!raw) return false
|
||||
const forceUntil = Number(raw)
|
||||
sessionStorage.removeItem(storageKey)
|
||||
return Number.isFinite(forceUntil) && Date.now() <= forceUntil
|
||||
}
|
||||
|
||||
const shouldSkipPersist = () => {
|
||||
const storageKey = `${PRICING_CLEAR_SKIP_PREFIX}${DB_KEY.value}`
|
||||
const raw = sessionStorage.getItem(storageKey)
|
||||
if (!raw) return false
|
||||
const skipUntil = Number(raw)
|
||||
if (Number.isFinite(skipUntil) && Date.now() <= skipUntil) return true
|
||||
sessionStorage.removeItem(storageKey)
|
||||
return false
|
||||
}
|
||||
|
||||
type majorLite = { code: string; name: string; defCoe: number | null }
|
||||
const serviceEntries = Object.entries(majorList as Record<string, majorLite>)
|
||||
.sort((a, b) => Number(a[0]) - Number(b[0]))
|
||||
.filter((entry): entry is [string, majorLite] => {
|
||||
@ -60,6 +89,11 @@ const serviceEntries = Object.entries(majorList as Record<string, majorLite>)
|
||||
return Boolean(item?.code && item?.name)
|
||||
})
|
||||
|
||||
const getDefaultMajorFactorById = (id: string): number | null => {
|
||||
const major = (majorList as Record<string, majorLite | undefined>)[id]
|
||||
return typeof major?.defCoe === 'number' && Number.isFinite(major.defCoe) ? major.defCoe : null
|
||||
}
|
||||
|
||||
const detailDict: DictGroup[] = (() => {
|
||||
const groupMap = new Map<string, DictGroup>()
|
||||
const groupOrder: string[] = []
|
||||
@ -122,8 +156,8 @@ const buildDefaultRows = (): DetailRow[] => {
|
||||
amount: null,
|
||||
landArea: null,
|
||||
benchmarkBudget: null,
|
||||
consultCategoryFactor: null,
|
||||
majorFactor: null,
|
||||
consultCategoryFactor: defaultConsultCategoryFactor.value,
|
||||
majorFactor: getDefaultMajorFactorById(child.id),
|
||||
budgetFee: null,
|
||||
remark: '',
|
||||
path: [group.id, child.id]
|
||||
@ -133,8 +167,9 @@ const buildDefaultRows = (): DetailRow[] => {
|
||||
return rows
|
||||
}
|
||||
|
||||
const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] => {
|
||||
const dbValueMap = new Map<string, DetailRow>()
|
||||
type SourceRow = Pick<DetailRow, 'id'> & Partial<Pick<DetailRow, 'amount' | 'landArea' | 'benchmarkBudget' | 'consultCategoryFactor' | 'majorFactor' | 'budgetFee' | 'remark'>>
|
||||
const mergeWithDictRows = (rowsFromDb: SourceRow[] | undefined): DetailRow[] => {
|
||||
const dbValueMap = new Map<string, SourceRow>()
|
||||
for (const row of rowsFromDb || []) {
|
||||
dbValueMap.set(row.id, row)
|
||||
}
|
||||
@ -142,6 +177,8 @@ const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] =>
|
||||
return buildDefaultRows().map(row => {
|
||||
const fromDb = dbValueMap.get(row.id)
|
||||
if (!fromDb) return row
|
||||
const hasConsultCategoryFactor = Object.prototype.hasOwnProperty.call(fromDb, 'consultCategoryFactor')
|
||||
const hasMajorFactor = Object.prototype.hasOwnProperty.call(fromDb, 'majorFactor')
|
||||
|
||||
return {
|
||||
...row,
|
||||
@ -149,8 +186,17 @@ const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] =>
|
||||
landArea: typeof fromDb.landArea === 'number' ? fromDb.landArea : null,
|
||||
benchmarkBudget: typeof fromDb.benchmarkBudget === 'number' ? fromDb.benchmarkBudget : null,
|
||||
consultCategoryFactor:
|
||||
typeof fromDb.consultCategoryFactor === 'number' ? fromDb.consultCategoryFactor : null,
|
||||
majorFactor: typeof fromDb.majorFactor === 'number' ? fromDb.majorFactor : null,
|
||||
typeof fromDb.consultCategoryFactor === 'number'
|
||||
? fromDb.consultCategoryFactor
|
||||
: hasConsultCategoryFactor
|
||||
? null
|
||||
: defaultConsultCategoryFactor.value,
|
||||
majorFactor:
|
||||
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 : ''
|
||||
}
|
||||
@ -171,6 +217,40 @@ const formatEditableNumber = (params: any) => {
|
||||
return Number(params.value).toFixed(2)
|
||||
}
|
||||
|
||||
const formatConsultCategoryFactor = (params: any) => {
|
||||
if (params.node?.group) {
|
||||
if (defaultConsultCategoryFactor.value == null) return ''
|
||||
return Number(defaultConsultCategoryFactor.value).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)
|
||||
}
|
||||
|
||||
const formatReadonlyNumber = (params: any) => {
|
||||
if (params.value == null || params.value === '') return ''
|
||||
return Number(params.value).toFixed(2)
|
||||
}
|
||||
|
||||
const getBenchmarkBudgetByLandArea = (row?: Pick<DetailRow, 'landArea'>) => {
|
||||
const result = getBasicFeeFromScale(row?.landArea, 'area')
|
||||
return result ? addNumbers(result.basic, result.optional) : null
|
||||
}
|
||||
|
||||
const getBudgetFee = (row?: Pick<DetailRow, 'landArea' | 'majorFactor' | 'consultCategoryFactor'>) => {
|
||||
const benchmarkBudget = getBenchmarkBudgetByLandArea(row)
|
||||
if (benchmarkBudget == null || row?.majorFactor == null || row?.consultCategoryFactor == null) return null
|
||||
return benchmarkBudget * row.majorFactor * row.consultCategoryFactor
|
||||
}
|
||||
|
||||
const formatEditableFlexibleNumber = (params: any) => {
|
||||
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
|
||||
return '点击输入'
|
||||
@ -180,16 +260,6 @@ const formatEditableFlexibleNumber = (params: any) => {
|
||||
}
|
||||
|
||||
const columnDefs: ColDef<DetailRow>[] = [
|
||||
{
|
||||
headerName: '工程专业名称',
|
||||
minWidth: 220,
|
||||
width: 240,
|
||||
pinned: 'left',
|
||||
valueGetter: params => {
|
||||
if (params.node?.rowPinned) return ''
|
||||
return params.node?.group ? params.data?.groupName || '' : params.data?.majorName || ''
|
||||
}
|
||||
},
|
||||
{
|
||||
headerName: '用地面积(亩)',
|
||||
field: 'landArea',
|
||||
@ -202,7 +272,7 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
aggFunc: 'sum',
|
||||
aggFunc: decimalAggSum,
|
||||
valueParser: params => parseNumberOrNull(params.newValue),
|
||||
valueFormatter: formatEditableFlexibleNumber
|
||||
},
|
||||
@ -211,15 +281,10 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
field: 'benchmarkBudget',
|
||||
minWidth: 170,
|
||||
flex: 1,
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
aggFunc: 'sum',
|
||||
aggFunc: decimalAggSum,
|
||||
valueGetter: params => getBenchmarkBudgetByLandArea(params.data),
|
||||
valueParser: params => parseNumberOrNull(params.newValue),
|
||||
valueFormatter: formatEditableNumber
|
||||
valueFormatter: formatReadonlyNumber
|
||||
},
|
||||
{
|
||||
headerName: '咨询分类系数',
|
||||
@ -233,7 +298,7 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
valueParser: params => parseNumberOrNull(params.newValue),
|
||||
valueFormatter: formatEditableNumber
|
||||
valueFormatter: formatConsultCategoryFactor
|
||||
},
|
||||
{
|
||||
headerName: '专业系数',
|
||||
@ -247,34 +312,34 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
valueParser: params => parseNumberOrNull(params.newValue),
|
||||
valueFormatter: formatEditableNumber
|
||||
valueFormatter: formatMajorFactor
|
||||
},
|
||||
{
|
||||
headerName: '预算费用',
|
||||
field: 'budgetFee',
|
||||
minWidth: 150,
|
||||
flex: 1,
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
aggFunc: 'sum',
|
||||
aggFunc: decimalAggSum,
|
||||
valueGetter: params => (params.node?.rowPinned ? params.data?.budgetFee ?? null : getBudgetFee(params.data)),
|
||||
valueParser: params => parseNumberOrNull(params.newValue),
|
||||
valueFormatter: formatEditableNumber
|
||||
valueFormatter: formatReadonlyNumber
|
||||
},
|
||||
{
|
||||
headerName: '说明',
|
||||
field: 'remark',
|
||||
minWidth: 180,
|
||||
flex: 1.2,
|
||||
cellEditor: 'agLargeTextCellEditor',
|
||||
wrapText: true,
|
||||
autoHeight: true,
|
||||
cellStyle: { whiteSpace: 'normal', lineHeight: '1.4' },
|
||||
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
valueFormatter: params => {
|
||||
if (!params.node?.group && !params.node?.rowPinned && !params.value) return '点击输入'
|
||||
return params.value || ''
|
||||
},
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line remark-wrap-cell' : ''),
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
@ -283,10 +348,10 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
]
|
||||
|
||||
const autoGroupColumnDef: ColDef = {
|
||||
headerName: '专业编码',
|
||||
minWidth: 160,
|
||||
headerName: '专业编码以及工程专业名称',
|
||||
minWidth: 320,
|
||||
pinned: 'left',
|
||||
width: 170,
|
||||
flex: 2,
|
||||
|
||||
cellRendererParams: {
|
||||
suppressCount: true
|
||||
@ -296,43 +361,19 @@ const autoGroupColumnDef: ColDef = {
|
||||
return '总合计'
|
||||
}
|
||||
const nodeId = String(params.value || '')
|
||||
const label = idLabelMap.get(nodeId) || nodeId
|
||||
return label.includes(' ') ? label.split(' ')[0] : label
|
||||
return idLabelMap.get(nodeId) || nodeId
|
||||
}
|
||||
}
|
||||
|
||||
const gridOptions: GridOptions<DetailRow> = {
|
||||
treeData: true,
|
||||
animateRows: true,
|
||||
singleClickEdit: true,
|
||||
suppressClickEdit: false,
|
||||
suppressContextMenu: false,
|
||||
groupDefaultExpanded: -1,
|
||||
suppressFieldDotNotation: true,
|
||||
getDataPath: data => data.path,
|
||||
getContextMenuItems: () => ['copy', 'paste', 'separator', 'export'],
|
||||
defaultColDef: {
|
||||
resizable: true,
|
||||
sortable: false,
|
||||
filter: false
|
||||
}
|
||||
}
|
||||
|
||||
const totalAmount = computed(() =>
|
||||
detailRows.value.reduce((sum, row) => sum + (row.amount || 0), 0)
|
||||
)
|
||||
|
||||
const totalLandArea = computed(() =>
|
||||
detailRows.value.reduce((sum, row) => sum + (row.landArea || 0), 0)
|
||||
)
|
||||
const totalAmount = computed(() => sumByNumber(detailRows.value, row => row.amount))
|
||||
|
||||
const totalBenchmarkBudget = computed(() =>
|
||||
detailRows.value.reduce((sum, row) => sum + (row.benchmarkBudget || 0), 0)
|
||||
)
|
||||
const totalLandArea = computed(() => sumByNumber(detailRows.value, row => row.landArea))
|
||||
|
||||
const totalBudgetFee = computed(() =>
|
||||
detailRows.value.reduce((sum, row) => sum + (row.budgetFee || 0), 0)
|
||||
)
|
||||
const totalBenchmarkBudget = computed(() => sumByNumber(detailRows.value, row => getBenchmarkBudgetByLandArea(row)))
|
||||
|
||||
const totalBudgetFee = computed(() => sumByNumber(detailRows.value, row => getBudgetFee(row)))
|
||||
const pinnedTopRowData = computed(() => [
|
||||
{
|
||||
id: 'pinned-total-row',
|
||||
@ -354,6 +395,7 @@ const pinnedTopRowData = computed(() => [
|
||||
|
||||
|
||||
const saveToIndexedDB = async () => {
|
||||
if (shouldSkipPersist()) return
|
||||
try {
|
||||
const payload = {
|
||||
detailRows: JSON.parse(JSON.stringify(detailRows.value))
|
||||
@ -367,12 +409,24 @@ const saveToIndexedDB = async () => {
|
||||
|
||||
const loadFromIndexedDB = async () => {
|
||||
try {
|
||||
if (shouldForceDefaultLoad()) {
|
||||
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
||||
detailRows.value = htData?.detailRows ? mergeWithDictRows(htData.detailRows) : buildDefaultRows()
|
||||
return
|
||||
}
|
||||
|
||||
const data = await localforage.getItem<XmInfoState>(DB_KEY.value)
|
||||
if (data) {
|
||||
detailRows.value = mergeWithDictRows(data.detailRows)
|
||||
return
|
||||
}
|
||||
|
||||
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
||||
if (htData?.detailRows) {
|
||||
detailRows.value = mergeWithDictRows(htData.detailRows)
|
||||
return
|
||||
}
|
||||
|
||||
detailRows.value = buildDefaultRows()
|
||||
} catch (error) {
|
||||
console.error('loadFromIndexedDB failed:', error)
|
||||
@ -380,6 +434,14 @@ const loadFromIndexedDB = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => pricingPaneReloadStore.getReloadVersion(props.contractId, props.serviceId),
|
||||
(nextVersion, prevVersion) => {
|
||||
if (nextVersion === prevVersion || nextVersion === 0) return
|
||||
void loadFromIndexedDB()
|
||||
}
|
||||
)
|
||||
|
||||
let persistTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
|
||||
@ -393,7 +455,6 @@ const handleCellValueChanged = () => {
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
await loadFromIndexedDB()
|
||||
})
|
||||
@ -432,58 +493,16 @@ const processCellFromClipboard = (params:any) => {
|
||||
</div>
|
||||
|
||||
<div class="ag-theme-quartz h-full min-h-0 w-full flex-1">
|
||||
<AgGridVue
|
||||
:style="{ height: '100%' }"
|
||||
:rowData="detailRows"
|
||||
:pinnedTopRowData="pinnedTopRowData"
|
||||
:columnDefs="columnDefs"
|
||||
:autoGroupColumnDef="autoGroupColumnDef"
|
||||
:gridOptions="gridOptions"
|
||||
:theme="myTheme"
|
||||
@cell-value-changed="handleCellValueChanged"
|
||||
:suppressColumnVirtualisation="true"
|
||||
:suppressRowVirtualisation="true"
|
||||
:cellSelection="{ handle: { mode: 'range' } }"
|
||||
:enableClipboard="true"
|
||||
:localeText="AG_GRID_LOCALE_CN"
|
||||
:tooltipShowDelay="500"
|
||||
:headerHeight="50"
|
||||
:processCellForClipboard="processCellForClipboard"
|
||||
:processCellFromClipboard="processCellFromClipboard"
|
||||
:undoRedoCellEditing="true"
|
||||
:undoRedoCellEditingLimit="20"
|
||||
|
||||
/>
|
||||
<AgGridVue :style="{ height: '100%' }" :rowData="detailRows" :pinnedTopRowData="pinnedTopRowData"
|
||||
:columnDefs="columnDefs" :autoGroupColumnDef="autoGroupColumnDef" :gridOptions="gridOptions" :theme="myTheme"
|
||||
@cell-value-changed="handleCellValueChanged" :suppressColumnVirtualisation="true"
|
||||
:suppressRowVirtualisation="true" :cellSelection="{ handle: { mode: 'range' } }" :enableClipboard="true"
|
||||
:localeText="AG_GRID_LOCALE_CN" :tooltipShowDelay="500" :headerHeight="50"
|
||||
:processCellForClipboard="processCellForClipboard" :processCellFromClipboard="processCellFromClipboard"
|
||||
:undoRedoCellEditing="true" :undoRedoCellEditingLimit="20" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style >
|
||||
.ag-floating-top{
|
||||
overflow-y:auto !important
|
||||
}
|
||||
|
||||
.xmMx .editable-cell-line .ag-cell-value {
|
||||
display: inline-block;
|
||||
min-width: 84%;
|
||||
padding: 2px 4px;
|
||||
border-bottom: 1px solid #cbd5e1;
|
||||
}
|
||||
|
||||
.xmMx .editable-cell-line.ag-cell-focus .ag-cell-value,
|
||||
.xmMx .editable-cell-line:hover .ag-cell-value {
|
||||
border-bottom-color: #2563eb;
|
||||
}
|
||||
|
||||
.xmMx .editable-cell-empty .ag-cell-value {
|
||||
color: #94a3b8 !important;
|
||||
font-style: italic;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.xmMx .ag-cell.editable-cell-empty,
|
||||
.xmMx .ag-cell.editable-cell-empty .ag-cell-value {
|
||||
color: #94a3b8 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,36 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { AgGridVue } from 'ag-grid-vue3'
|
||||
import type { ColDef, GridOptions } from 'ag-grid-community'
|
||||
import type { ColDef } from 'ag-grid-community'
|
||||
import localforage from 'localforage'
|
||||
import { majorList } from '@/sql'
|
||||
import 'ag-grid-enterprise'
|
||||
import { myTheme } from '@/lib/diyAgGridTheme'
|
||||
import { serviceList, taskList } from '@/sql'
|
||||
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
||||
import { decimalAggSum, sumByNumber } from '@/lib/decimal'
|
||||
import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
||||
|
||||
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale';
|
||||
|
||||
interface DictLeaf {
|
||||
id: string
|
||||
code: string
|
||||
name: string
|
||||
}
|
||||
|
||||
interface DictGroup {
|
||||
id: string
|
||||
code: string
|
||||
name: string
|
||||
children: DictLeaf[]
|
||||
}
|
||||
|
||||
interface DetailRow {
|
||||
id: string
|
||||
groupCode: string
|
||||
groupName: string
|
||||
majorCode: string
|
||||
majorName: string
|
||||
taskCode: string
|
||||
taskName: string
|
||||
unit: string
|
||||
workload: number | null
|
||||
budgetBase: number | null
|
||||
budgetReferenceUnitPrice: number | null
|
||||
budgetBase: string
|
||||
budgetReferenceUnitPrice: string
|
||||
budgetAdoptedUnitPrice: number | null
|
||||
consultCategoryFactor: number | null
|
||||
serviceFee: number | null
|
||||
@ -49,90 +36,111 @@ const props = defineProps<{
|
||||
serviceId: string | number
|
||||
}>()
|
||||
const DB_KEY = computed(() => `gzlF-${props.contractId}-${props.serviceId}`)
|
||||
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
|
||||
const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
|
||||
const pricingPaneReloadStore = usePricingPaneReloadStore()
|
||||
|
||||
const shouldSkipPersist = () => {
|
||||
const storageKey = `${PRICING_CLEAR_SKIP_PREFIX}${DB_KEY.value}`
|
||||
const raw = sessionStorage.getItem(storageKey)
|
||||
if (!raw) return false
|
||||
const skipUntil = Number(raw)
|
||||
if (Number.isFinite(skipUntil) && Date.now() <= skipUntil) return true
|
||||
sessionStorage.removeItem(storageKey)
|
||||
return false
|
||||
}
|
||||
|
||||
const shouldForceDefaultLoad = () => {
|
||||
const storageKey = `${PRICING_FORCE_DEFAULT_PREFIX}${DB_KEY.value}`
|
||||
const raw = sessionStorage.getItem(storageKey)
|
||||
if (!raw) return false
|
||||
const forceUntil = Number(raw)
|
||||
sessionStorage.removeItem(storageKey)
|
||||
return Number.isFinite(forceUntil) && Date.now() <= forceUntil
|
||||
}
|
||||
|
||||
const detailRows = ref<DetailRow[]>([])
|
||||
|
||||
type majorLite = { code: string; name: string }
|
||||
const serviceEntries = Object.entries(majorList as Record<string, majorLite>)
|
||||
.sort((a, b) => Number(a[0]) - Number(b[0]))
|
||||
.filter((entry): entry is [string, majorLite] => {
|
||||
const item = entry[1]
|
||||
return Boolean(item?.code && item?.name)
|
||||
})
|
||||
|
||||
const detailDict: DictGroup[] = (() => {
|
||||
const groupMap = new Map<string, DictGroup>()
|
||||
const groupOrder: string[] = []
|
||||
const codeLookup = new Map(serviceEntries.map(([key, item]) => [item.code, { id: key, code: item.code, name: item.name }]))
|
||||
|
||||
for (const [key, item] of serviceEntries) {
|
||||
const code = item.code
|
||||
const isGroup = !code.includes('-')
|
||||
if (isGroup) {
|
||||
if (!groupMap.has(code)) groupOrder.push(code)
|
||||
groupMap.set(code, {
|
||||
id: key,
|
||||
code,
|
||||
name: item.name,
|
||||
children: []
|
||||
})
|
||||
continue
|
||||
type serviceLite = { defCoe: number | null }
|
||||
type taskLite = {
|
||||
serviceID: number
|
||||
code: string
|
||||
name: string
|
||||
basicParam: string
|
||||
unit: string
|
||||
maxPrice: number | null
|
||||
minPrice: number | null
|
||||
defPrice: number | null
|
||||
}
|
||||
|
||||
const parentCode = code.split('-')[0]
|
||||
if (!groupMap.has(parentCode)) {
|
||||
const parent = codeLookup.get(parentCode)
|
||||
if (!groupOrder.includes(parentCode)) groupOrder.push(parentCode)
|
||||
groupMap.set(parentCode, {
|
||||
id: parent?.id || `group-${parentCode}`,
|
||||
code: parentCode,
|
||||
name: parent?.name || parentCode,
|
||||
children: []
|
||||
const defaultConsultCategoryFactor = computed<number | null>(() => {
|
||||
const service = (serviceList as Record<string, serviceLite | undefined>)[String(props.serviceId)]
|
||||
return typeof service?.defCoe === 'number' && Number.isFinite(service.defCoe) ? service.defCoe : null
|
||||
})
|
||||
}
|
||||
|
||||
groupMap.get(parentCode)!.children.push({
|
||||
id: key,
|
||||
code,
|
||||
name: item.name
|
||||
})
|
||||
}
|
||||
|
||||
return groupOrder.map(code => groupMap.get(code)).filter((group): group is DictGroup => Boolean(group))
|
||||
})()
|
||||
|
||||
const idLabelMap = new Map<string, string>()
|
||||
for (const group of detailDict) {
|
||||
idLabelMap.set(group.id, `${group.code} ${group.name}`)
|
||||
for (const child of group.children) {
|
||||
idLabelMap.set(child.id, `${child.code} ${child.name}`)
|
||||
}
|
||||
const formatTaskReferenceUnitPrice = (task: taskLite) => {
|
||||
const unit = task.unit || ''
|
||||
const hasMin = typeof task.minPrice === 'number' && Number.isFinite(task.minPrice)
|
||||
const hasMax = typeof task.maxPrice === 'number' && Number.isFinite(task.maxPrice)
|
||||
if (hasMin && hasMax) return `${task.minPrice}${unit}-${task.maxPrice}${unit}`
|
||||
if (hasMin) return `${task.minPrice}${unit}`
|
||||
if (hasMax) return `${task.maxPrice}${unit}`
|
||||
return ''
|
||||
}
|
||||
|
||||
const buildDefaultRows = (): DetailRow[] => {
|
||||
const rows: DetailRow[] = []
|
||||
for (const group of detailDict) {
|
||||
for (const child of group.children) {
|
||||
const currentServiceId = Number(props.serviceId)
|
||||
const sourceTaskIds = Object.entries(taskList as Record<string, taskLite>)
|
||||
.filter(([, task]) => Number(task.serviceID) === currentServiceId)
|
||||
.map(([key]) => Number(key))
|
||||
.filter(Number.isFinite)
|
||||
.sort((a, b) => a - b)
|
||||
|
||||
for (const [order, taskId] of sourceTaskIds.entries()) {
|
||||
const task = (taskList as Record<string, taskLite | undefined>)[String(taskId)]
|
||||
if (!task?.code || !task?.name) continue
|
||||
const rowId = `task-${taskId}-${order}`
|
||||
rows.push({
|
||||
id: child.id,
|
||||
groupCode: group.code,
|
||||
groupName: group.name,
|
||||
majorCode: child.code,
|
||||
majorName: child.name,
|
||||
id: rowId,
|
||||
taskCode: task.code,
|
||||
taskName: task.name,
|
||||
unit: task.unit || '',
|
||||
workload: null,
|
||||
budgetBase: null,
|
||||
budgetReferenceUnitPrice: null,
|
||||
budgetBase: task.basicParam || '',
|
||||
budgetReferenceUnitPrice: formatTaskReferenceUnitPrice(task),
|
||||
budgetAdoptedUnitPrice:
|
||||
typeof task.defPrice === 'number' && Number.isFinite(task.defPrice) ? task.defPrice : null,
|
||||
consultCategoryFactor: defaultConsultCategoryFactor.value,
|
||||
serviceFee: null,
|
||||
remark: '',
|
||||
path: [rowId]
|
||||
})
|
||||
}
|
||||
|
||||
if (rows.length === 0) {
|
||||
const emptyRowId = `task-none-${String(props.serviceId)}`
|
||||
rows.push({
|
||||
id: emptyRowId,
|
||||
taskCode: '无',
|
||||
taskName: '无',
|
||||
unit: '',
|
||||
workload: null,
|
||||
budgetBase: '无',
|
||||
budgetReferenceUnitPrice: '无',
|
||||
budgetAdoptedUnitPrice: null,
|
||||
consultCategoryFactor: null,
|
||||
serviceFee: null,
|
||||
remark: '',
|
||||
path: [group.id, child.id]
|
||||
path: [emptyRowId]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
const isNoTaskRow = (row: DetailRow | undefined) => row?.id?.startsWith('task-none-') ?? false
|
||||
|
||||
const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] => {
|
||||
const dbValueMap = new Map<string, DetailRow>()
|
||||
for (const row of rowsFromDb || []) {
|
||||
@ -146,9 +154,6 @@ const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] =>
|
||||
return {
|
||||
...row,
|
||||
workload: typeof fromDb.workload === 'number' ? fromDb.workload : null,
|
||||
budgetBase: typeof fromDb.budgetBase === 'number' ? fromDb.budgetBase : null,
|
||||
budgetReferenceUnitPrice:
|
||||
typeof fromDb.budgetReferenceUnitPrice === 'number' ? fromDb.budgetReferenceUnitPrice : null,
|
||||
budgetAdoptedUnitPrice:
|
||||
typeof fromDb.budgetAdoptedUnitPrice === 'number' ? fromDb.budgetAdoptedUnitPrice : null,
|
||||
consultCategoryFactor:
|
||||
@ -161,11 +166,13 @@ const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] =>
|
||||
|
||||
const parseNumberOrNull = (value: unknown) => {
|
||||
if (value === '' || value == null) return null
|
||||
const v = Number(value)
|
||||
const normalized = typeof value === 'string' ? value.replace(/[^0-9.\-]/g, '') : value
|
||||
const v = Number(normalized)
|
||||
return Number.isFinite(v) ? v : null
|
||||
}
|
||||
|
||||
const formatEditableNumber = (params: any) => {
|
||||
if (isNoTaskRow(params.data)) return '无'
|
||||
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
|
||||
return '点击输入'
|
||||
}
|
||||
@ -173,15 +180,84 @@ const formatEditableNumber = (params: any) => {
|
||||
return Number(params.value).toFixed(2)
|
||||
}
|
||||
|
||||
const isRepeatedTaskNameRow = (params: any) => {
|
||||
const rowIndex = params.node?.rowIndex
|
||||
if (typeof rowIndex !== 'number' || rowIndex <= 0) return false
|
||||
const prevData = params.api.getDisplayedRowAtIndex(rowIndex - 1)?.data as DetailRow | undefined
|
||||
return prevData?.taskName === params.data?.taskName
|
||||
}
|
||||
|
||||
const getTaskNameRowSpan = (params: any) => {
|
||||
if (params.node?.rowPinned) return 1
|
||||
if (isRepeatedTaskNameRow(params)) return 1
|
||||
const currentName = params.data?.taskName
|
||||
if (!currentName) return 1
|
||||
let span = 1
|
||||
let offset = 1
|
||||
while (true) {
|
||||
const nextData = params.api.getDisplayedRowAtIndex((params.node?.rowIndex ?? 0) + offset)?.data as DetailRow | undefined
|
||||
if (!nextData || nextData.taskName !== currentName) break
|
||||
span += 1
|
||||
offset += 1
|
||||
}
|
||||
return span
|
||||
}
|
||||
|
||||
const columnDefs: ColDef<DetailRow>[] = [
|
||||
{
|
||||
headerName: '工作内容',
|
||||
minWidth: 220,
|
||||
width: 240,
|
||||
headerName: '编码',
|
||||
field: 'taskCode',
|
||||
minWidth: 150,
|
||||
width: 170,
|
||||
pinned: 'left',
|
||||
valueGetter: params => {
|
||||
if (params.node?.rowPinned) return ''
|
||||
return params.node?.group ? params.data?.groupName || '' : params.data?.majorName || ''
|
||||
valueFormatter: params => params.value || ''
|
||||
},
|
||||
{
|
||||
headerName: '名称',
|
||||
field: 'taskName',
|
||||
minWidth: 220,
|
||||
width: 260,
|
||||
pinned: 'left',
|
||||
rowSpan: params => getTaskNameRowSpan(params),
|
||||
valueFormatter: params => (isRepeatedTaskNameRow(params) ? '' : params.value || '')
|
||||
},
|
||||
{
|
||||
headerName: '预算基数',
|
||||
field: 'budgetBase',
|
||||
minWidth: 170,
|
||||
flex: 1,
|
||||
valueFormatter: params => params.value || ''
|
||||
},
|
||||
{
|
||||
headerName: '预算参考单价',
|
||||
field: 'budgetReferenceUnitPrice',
|
||||
minWidth: 170,
|
||||
flex: 1,
|
||||
valueFormatter: params => params.value || ''
|
||||
},
|
||||
{
|
||||
headerName: '预算采用单价',
|
||||
field: 'budgetAdoptedUnitPrice',
|
||||
minWidth: 170,
|
||||
flex: 1,
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned && !isNoTaskRow(params.data),
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group &&
|
||||
!params.node?.rowPinned &&
|
||||
!isNoTaskRow(params.data) &&
|
||||
(params.value == null || params.value === '')
|
||||
},
|
||||
valueParser: params => parseNumberOrNull(params.newValue),
|
||||
valueFormatter: params => {
|
||||
if (isNoTaskRow(params.data)) return '无'
|
||||
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
|
||||
return '点击输入'
|
||||
}
|
||||
if (params.value == null) return ''
|
||||
const unit = params.data?.unit || ''
|
||||
return `${Number(params.value).toFixed(2)}${unit}`
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -189,57 +265,16 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
field: 'workload',
|
||||
minWidth: 140,
|
||||
flex: 1,
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned && !isNoTaskRow(params.data),
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
aggFunc: 'sum',
|
||||
valueParser: params => parseNumberOrNull(params.newValue),
|
||||
valueFormatter: formatEditableNumber
|
||||
},
|
||||
{
|
||||
headerName: '预算基数',
|
||||
field: 'budgetBase',
|
||||
minWidth: 170,
|
||||
flex: 1,
|
||||
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
aggFunc: 'sum',
|
||||
valueParser: params => parseNumberOrNull(params.newValue),
|
||||
valueFormatter: formatEditableNumber
|
||||
},
|
||||
{
|
||||
headerName: '预算参考单价',
|
||||
field: 'budgetReferenceUnitPrice',
|
||||
minWidth: 170,
|
||||
flex: 1,
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
valueParser: params => parseNumberOrNull(params.newValue),
|
||||
valueFormatter: formatEditableNumber
|
||||
},
|
||||
{
|
||||
headerName: '预算采用单价',
|
||||
field: 'budgetAdoptedUnitPrice',
|
||||
minWidth: 170,
|
||||
flex: 1,
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
!params.node?.group &&
|
||||
!params.node?.rowPinned &&
|
||||
!isNoTaskRow(params.data) &&
|
||||
(params.value == null || params.value === '')
|
||||
},
|
||||
aggFunc: decimalAggSum,
|
||||
valueParser: params => parseNumberOrNull(params.newValue),
|
||||
valueFormatter: formatEditableNumber
|
||||
},
|
||||
@ -248,11 +283,14 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
field: 'consultCategoryFactor',
|
||||
minWidth: 150,
|
||||
flex: 1,
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned && !isNoTaskRow(params.data),
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
!params.node?.group &&
|
||||
!params.node?.rowPinned &&
|
||||
!isNoTaskRow(params.data) &&
|
||||
(params.value == null || params.value === '')
|
||||
},
|
||||
valueParser: params => parseNumberOrNull(params.newValue),
|
||||
valueFormatter: formatEditableNumber
|
||||
@ -262,13 +300,16 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
field: 'serviceFee',
|
||||
minWidth: 150,
|
||||
flex: 1,
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned && !isNoTaskRow(params.data),
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
!params.node?.group &&
|
||||
!params.node?.rowPinned &&
|
||||
!isNoTaskRow(params.data) &&
|
||||
(params.value == null || params.value === '')
|
||||
},
|
||||
aggFunc: 'sum',
|
||||
aggFunc: decimalAggSum,
|
||||
valueParser: params => parseNumberOrNull(params.newValue),
|
||||
valueFormatter: formatEditableNumber
|
||||
},
|
||||
@ -277,76 +318,40 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
field: 'remark',
|
||||
minWidth: 180,
|
||||
flex: 1.2,
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellEditor: 'agLargeTextCellEditor',
|
||||
wrapText: true,
|
||||
autoHeight: true,
|
||||
cellStyle: { whiteSpace: 'normal', lineHeight: '1.4' },
|
||||
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned && !isNoTaskRow(params.data),
|
||||
valueFormatter: params => {
|
||||
if (isNoTaskRow(params.data)) return '无'
|
||||
if (!params.node?.group && !params.node?.rowPinned && !params.value) return '点击输入'
|
||||
return params.value || ''
|
||||
},
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line remark-wrap-cell' : ''),
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
!params.node?.group &&
|
||||
!params.node?.rowPinned &&
|
||||
!isNoTaskRow(params.data) &&
|
||||
(params.value == null || params.value === '')
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const autoGroupColumnDef: ColDef = {
|
||||
headerName: '编码',
|
||||
minWidth: 160,
|
||||
pinned: 'left',
|
||||
width: 170,
|
||||
const totalWorkload = computed(() => sumByNumber(detailRows.value, row => row.workload))
|
||||
|
||||
cellRendererParams: {
|
||||
suppressCount: true
|
||||
},
|
||||
valueFormatter: params => {
|
||||
if (params.node?.rowPinned) {
|
||||
return '总合计'
|
||||
}
|
||||
const nodeId = String(params.value || '')
|
||||
const label = idLabelMap.get(nodeId) || nodeId
|
||||
return label.includes(' ') ? label.split(' ')[0] : label
|
||||
}
|
||||
}
|
||||
|
||||
const gridOptions: GridOptions<DetailRow> = {
|
||||
treeData: true,
|
||||
animateRows: true,
|
||||
singleClickEdit: true,
|
||||
suppressClickEdit: false,
|
||||
suppressContextMenu: false,
|
||||
groupDefaultExpanded: -1,
|
||||
suppressFieldDotNotation: true,
|
||||
getDataPath: data => data.path,
|
||||
getContextMenuItems: () => ['copy', 'paste', 'separator', 'export'],
|
||||
defaultColDef: {
|
||||
resizable: true,
|
||||
sortable: false,
|
||||
filter: false
|
||||
}
|
||||
}
|
||||
|
||||
const totalBudgetBase = computed(() =>
|
||||
detailRows.value.reduce((sum, row) => sum + (row.budgetBase || 0), 0)
|
||||
)
|
||||
|
||||
const totalWorkload = computed(() =>
|
||||
detailRows.value.reduce((sum, row) => sum + (row.workload || 0), 0)
|
||||
)
|
||||
|
||||
const totalServiceFee = computed(() =>
|
||||
detailRows.value.reduce((sum, row) => sum + (row.serviceFee || 0), 0)
|
||||
)
|
||||
const totalServiceFee = computed(() => sumByNumber(detailRows.value, row => row.serviceFee))
|
||||
const pinnedTopRowData = computed(() => [
|
||||
{
|
||||
id: 'pinned-total-row',
|
||||
groupCode: '',
|
||||
groupName: '',
|
||||
majorCode: '',
|
||||
majorName: '',
|
||||
taskCode: '',
|
||||
taskName: '',
|
||||
unit: '',
|
||||
workload: totalWorkload.value,
|
||||
budgetBase: totalBudgetBase.value,
|
||||
budgetReferenceUnitPrice: null,
|
||||
budgetBase: '',
|
||||
budgetReferenceUnitPrice: '',
|
||||
budgetAdoptedUnitPrice: null,
|
||||
consultCategoryFactor: null,
|
||||
serviceFee: totalServiceFee.value,
|
||||
@ -358,6 +363,7 @@ const pinnedTopRowData = computed(() => [
|
||||
|
||||
|
||||
const saveToIndexedDB = async () => {
|
||||
if (shouldSkipPersist()) return
|
||||
try {
|
||||
const payload = {
|
||||
detailRows: JSON.parse(JSON.stringify(detailRows.value))
|
||||
@ -371,6 +377,11 @@ const saveToIndexedDB = async () => {
|
||||
|
||||
const loadFromIndexedDB = async () => {
|
||||
try {
|
||||
if (shouldForceDefaultLoad()) {
|
||||
detailRows.value = buildDefaultRows()
|
||||
return
|
||||
}
|
||||
|
||||
const data = await localforage.getItem<XmInfoState>(DB_KEY.value)
|
||||
if (data) {
|
||||
detailRows.value = mergeWithDictRows(data.detailRows)
|
||||
@ -384,6 +395,14 @@ const loadFromIndexedDB = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => pricingPaneReloadStore.getReloadVersion(props.contractId, props.serviceId),
|
||||
(nextVersion, prevVersion) => {
|
||||
if (nextVersion === prevVersion || nextVersion === 0) return
|
||||
void loadFromIndexedDB()
|
||||
}
|
||||
)
|
||||
|
||||
let persistTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
|
||||
@ -397,7 +416,6 @@ const handleCellValueChanged = () => {
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
await loadFromIndexedDB()
|
||||
})
|
||||
@ -436,58 +454,15 @@ const processCellFromClipboard = (params:any) => {
|
||||
</div>
|
||||
|
||||
<div class="ag-theme-quartz h-full min-h-0 w-full flex-1">
|
||||
<AgGridVue
|
||||
:style="{ height: '100%' }"
|
||||
:rowData="detailRows"
|
||||
:pinnedTopRowData="pinnedTopRowData"
|
||||
:columnDefs="columnDefs"
|
||||
:autoGroupColumnDef="autoGroupColumnDef"
|
||||
:gridOptions="gridOptions"
|
||||
:theme="myTheme"
|
||||
@cell-value-changed="handleCellValueChanged"
|
||||
:suppressColumnVirtualisation="true"
|
||||
:suppressRowVirtualisation="true"
|
||||
:cellSelection="{ handle: { mode: 'range' } }"
|
||||
:enableClipboard="true"
|
||||
:localeText="AG_GRID_LOCALE_CN"
|
||||
:tooltipShowDelay="500"
|
||||
:headerHeight="50"
|
||||
:processCellForClipboard="processCellForClipboard"
|
||||
:processCellFromClipboard="processCellFromClipboard"
|
||||
:undoRedoCellEditing="true"
|
||||
:undoRedoCellEditingLimit="20"
|
||||
|
||||
/>
|
||||
<AgGridVue :style="{ height: '100%' }" :rowData="detailRows" :pinnedTopRowData="pinnedTopRowData"
|
||||
:columnDefs="columnDefs" :gridOptions="gridOptions" :theme="myTheme" :treeData="false"
|
||||
@cell-value-changed="handleCellValueChanged" :suppressColumnVirtualisation="true"
|
||||
:suppressRowVirtualisation="true" :suppressRowTransform="true"
|
||||
:cellSelection="{ handle: { mode: 'range' } }" :enableClipboard="true"
|
||||
:localeText="AG_GRID_LOCALE_CN" :tooltipShowDelay="500" :headerHeight="50"
|
||||
:processCellForClipboard="processCellForClipboard" :processCellFromClipboard="processCellFromClipboard"
|
||||
:undoRedoCellEditing="true" :undoRedoCellEditingLimit="20" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style >
|
||||
.ag-floating-top{
|
||||
overflow-y:auto !important
|
||||
}
|
||||
|
||||
.xmMx .editable-cell-line .ag-cell-value {
|
||||
display: inline-block;
|
||||
min-width: 84%;
|
||||
padding: 2px 4px;
|
||||
border-bottom: 1px solid #cbd5e1;
|
||||
}
|
||||
|
||||
.xmMx .editable-cell-line.ag-cell-focus .ag-cell-value,
|
||||
.xmMx .editable-cell-line:hover .ag-cell-value {
|
||||
border-bottom-color: #2563eb;
|
||||
}
|
||||
|
||||
.xmMx .editable-cell-empty .ag-cell-value {
|
||||
color: #94a3b8 !important;
|
||||
font-style: italic;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.xmMx .ag-cell.editable-cell-empty,
|
||||
.xmMx .ag-cell.editable-cell-empty .ag-cell-value {
|
||||
color: #94a3b8 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { AgGridVue } from 'ag-grid-vue3'
|
||||
import type { ColDef, GridOptions } from 'ag-grid-community'
|
||||
import type { ColDef } from 'ag-grid-community'
|
||||
import localforage from 'localforage'
|
||||
import { majorList } from '@/sql'
|
||||
import 'ag-grid-enterprise'
|
||||
import { myTheme } from '@/lib/diyAgGridTheme'
|
||||
import { myTheme ,gridOptions} from '@/lib/diyAgGridOptions'
|
||||
import { decimalAggSum, sumByNumber } from '@/lib/decimal'
|
||||
|
||||
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale';
|
||||
// 精简的边框配置(细线条+浅灰色,弱化分割线视觉)
|
||||
@ -220,7 +220,7 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
aggFunc: 'sum',
|
||||
aggFunc: decimalAggSum,
|
||||
valueParser: params => {
|
||||
if (params.newValue === '' || params.newValue == null) return null
|
||||
const v = Number(params.newValue)
|
||||
@ -246,7 +246,7 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
aggFunc: 'sum',
|
||||
aggFunc: decimalAggSum,
|
||||
valueParser: params => {
|
||||
if (params.newValue === '' || params.newValue == null) return null
|
||||
const v = Number(params.newValue)
|
||||
@ -280,30 +280,11 @@ const autoGroupColumnDef: ColDef = {
|
||||
}
|
||||
}
|
||||
|
||||
const gridOptions: GridOptions<DetailRow> = {
|
||||
treeData: true,
|
||||
animateRows: true,
|
||||
singleClickEdit: true,
|
||||
suppressClickEdit: false,
|
||||
suppressContextMenu: false,
|
||||
groupDefaultExpanded: -1,
|
||||
suppressFieldDotNotation: true,
|
||||
getDataPath: data => data.path,
|
||||
getContextMenuItems: () => ['copy', 'paste', 'separator', 'export'],
|
||||
defaultColDef: {
|
||||
resizable: true,
|
||||
sortable: false,
|
||||
filter: false
|
||||
}
|
||||
}
|
||||
|
||||
const totalAmount = computed(() =>
|
||||
detailRows.value.reduce((sum, row) => sum + (row.amount || 0), 0)
|
||||
)
|
||||
|
||||
const totalLandArea = computed(() =>
|
||||
detailRows.value.reduce((sum, row) => sum + (row.landArea || 0), 0)
|
||||
)
|
||||
const totalAmount = computed(() => sumByNumber(detailRows.value, row => row.amount))
|
||||
|
||||
const totalLandArea = computed(() => sumByNumber(detailRows.value, row => row.landArea))
|
||||
const pinnedTopRowData = computed(() => [
|
||||
{
|
||||
id: 'pinned-total-row',
|
||||
@ -460,31 +441,3 @@ const processCellFromClipboard = (params:any) => {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style >
|
||||
.ag-floating-top{
|
||||
overflow-y:auto !important
|
||||
}
|
||||
|
||||
.xmMx .editable-cell-line .ag-cell-value {
|
||||
display: inline-block;
|
||||
min-width: 84%;
|
||||
padding: 2px 4px;
|
||||
border-bottom: 1px solid #cbd5e1;
|
||||
}
|
||||
|
||||
.xmMx .editable-cell-line.ag-cell-focus .ag-cell-value,
|
||||
.xmMx .editable-cell-line:hover .ag-cell-value {
|
||||
border-bottom-color: #2563eb;
|
||||
}
|
||||
|
||||
.xmMx .editable-cell-empty .ag-cell-value {
|
||||
color: #94a3b8 !important;
|
||||
font-style: italic;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.xmMx .ag-cell.editable-cell-empty,
|
||||
.xmMx .ag-cell.editable-cell-empty .ag-cell-value {
|
||||
color: #94a3b8 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -4,9 +4,9 @@ import type { ComponentPublicInstance } from 'vue'
|
||||
import { AgGridVue } from 'ag-grid-vue3'
|
||||
import type { ColDef, GridOptions, ICellRendererParams } from 'ag-grid-community'
|
||||
import localforage from 'localforage'
|
||||
import 'ag-grid-enterprise'
|
||||
import { myTheme } from '@/lib/diyAgGridTheme'
|
||||
import { myTheme ,gridOptions} from '@/lib/diyAgGridOptions'
|
||||
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
|
||||
import { addNumbers } from '@/lib/decimal'
|
||||
import { Search } from 'lucide-vue-next'
|
||||
import {
|
||||
DialogClose,
|
||||
@ -21,6 +21,7 @@ import {
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { serviceList } from '@/sql'
|
||||
import { useTabStore } from '@/pinia/tab'
|
||||
import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
||||
|
||||
interface ServiceItem {
|
||||
id: string
|
||||
@ -50,7 +51,11 @@ const props = defineProps<{
|
||||
contractId: string
|
||||
}>()
|
||||
const tabStore = useTabStore()
|
||||
const pricingPaneReloadStore = usePricingPaneReloadStore()
|
||||
const DB_KEY = computed(() => `zxFW-${props.contractId}`)
|
||||
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
|
||||
const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
|
||||
const PRICING_CLEAR_SKIP_TTL_MS = 5000
|
||||
|
||||
type ServiceListItem = { code?: string; ref?: string; name: string; defCoe: number | null }
|
||||
const serviceDict: ServiceItem[] = Object.entries(serviceList as Record<string, ServiceListItem>)
|
||||
@ -146,8 +151,9 @@ const pickerOpen = ref(false)
|
||||
const pickerTempIds = ref<string[]>([])
|
||||
const pickerSearch = ref('')
|
||||
const dragSelecting = ref(false)
|
||||
const dragMoved = ref(false)
|
||||
let dragSelectChecked = false
|
||||
const dragAppliedCodes = new Set<string>()
|
||||
const dragBaseIds = ref<string[]>([])
|
||||
const dragStartPoint = ref({ x: 0, y: 0 })
|
||||
const dragCurrentPoint = ref({ x: 0, y: 0 })
|
||||
const pickerItemElMap = new Map<string, HTMLElement>()
|
||||
@ -198,13 +204,19 @@ const getPricingPaneStorageKeys = (serviceId: string) => [
|
||||
|
||||
const clearPricingPaneValues = async (serviceId: string) => {
|
||||
const keys = getPricingPaneStorageKeys(serviceId)
|
||||
const skipUntil = Date.now() + PRICING_CLEAR_SKIP_TTL_MS
|
||||
for (const key of keys) {
|
||||
sessionStorage.setItem(`${PRICING_CLEAR_SKIP_PREFIX}${key}`, String(skipUntil))
|
||||
sessionStorage.setItem(`${PRICING_FORCE_DEFAULT_PREFIX}${key}`, String(skipUntil))
|
||||
}
|
||||
await Promise.all(keys.map(key => localforage.removeItem(key)))
|
||||
pricingPaneReloadStore.markReload(props.contractId, serviceId)
|
||||
}
|
||||
|
||||
const clearRowValues = async (row: DetailRow) => {
|
||||
if (isFixedRow(row)) return
|
||||
|
||||
// 若该服务编辑页已打开,先关闭,避免子页面卸载时把旧数据写回缓<EFBFBD>? tabStore.removeTab(`zxfw-edit-${props.contractId}-${row.id}`)
|
||||
// 若该服务编辑页已打开,先关闭,避免子页面卸载时把旧数据写回缓? tabStore.removeTab(`zxfw-edit-${props.contractId}-${row.id}`)
|
||||
await nextTick()
|
||||
|
||||
row.investScale = null
|
||||
@ -215,11 +227,6 @@ const clearRowValues = async (row: DetailRow) => {
|
||||
detailRows.value = [...detailRows.value]
|
||||
|
||||
await clearPricingPaneValues(row.id)
|
||||
// 定时<EFBFBD>?卸载保存可能产生回写,延迟再清一次更稳妥
|
||||
await new Promise(resolve => setTimeout(resolve, 80))
|
||||
await clearPricingPaneValues(row.id)
|
||||
|
||||
await saveToIndexedDB()
|
||||
}
|
||||
|
||||
const openEditTab = (row: DetailRow) => {
|
||||
@ -285,10 +292,10 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
editable: false,
|
||||
valueGetter: params => {
|
||||
if (!params.data) return null
|
||||
return (
|
||||
valueOrZero(params.data.investScale) +
|
||||
valueOrZero(params.data.landScale) +
|
||||
valueOrZero(params.data.workload) +
|
||||
return addNumbers(
|
||||
valueOrZero(params.data.investScale),
|
||||
valueOrZero(params.data.landScale),
|
||||
valueOrZero(params.data.workload),
|
||||
valueOrZero(params.data.hourly)
|
||||
)
|
||||
},
|
||||
@ -308,22 +315,17 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
isFixedRow(params.data)
|
||||
? ''
|
||||
: `<div class="zxfw-action-wrap">
|
||||
<button class="zxfw-action-btn" data-action="clear" title="清空">🧹</button>
|
||||
<button class="zxfw-action-btn" data-action="edit" title="编辑">✏️</button>
|
||||
|
||||
<button class="zxfw-action-btn" data-action="clear" title="清空(重置到默认状态)">🧹</button>
|
||||
</div>`
|
||||
}
|
||||
]
|
||||
|
||||
const gridOptions: GridOptions<DetailRow> = {
|
||||
animateRows: true,
|
||||
singleClickEdit: true,
|
||||
suppressClickEdit: false,
|
||||
suppressContextMenu: false,
|
||||
defaultColDef: {
|
||||
resizable: true,
|
||||
sortable: false,
|
||||
filter: false
|
||||
},
|
||||
const detailGridOptions: GridOptions<DetailRow> = {
|
||||
...gridOptions,
|
||||
treeData: false,
|
||||
getDataPath: undefined,
|
||||
onCellClicked: async params => {
|
||||
if (params.colDef.field !== 'actions' || !params.data || isFixedRow(params.data)) return
|
||||
const target = params.event?.target as HTMLElement | null
|
||||
@ -462,45 +464,59 @@ const applyDragSelectionByRect = () => {
|
||||
bottom: Math.max(dragStartPoint.value.y, dragCurrentPoint.value.y)
|
||||
}
|
||||
|
||||
const nextSelectedSet = new Set(dragBaseIds.value)
|
||||
for (const [code, el] of pickerItemElMap.entries()) {
|
||||
if (dragAppliedCodes.has(code)) continue
|
||||
const itemRect = el.getBoundingClientRect()
|
||||
const hit = isRectIntersect(rect, itemRect)
|
||||
if (hit) {
|
||||
applyTempChecked(code, dragSelectChecked)
|
||||
dragAppliedCodes.add(code)
|
||||
if (dragSelectChecked) {
|
||||
nextSelectedSet.add(code)
|
||||
} else {
|
||||
nextSelectedSet.delete(code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pickerTempIds.value = serviceDict
|
||||
.map(item => item.id)
|
||||
.filter(id => nextSelectedSet.has(id))
|
||||
}
|
||||
|
||||
const stopDragSelect = () => {
|
||||
dragSelecting.value = false
|
||||
dragAppliedCodes.clear()
|
||||
dragMoved.value = false
|
||||
dragBaseIds.value = []
|
||||
window.removeEventListener('mousemove', onDragSelectingMove)
|
||||
window.removeEventListener('mouseup', stopDragSelect)
|
||||
}
|
||||
|
||||
const onDragSelectingMove = (event: MouseEvent) => {
|
||||
dragCurrentPoint.value = { x: event.clientX, y: event.clientY }
|
||||
if (!dragMoved.value) {
|
||||
const dx = Math.abs(event.clientX - dragStartPoint.value.x)
|
||||
const dy = Math.abs(event.clientY - dragStartPoint.value.y)
|
||||
if (dx >= 3 || dy >= 3) {
|
||||
dragMoved.value = true
|
||||
}
|
||||
}
|
||||
applyDragSelectionByRect()
|
||||
}
|
||||
|
||||
const startDragSelect = (event: MouseEvent, code: string) => {
|
||||
dragSelecting.value = true
|
||||
dragAppliedCodes.clear()
|
||||
dragMoved.value = false
|
||||
dragBaseIds.value = [...pickerTempIds.value]
|
||||
dragSelectChecked = !pickerTempIds.value.includes(code)
|
||||
dragStartPoint.value = { x: event.clientX, y: event.clientY }
|
||||
dragCurrentPoint.value = { x: event.clientX, y: event.clientY }
|
||||
applyTempChecked(code, dragSelectChecked)
|
||||
dragAppliedCodes.add(code)
|
||||
window.addEventListener('mousemove', onDragSelectingMove)
|
||||
window.addEventListener('mouseup', stopDragSelect)
|
||||
}
|
||||
|
||||
const handleDragHover = (code: string) => {
|
||||
if (!dragSelecting.value || dragAppliedCodes.has(code)) return
|
||||
applyTempChecked(code, dragSelectChecked)
|
||||
dragAppliedCodes.add(code)
|
||||
const handleDragHover = (_code: string) => {
|
||||
if (!dragSelecting.value || !dragMoved.value) return
|
||||
applyDragSelectionByRect()
|
||||
}
|
||||
|
||||
const saveToIndexedDB = async () => {
|
||||
@ -615,7 +631,12 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-2 md:grid-cols-2">
|
||||
<label v-for="item in filteredServiceDict" :key="item.id" :ref="el => setPickerItemRef(item.id, el)"
|
||||
class="flex select-none items-center gap-2 rounded-md border px-3 py-2 text-sm"
|
||||
:class="[
|
||||
'picker-item-clickable flex select-none items-center gap-2 rounded-md border px-3 py-2 text-sm',
|
||||
dragMoved ? 'cursor-default picker-item-dragging' : 'cursor-pointer',
|
||||
pickerTempIds.includes(item.id) ? 'picker-item-selected' : '',
|
||||
dragSelecting && pickerTempIds.includes(item.id) ? 'picker-item-selected-drag' : ''
|
||||
]"
|
||||
@mousedown.prevent="startDragSelect($event, item.id)" @mouseenter="handleDragHover(item.id)"
|
||||
@click.prevent>
|
||||
<input type="checkbox" :checked="pickerTempIds.includes(item.id)" class="pointer-events-none" />
|
||||
@ -652,7 +673,7 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
|
||||
<div ref="agGridRef" class="ag-theme-quartz w-full h-full" >
|
||||
<AgGridVue :style="{ height: '100%' }" :rowData="detailRows" :columnDefs="columnDefs" :gridOptions="gridOptions"
|
||||
<AgGridVue :style="{ height: '100%' }" :rowData="detailRows" :columnDefs="columnDefs" :gridOptions="detailGridOptions"
|
||||
:theme="myTheme" @cell-value-changed="handleCellValueChanged" :enableClipboard="true"
|
||||
:localeText="AG_GRID_LOCALE_CN" :tooltipShowDelay="500" :headerHeight="50" :undoRedoCellEditing="true"
|
||||
:undoRedoCellEditingLimit="20" />
|
||||
@ -683,4 +704,25 @@ onBeforeUnmount(() => {
|
||||
line-height: 1;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.picker-item-clickable {
|
||||
transition: transform 120ms ease, border-color 120ms ease, box-shadow 120ms ease;
|
||||
}
|
||||
|
||||
.picker-item-selected {
|
||||
transform: translateY(-1px);
|
||||
border-color: rgba(14, 165, 233, 0.75);
|
||||
box-shadow: inset 0 0 0 1px rgba(14, 165, 233, 0.3);
|
||||
background: rgba(14, 165, 233, 0.08);
|
||||
}
|
||||
|
||||
.picker-item-selected-drag {
|
||||
box-shadow: inset 0 0 0 2px rgba(14, 165, 233, 0.35), 0 0 0 1px rgba(14, 165, 233, 0.25);
|
||||
}
|
||||
|
||||
.picker-item-clickable:not(.picker-item-dragging):active {
|
||||
transform: translateY(1px) scale(0.985);
|
||||
border-color: rgba(14, 165, 233, 0.8);
|
||||
box-shadow: inset 0 0 0 2px rgba(14, 165, 233, 0.22);
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -90,6 +90,19 @@ const openTabContextMenu = (event: MouseEvent, tabId: string) => {
|
||||
tabContextX.value = event.clientX
|
||||
tabContextY.value = event.clientY
|
||||
tabContextOpen.value = true
|
||||
void nextTick(() => {
|
||||
if (!tabContextRef.value) return
|
||||
const gap = 8
|
||||
const rect = tabContextRef.value.getBoundingClientRect()
|
||||
if (tabContextX.value + rect.width + gap > window.innerWidth) {
|
||||
tabContextX.value = Math.max(gap, window.innerWidth - rect.width - gap)
|
||||
}
|
||||
if (tabContextY.value + rect.height + gap > window.innerHeight) {
|
||||
tabContextY.value = Math.max(gap, window.innerHeight - rect.height - gap)
|
||||
}
|
||||
if (tabContextX.value < gap) tabContextX.value = gap
|
||||
if (tabContextY.value < gap) tabContextY.value = gap
|
||||
})
|
||||
}
|
||||
|
||||
const handleGlobalMouseDown = (event: MouseEvent) => {
|
||||
@ -456,32 +469,32 @@ watch(
|
||||
<div
|
||||
v-if="tabContextOpen"
|
||||
ref="tabContextRef"
|
||||
class="fixed z-[70] min-w-[150px] rounded-md border bg-background p-1 shadow-lg"
|
||||
class="fixed z-[70] w-max rounded-lg border border-border/80 bg-background/95 p-1.5 shadow-xl ring-1 ring-black/5 backdrop-blur-sm"
|
||||
:style="{ left: `${tabContextX}px`, top: `${tabContextY}px` }"
|
||||
>
|
||||
<button
|
||||
class="w-full rounded px-3 py-1.5 text-left text-sm hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50"
|
||||
class="flex cursor-pointer items-center whitespace-nowrap rounded-md px-3 py-1.5 text-left text-sm transition-colors hover:bg-muted/80 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:disabled="!hasClosableTabs"
|
||||
@click="runTabMenuAction('all')"
|
||||
>
|
||||
删除所有
|
||||
</button>
|
||||
<button
|
||||
class="w-full rounded px-3 py-1.5 text-left text-sm hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50"
|
||||
class="mt-0.5 flex cursor-pointer items-center whitespace-nowrap rounded-md px-3 py-1.5 text-left text-sm transition-colors hover:bg-muted/80 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:disabled="!canCloseLeft"
|
||||
@click="runTabMenuAction('left')"
|
||||
>
|
||||
删除左侧
|
||||
</button>
|
||||
<button
|
||||
class="w-full rounded px-3 py-1.5 text-left text-sm hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50"
|
||||
class="mt-0.5 flex cursor-pointer items-center whitespace-nowrap rounded-md px-3 py-1.5 text-left text-sm transition-colors hover:bg-muted/80 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:disabled="!canCloseRight"
|
||||
@click="runTabMenuAction('right')"
|
||||
>
|
||||
删除右侧
|
||||
</button>
|
||||
<button
|
||||
class="w-full rounded px-3 py-1.5 text-left text-sm hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50"
|
||||
class="mt-0.5 flex cursor-pointer items-center whitespace-nowrap rounded-md px-3 py-1.5 text-left text-sm transition-colors hover:bg-muted/80 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:disabled="!canCloseOther"
|
||||
@click="runTabMenuAction('other')"
|
||||
>
|
||||
|
||||
37
src/lib/decimal.ts
Normal file
37
src/lib/decimal.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import Decimal from 'decimal.js'
|
||||
|
||||
type MaybeNumber = number | null | undefined
|
||||
type DecimalInput = Decimal.Value
|
||||
|
||||
export const toDecimal = (value: DecimalInput) => new Decimal(value)
|
||||
|
||||
export const roundTo = (value: DecimalInput, decimalPlaces = 2) =>
|
||||
new Decimal(value).toDecimalPlaces(decimalPlaces, Decimal.ROUND_HALF_UP).toNumber()
|
||||
|
||||
export const addNumbers = (...values: MaybeNumber[]) => {
|
||||
let total = new Decimal(0)
|
||||
for (const value of values) {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value)) continue
|
||||
total = total.plus(value)
|
||||
}
|
||||
return total.toNumber()
|
||||
}
|
||||
|
||||
export const sumByNumber = <T>(list: T[], pick: (item: T) => MaybeNumber) => {
|
||||
let total = new Decimal(0)
|
||||
for (const item of list) {
|
||||
const value = pick(item)
|
||||
if (typeof value !== 'number' || !Number.isFinite(value)) continue
|
||||
total = total.plus(value)
|
||||
}
|
||||
return total.toNumber()
|
||||
}
|
||||
|
||||
export const decimalAggSum = (params: { values?: unknown[] }) => {
|
||||
let total = new Decimal(0)
|
||||
for (const value of params.values || []) {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value)) continue
|
||||
total = total.plus(value)
|
||||
}
|
||||
return total.toNumber()
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import {
|
||||
|
||||
GridOptions,
|
||||
themeQuartz
|
||||
} from "ag-grid-community"
|
||||
const borderConfig = {
|
||||
@ -28,3 +29,20 @@ export const myTheme = themeQuartz.withParams({
|
||||
// 可选:偶数行背景色(轻微区分,更清新)
|
||||
dataBackgroundColor: "#fefefe"
|
||||
});
|
||||
export const gridOptions: GridOptions<any> = {
|
||||
treeData: true,
|
||||
animateRows: true,
|
||||
suppressAggFuncInHeader: true,
|
||||
singleClickEdit: true,
|
||||
suppressClickEdit: false,
|
||||
suppressContextMenu: false,
|
||||
groupDefaultExpanded: -1,
|
||||
suppressFieldDotNotation: true,
|
||||
getDataPath: data => data.path,
|
||||
getContextMenuItems: () => ['copy', 'paste', 'separator', 'export'],
|
||||
defaultColDef: {
|
||||
resizable: true,
|
||||
sortable: false,
|
||||
filter: false
|
||||
}
|
||||
}
|
||||
28
src/pinia/pricingPaneReload.ts
Normal file
28
src/pinia/pricingPaneReload.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const buildReloadKey = (contractId: string, serviceId: string | number) =>
|
||||
`${contractId}::${String(serviceId)}`
|
||||
|
||||
export const usePricingPaneReloadStore = defineStore('pricing-pane-reload', () => {
|
||||
const reloadVersionMap = ref<Record<string, number>>({})
|
||||
|
||||
const markReload = (contractId: string, serviceId: string | number) => {
|
||||
const key = buildReloadKey(contractId, serviceId)
|
||||
const current = reloadVersionMap.value[key] || 0
|
||||
reloadVersionMap.value = {
|
||||
...reloadVersionMap.value,
|
||||
[key]: current + 1
|
||||
}
|
||||
}
|
||||
|
||||
const getReloadVersion = (contractId: string, serviceId: string | number) => {
|
||||
const key = buildReloadKey(contractId, serviceId)
|
||||
return reloadVersionMap.value[key] || 0
|
||||
}
|
||||
|
||||
return {
|
||||
markReload,
|
||||
getReloadVersion
|
||||
}
|
||||
})
|
||||
259
src/sql.ts
259
src/sql.ts
@ -1,3 +1,5 @@
|
||||
import { roundTo, toDecimal } from '@/lib/decimal'
|
||||
|
||||
export const majorList = {
|
||||
0: { code: 'E1', name: '交通运输工程通用专业', maxCoe: null, minCoe: null, defCoe: 1, desc: '' },
|
||||
1: { code: 'E1-1', name: '征地(用海)补偿', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于交通建设项目征地(用海)补偿的施工图预算、招标工程量清单及清单预算(或最高投标限价)、清理概算(仅限铁路工程)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算' },
|
||||
@ -33,112 +35,161 @@ export const majorList = {
|
||||
31: { code: 'E4-5', name: '附属房建工程(房屋建筑及附属工程)', maxCoe: null, minCoe: null, defCoe: 2.5, desc: '适用于房屋建筑与水运附属工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算' },
|
||||
}
|
||||
export const serviceList = {
|
||||
0: { code: 'D1', name: '全过程造价咨询', maxCoe: null, minCoe: null, defCoe: 1, desc: '', taskList: null },
|
||||
1: { code: 'D2', name: '分阶段造价咨询', maxCoe: null, minCoe: null, defCoe: null, desc: '', taskList: null },
|
||||
2: { code: 'D2-1', name: '前期阶段造价咨询', maxCoe: null, minCoe: null, defCoe: 0.5, desc: '', taskList: null },
|
||||
3: { code: 'D2-2-1', name: '实施阶段造价咨询(公路、水运)', maxCoe: null, minCoe: null, defCoe: 0.55, desc: '本系数适用于公路和水运工程。', taskList: null },
|
||||
4: { code: 'D2-2-2', name: '实施阶段造价咨询(铁路)', maxCoe: null, minCoe: null, defCoe: 0.6, desc: '本系数适用于铁路工程。', taskList: null },
|
||||
5: { code: 'D3', name: '基本造价咨询', maxCoe: null, minCoe: null, defCoe: null, desc: '', taskList: null },
|
||||
6: { code: 'D3-1', name: '投资估算', maxCoe: null, minCoe: null, defCoe: 0.1, desc: '委托同一咨询人同时负责D3-1和D3-2时,D3-1和D3-2的合计调整系数为0.25。', taskList: null },
|
||||
7: { code: 'D3-2', name: '设计概算', maxCoe: null, minCoe: null, defCoe: 0.2, desc: '', taskList: null },
|
||||
8: { code: 'D3-3', name: '施工图预算', maxCoe: null, minCoe: null, defCoe: 0.25, desc: '委托同一咨询人同时负责D3-3和D3-4时,D3-3和D3-4的合计调整系数为0.3。', taskList: null },
|
||||
9: { code: 'D3-4', name: '招标工程量清单及清单预算(或最高投标限价)', maxCoe: null, minCoe: null, defCoe: 0.15, desc: '', taskList: null },
|
||||
10: { code: 'D3-5', name: '清理概算(仅限铁路)', maxCoe: null, minCoe: null, defCoe: 0.2, desc: '本系数适用于铁路工程。', taskList: null },
|
||||
11: { code: 'D3-6-1', name: '合同(工程)结算', maxCoe: null, minCoe: null, defCoe: 0.3, desc: '本系数适用于公路和水运工程。', taskList: null },
|
||||
12: { code: 'D3-6-2', name: '合同(工程)结算', maxCoe: null, minCoe: null, defCoe: 0.2, desc: '本系数适用于铁路工程。', taskList: null },
|
||||
13: { code: 'D3-7', name: '竣工决算', maxCoe: null, minCoe: null, defCoe: 0.1, desc: '', taskList: null },
|
||||
14: { code: 'D4', name: '专项造价咨询', maxCoe: null, minCoe: null, defCoe: null, desc: '', taskList: null },
|
||||
15: { code: 'D4-1', name: '工程造价顾问', maxCoe: null, minCoe: null, defCoe: 1, desc: '本表系数适用于采用工作量计价法基准预算的调整系数。', taskList: [0, 1] },
|
||||
16: { code: 'D4-2', name: '造价政策制(修)订', maxCoe: null, minCoe: null, defCoe: 1, desc: '', taskList: [2, 3, 4, 5, 6, 7] },
|
||||
17: { code: 'D4-3', name: '造价科学与技术研究', maxCoe: null, minCoe: null, defCoe: 1, desc: '', taskList: [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21] },
|
||||
18: { code: 'D4-4', name: '定额测定', maxCoe: null, minCoe: null, defCoe: 1, desc: '', taskList: [22, 23, 24, 25, 26, 27, 28, 22, 23, 24, 25, 26, 27, 28] },
|
||||
19: { code: 'D4-5', name: '造价信息咨询', maxCoe: null, minCoe: null, defCoe: 1, desc: '', taskList: null },
|
||||
20: { code: 'D4-6', name: '造价鉴定', maxCoe: null, minCoe: null, defCoe: 0.5, desc: '本表系数适用于采用规模计价法基准预算的调整系数。', taskList: null },
|
||||
21: { code: 'D4-7', name: '工程成本测算', maxCoe: null, minCoe: null, defCoe: 0.1, desc: '', taskList: null },
|
||||
22: { code: 'D4-8', name: '工程成本核算', maxCoe: null, minCoe: null, defCoe: 0.1, desc: '', taskList: null },
|
||||
23: { code: 'D4-9', name: '计算工程量', maxCoe: null, minCoe: null, defCoe: 0.2, desc: '', taskList: null },
|
||||
24: { code: 'D4-10', name: '工程变更费用咨询', maxCoe: null, minCoe: null, defCoe: 0.5, desc: '', taskList: null },
|
||||
25: { code: 'D4-11', name: '调整估算', maxCoe: 0.2, minCoe: 0.1, defCoe: 0.15, desc: '', taskList: null },
|
||||
26: { code: 'D4-12', name: '调整概算', maxCoe: 0.3, minCoe: 0.15, defCoe: 0.225, desc: '本表系数适用于采用规模计价法基准预算的系数;依据其调整时期所在建设阶段和基础资料的不同,其系数取值不同。', taskList: null },
|
||||
27: { code: 'D4-13', name: '造价检查', maxCoe: null, minCoe: null, defCoe: null, desc: '可按照服务工日数量×服务工日人工单价×综合预算系数;也可按照服务工日数量×服务工日综合预算单价。', taskList: null },
|
||||
28: { code: 'D4-14', name: '其他专项咨询', maxCoe: null, minCoe: null, defCoe: null, desc: '可参照相同或相似服务的系数。', taskList: null },
|
||||
0: { code: 'D1', name: '全过程造价咨询', maxCoe: null, minCoe: null, defCoe: 1, desc: '' },
|
||||
1: { code: 'D2', name: '分阶段造价咨询', maxCoe: null, minCoe: null, defCoe: null, desc: '' },
|
||||
2: { code: 'D2-1', name: '前期阶段造价咨询', maxCoe: null, minCoe: null, defCoe: 0.5, desc: '' },
|
||||
3: { code: 'D2-2-1', name: '实施阶段造价咨询(公路、水运)', maxCoe: null, minCoe: null, defCoe: 0.55, desc: '本系数适用于公路和水运工程。' },
|
||||
4: { code: 'D2-2-2', name: '实施阶段造价咨询(铁路)', maxCoe: null, minCoe: null, defCoe: 0.6, desc: '本系数适用于铁路工程。' },
|
||||
5: { code: 'D3', name: '基本造价咨询', maxCoe: null, minCoe: null, defCoe: null, desc: '' },
|
||||
6: { code: 'D3-1', name: '投资估算', maxCoe: null, minCoe: null, defCoe: 0.1, desc: '委托同一咨询人同时负责D3-1和D3-2时,D3-1和D3-2的合计调整系数为0.25。' },
|
||||
7: { code: 'D3-2', name: '设计概算', maxCoe: null, minCoe: null, defCoe: 0.2, desc: '' },
|
||||
8: { code: 'D3-3', name: '施工图预算', maxCoe: null, minCoe: null, defCoe: 0.25, desc: '委托同一咨询人同时负责D3-3和D3-4时,D3-3和D3-4的合计调整系数为0.3。' },
|
||||
9: { code: 'D3-4', name: '招标工程量清单及清单预算(或最高投标限价)', maxCoe: null, minCoe: null, defCoe: 0.15, desc: '' },
|
||||
10: { code: 'D3-5', name: '清理概算(仅限铁路)', maxCoe: null, minCoe: null, defCoe: 0.2, desc: '本系数适用于铁路工程。' },
|
||||
11: { code: 'D3-6-1', name: '合同(工程)结算', maxCoe: null, minCoe: null, defCoe: 0.3, desc: '本系数适用于公路和水运工程。' },
|
||||
12: { code: 'D3-6-2', name: '合同(工程)结算', maxCoe: null, minCoe: null, defCoe: 0.2, desc: '本系数适用于铁路工程。' },
|
||||
13: { code: 'D3-7', name: '竣工决算', maxCoe: null, minCoe: null, defCoe: 0.1, desc: '' },
|
||||
14: { code: 'D4', name: '专项造价咨询', maxCoe: null, minCoe: null, defCoe: null, desc: '' },
|
||||
15: { code: 'D4-1', name: '工程造价顾问', maxCoe: null, minCoe: null, defCoe: 1, desc: '本表系数适用于采用工作量计价法基准预算的调整系数。' },
|
||||
16: { code: 'D4-2', name: '造价政策制(修)订', maxCoe: null, minCoe: null, defCoe: 1, desc: '' },
|
||||
17: { code: 'D4-3', name: '造价科学与技术研究', maxCoe: null, minCoe: null, defCoe: 1, desc: ''},
|
||||
18: { code: 'D4-4', name: '定额测定', maxCoe: null, minCoe: null, defCoe: 1, desc: ''},
|
||||
19: { code: 'D4-5', name: '造价信息咨询', maxCoe: null, minCoe: null, defCoe: 1, desc: '' },
|
||||
20: { code: 'D4-6', name: '造价鉴定', maxCoe: null, minCoe: null, defCoe: 0.5, desc: '本表系数适用于采用规模计价法基准预算的调整系数。' },
|
||||
21: { code: 'D4-7', name: '工程成本测算', maxCoe: null, minCoe: null, defCoe: 0.1, desc: '' },
|
||||
22: { code: 'D4-8', name: '工程成本核算', maxCoe: null, minCoe: null, defCoe: 0.1, desc: '' },
|
||||
23: { code: 'D4-9', name: '计算工程量', maxCoe: null, minCoe: null, defCoe: 0.2, desc: '' },
|
||||
24: { code: 'D4-10', name: '工程变更费用咨询', maxCoe: null, minCoe: null, defCoe: 0.5, desc: '' },
|
||||
25: { code: 'D4-11', name: '调整估算', maxCoe: 0.2, minCoe: 0.1, defCoe: 0.15, desc: '' },
|
||||
26: { code: 'D4-12', name: '调整概算', maxCoe: 0.3, minCoe: 0.15, defCoe: 0.225, desc: '本表系数适用于采用规模计价法基准预算的系数;依据其调整时期所在建设阶段和基础资料的不同,其系数取值不同。' },
|
||||
27: { code: 'D4-13', name: '造价检查', maxCoe: null, minCoe: null, defCoe: null, desc: '可按照服务工日数量×服务工日人工单价×综合预算系数;也可按照服务工日数量×服务工日综合预算单价。' },
|
||||
28: { code: 'D4-14', name: '其他专项咨询', maxCoe: null, minCoe: null, defCoe: null, desc: '可参照相同或相似服务的系数。' },
|
||||
};
|
||||
//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: '' },
|
||||
};
|
||||
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 },
|
||||
2: { code: 'C9-1-3', name: '中级工程师或二级造价工程师', maxPrice: 1500, minPrice: 1000, defPrice: 1250, manageCoe: 2.2 },
|
||||
3: { code: 'C9-1-4', name: '高级工程师或一级造价工程师', maxPrice: 1800, minPrice: 1500, defPrice: 1650, manageCoe: 2.1 },
|
||||
4: { code: 'C9-1-5', name: '正高级工程师或资深专家', maxPrice: 2500, minPrice: 2000, defPrice: 2250, manageCoe: 2 },
|
||||
5: { code: 'C9-2-1', name: '二级造价工程师且具备中级工程师资格', maxPrice: 1500, minPrice: 1200, defPrice: 1350, manageCoe: 2.2 },
|
||||
6: { code: 'C9-3-1', name: '一级造价工程师且具备中级工程师资格', maxPrice: 1800, minPrice: 1500, defPrice: 1650, manageCoe: 2.1 },
|
||||
7: { code: 'C9-3-2', name: '一级造价工程师且具备高级工程师资格', maxPrice: 2000, minPrice: 1800, defPrice: 1900, manageCoe: 2.05 },
|
||||
};
|
||||
|
||||
let 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: '' },
|
||||
};
|
||||
|
||||
export const expertList = {
|
||||
0: { ref: 'C9-1-1', name: '技术员及其他', maxPrice: 800, minPrice: 600, defPrice: 700, manageCoe: 2.3 },
|
||||
1: { ref: 'C9-1-2', name: '助理工程师', maxPrice: 1000, minPrice: 800, defPrice: 900, manageCoe: 2.3 },
|
||||
2: { ref: 'C9-1-3', name: '中级工程师或二级造价工程师', maxPrice: 1500, minPrice: 1000, defPrice: 1250, manageCoe: 2.2 },
|
||||
3: { ref: 'C9-1-4', name: '高级工程师或一级造价工程师', maxPrice: 1800, minPrice: 1500, defPrice: 1650, manageCoe: 2.1 },
|
||||
4: { ref: 'C9-1-5', name: '正高级工程师或资深专家', maxPrice: 2500, minPrice: 2000, defPrice: 2250, manageCoe: 2 },
|
||||
5: { ref: 'C9-2-1', name: '二级造价工程师且具备中级工程师资格', maxPrice: 1500, minPrice: 1200, defPrice: 1350, manageCoe: 2.2 },
|
||||
6: { ref: 'C9-3-1', name: '一级造价工程师且具备中级工程师资格', maxPrice: 1800, minPrice: 1500, defPrice: 1650, manageCoe: 2.1 },
|
||||
7: { ref: 'C9-3-2', name: '一级造价工程师且具备高级工程师资格', maxPrice: 2000, minPrice: 1800, defPrice: 1900, manageCoe: 2.05 },
|
||||
};
|
||||
|
||||
export const costScaleCal = [
|
||||
{ ref: 'C1-1', staLine: 0, endLine: 100, basic: { staPrice: 0, rate: 0.01 }, optional: { staPrice: 0, rate: 0.002 } },
|
||||
{ ref: 'C1-2', staLine: 100, endLine: 300, basic: { staPrice: 10000, rate: 0.008 }, optional: { staPrice: 2000, rate: 0.0016 } },
|
||||
{ ref: 'C1-3', staLine: 300, endLine: 500, basic: { staPrice: 26000, rate: 0.005 }, optional: { staPrice: 5200, rate: 0.001 } },
|
||||
{ ref: 'C1-4', staLine: 500, endLine: 1000, basic: { staPrice: 36000, rate: 0.004 }, optional: { staPrice: 7200, rate: 0.0008 } },
|
||||
{ ref: 'C1-5', staLine: 1000, endLine: 5000, basic: { staPrice: 56000, rate: 0.003 }, optional: { staPrice: 11200, rate: 0.0006 } },
|
||||
{ ref: 'C1-6', staLine: 5000, endLine: 10000, basic: { staPrice: 176000, rate: 0.002 }, optional: { staPrice: 35200, rate: 0.0004 } },
|
||||
{ ref: 'C1-7', staLine: 10000, endLine: 30000, basic: { staPrice: 276000, rate: 0.0016 }, optional: { staPrice: 55200, rate: 0.00032 } },
|
||||
{ ref: 'C1-8', staLine: 30000, endLine: 50000, basic: { staPrice: 596000, rate: 0.0013 }, optional: { staPrice: 119200, rate: 0.00026 } },
|
||||
{ ref: 'C1-9', staLine: 50000, endLine: 100000, basic: { staPrice: 856000, rate: 0.001 }, optional: { staPrice: 171200, rate: 0.0002 } },
|
||||
{ ref: 'C1-10', staLine: 100000, endLine: 150000, basic: { staPrice: 1356000, rate: 0.0009 }, optional: { staPrice: 271200, rate: 0.00018 } },
|
||||
{ ref: 'C1-11', staLine: 150000, endLine: 200000, basic: { staPrice: 1806000, rate: 0.0008 }, optional: { staPrice: 361200, rate: 0.00016 } },
|
||||
{ ref: 'C1-12', staLine: 200000, endLine: 300000, basic: { staPrice: 2206000, rate: 0.0007 }, optional: { staPrice: 441200, rate: 0.00014 } },
|
||||
{ ref: 'C1-13', staLine: 300000, endLine: 400000, basic: { staPrice: 2906000, rate: 0.0006 }, optional: { staPrice: 581200, rate: 0.00012 } },
|
||||
{ ref: 'C1-14', staLine: 400000, endLine: 600000, basic: { staPrice: 3506000, rate: 0.0005 }, optional: { staPrice: 701200, rate: 0.0001 } },
|
||||
{ ref: 'C1-15', staLine: 600000, endLine: 800000, basic: { staPrice: 4506000, rate: 0.0004 }, optional: { staPrice: 901200, rate: 0.00008 } },
|
||||
{ ref: 'C1-16', staLine: 800000, endLine: 1000000, basic: { staPrice: 5306000, rate: 0.0003 }, optional: { staPrice: 1061200, rate: 0.00006 } },
|
||||
{ ref: 'C1-17', staLine: 1000000, endLine: null, basic: { staPrice: 5906000, rate: 0.00025 }, optional: { staPrice: 1181200, rate: 0.00005 } },
|
||||
const costScaleCal = [
|
||||
{ code: 'C1-1', staLine: 0, endLine: 100, basic: { staPrice: 0, rate: 0.01 }, optional: { staPrice: 0, rate: 0.002 } },
|
||||
{ code: 'C1-2', staLine: 100, endLine: 300, basic: { staPrice: 10000, rate: 0.008 }, optional: { staPrice: 2000, rate: 0.0016 } },
|
||||
{ code: 'C1-3', staLine: 300, endLine: 500, basic: { staPrice: 26000, rate: 0.005 }, optional: { staPrice: 5200, rate: 0.001 } },
|
||||
{ code: 'C1-4', staLine: 500, endLine: 1000, basic: { staPrice: 36000, rate: 0.004 }, optional: { staPrice: 7200, rate: 0.0008 } },
|
||||
{ code: 'C1-5', staLine: 1000, endLine: 5000, basic: { staPrice: 56000, rate: 0.003 }, optional: { staPrice: 11200, rate: 0.0006 } },
|
||||
{ code: 'C1-6', staLine: 5000, endLine: 10000, basic: { staPrice: 176000, rate: 0.002 }, optional: { staPrice: 35200, rate: 0.0004 } },
|
||||
{ code: 'C1-7', staLine: 10000, endLine: 30000, basic: { staPrice: 276000, rate: 0.0016 }, optional: { staPrice: 55200, rate: 0.00032 } },
|
||||
{ code: 'C1-8', staLine: 30000, endLine: 50000, basic: { staPrice: 596000, rate: 0.0013 }, optional: { staPrice: 119200, rate: 0.00026 } },
|
||||
{ code: 'C1-9', staLine: 50000, endLine: 100000, basic: { staPrice: 856000, rate: 0.001 }, optional: { staPrice: 171200, rate: 0.0002 } },
|
||||
{ code: 'C1-10', staLine: 100000, endLine: 150000, basic: { staPrice: 1356000, rate: 0.0009 }, optional: { staPrice: 271200, rate: 0.00018 } },
|
||||
{ code: 'C1-11', staLine: 150000, endLine: 200000, basic: { staPrice: 1806000, rate: 0.0008 }, optional: { staPrice: 361200, rate: 0.00016 } },
|
||||
{ code: 'C1-12', staLine: 200000, endLine: 300000, basic: { staPrice: 2206000, rate: 0.0007 }, optional: { staPrice: 441200, rate: 0.00014 } },
|
||||
{ code: 'C1-13', staLine: 300000, endLine: 400000, basic: { staPrice: 2906000, rate: 0.0006 }, optional: { staPrice: 581200, rate: 0.00012 } },
|
||||
{ code: 'C1-14', staLine: 400000, endLine: 600000, basic: { staPrice: 3506000, rate: 0.0005 }, optional: { staPrice: 701200, rate: 0.0001 } },
|
||||
{ code: 'C1-15', staLine: 600000, endLine: 800000, basic: { staPrice: 4506000, rate: 0.0004 }, optional: { staPrice: 901200, rate: 0.00008 } },
|
||||
{ code: 'C1-16', staLine: 800000, endLine: 1000000, basic: { staPrice: 5306000, rate: 0.0003 }, optional: { staPrice: 1061200, rate: 0.00006 } },
|
||||
{ code: 'C1-17', staLine: 1000000, endLine: null, basic: { staPrice: 5906000, rate: 0.00025 }, optional: { staPrice: 1181200, rate: 0.00005 } },
|
||||
];
|
||||
|
||||
export const areaScaleCal = [
|
||||
{ ref: 'C2-1', staLine: 0, endLine: 50, basic: { staPrice: 0, rate: 200 }, optional: { staPrice: 0, rate: 40 } },
|
||||
{ ref: 'C2-2', staLine: 50, endLine: 100, basic: { staPrice: 10000, rate: 160 }, optional: { staPrice: 2000, rate: 32 } },
|
||||
{ ref: 'C2-3', staLine: 100, endLine: 500, basic: { staPrice: 18000, rate: 120 }, optional: { staPrice: 3600, rate: 24 } },
|
||||
{ ref: 'C2-4', staLine: 500, endLine: 1000, basic: { staPrice: 66000, rate: 80 }, optional: { staPrice: 13200, rate: 16 } },
|
||||
{ ref: 'C2-5', staLine: 1000, endLine: 5000, basic: { staPrice: 106000, rate: 60 }, optional: { staPrice: 21200, rate: 12 } },
|
||||
{ ref: 'C2-6', staLine: 5000, endLine: null, basic: { staPrice: 346000, rate: 20 }, optional: { staPrice: 69200, rate: 4 } },
|
||||
const areaScaleCal = [
|
||||
{ code: 'C2-1', staLine: 0, endLine: 50, basic: { staPrice: 0, rate: 200 }, optional: { staPrice: 0, rate: 40 } },
|
||||
{ code: 'C2-2', staLine: 50, endLine: 100, basic: { staPrice: 10000, rate: 160 }, optional: { staPrice: 2000, rate: 32 } },
|
||||
{ code: 'C2-3', staLine: 100, endLine: 500, basic: { staPrice: 18000, rate: 120 }, optional: { staPrice: 3600, rate: 24 } },
|
||||
{ code: 'C2-4', staLine: 500, endLine: 1000, basic: { staPrice: 66000, rate: 80 }, optional: { staPrice: 13200, rate: 16 } },
|
||||
{ code: 'C2-5', staLine: 1000, endLine: 5000, basic: { staPrice: 106000, rate: 60 }, optional: { staPrice: 21200, rate: 12 } },
|
||||
{ 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)
|
||||
|
||||
export function getBasicFeeFromScale(scaleValue: unknown, scaleType: 'cost' | 'area' | 'amount') {
|
||||
const sv = Number(scaleValue)
|
||||
if (!Number.isFinite(sv) || sv <= 0) return null
|
||||
|
||||
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))
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
#app {
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
}
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
@ -112,8 +114,9 @@
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
.ag-horizontal-left-spacer {
|
||||
overflow-x: auto
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
@ -131,3 +134,38 @@ overflow-x: auto
|
||||
user-select: text;
|
||||
}
|
||||
}
|
||||
|
||||
.ag-floating-top {
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.xmMx .editable-cell-line .ag-cell-value {
|
||||
display: inline-block;
|
||||
min-width: 84%;
|
||||
padding: 2px 4px;
|
||||
border-bottom: 1px solid #cbd5e1;
|
||||
}
|
||||
|
||||
.xmMx .remark-wrap-cell .ag-cell-value {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
border-bottom: none !important;
|
||||
white-space: normal;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.xmMx .editable-cell-line.ag-cell-focus .ag-cell-value,
|
||||
.xmMx .editable-cell-line:hover .ag-cell-value {
|
||||
border-bottom-color: #2563eb;
|
||||
}
|
||||
|
||||
.xmMx .editable-cell-empty .ag-cell-value {
|
||||
color: #94a3b8 !important;
|
||||
font-style: italic;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.xmMx .ag-cell.editable-cell-empty,
|
||||
.xmMx .ag-cell.editable-cell-empty .ag-cell-value {
|
||||
color: #94a3b8 !important;
|
||||
}
|
||||
|
||||
@ -1 +1 @@
|
||||
{"root":["./src/main.ts","./src/sql.ts","./src/components/ui/button/index.ts","./src/components/ui/card/index.ts","./src/components/ui/scroll-area/index.ts","./src/lib/diyaggridtheme.ts","./src/lib/utils.ts","./src/pinia/tab.ts","./src/app.vue","./src/components/ui/button/button.vue","./src/components/ui/card/card.vue","./src/components/ui/card/cardaction.vue","./src/components/ui/card/cardcontent.vue","./src/components/ui/card/carddescription.vue","./src/components/ui/card/cardfooter.vue","./src/components/ui/card/cardheader.vue","./src/components/ui/card/cardtitle.vue","./src/components/ui/scroll-area/scrollarea.vue","./src/components/ui/scroll-area/scrollbar.vue","./src/components/views/contractdetailview.vue","./src/components/views/ht.vue","./src/components/views/xm.vue","./src/components/views/zxfwview.vue","./src/components/views/htinfo.vue","./src/components/views/xminfo.vue","./src/components/views/zxfw.vue","./src/components/views/pricingview/hourlypricingpane.vue","./src/components/views/pricingview/investmentscalepricingpane.vue","./src/components/views/pricingview/landscalepricingpane.vue","./src/components/views/pricingview/workloadpricingpane.vue","./src/layout/tab.vue","./src/layout/typeline.vue"],"version":"5.9.3"}
|
||||
{"root":["./src/main.ts","./src/sql.ts","./src/components/ui/button/index.ts","./src/components/ui/card/index.ts","./src/components/ui/scroll-area/index.ts","./src/components/ui/tooltip/index.ts","./src/lib/decimal.ts","./src/lib/diyaggridoptions.ts","./src/lib/utils.ts","./src/pinia/pricingpanereload.ts","./src/pinia/tab.ts","./src/app.vue","./src/components/ui/button/button.vue","./src/components/ui/card/card.vue","./src/components/ui/card/cardaction.vue","./src/components/ui/card/cardcontent.vue","./src/components/ui/card/carddescription.vue","./src/components/ui/card/cardfooter.vue","./src/components/ui/card/cardheader.vue","./src/components/ui/card/cardtitle.vue","./src/components/ui/scroll-area/scrollarea.vue","./src/components/ui/scroll-area/scrollbar.vue","./src/components/ui/tooltip/tooltipcontent.vue","./src/components/views/contractdetailview.vue","./src/components/views/ht.vue","./src/components/views/xm.vue","./src/components/views/zxfwview.vue","./src/components/views/htinfo.vue","./src/components/views/xminfo.vue","./src/components/views/zxfw.vue","./src/components/views/pricingview/hourlypricingpane.vue","./src/components/views/pricingview/investmentscalepricingpane.vue","./src/components/views/pricingview/landscalepricingpane.vue","./src/components/views/pricingview/workloadpricingpane.vue","./src/layout/tab.vue","./src/layout/typeline.vue"],"version":"5.9.3"}
|
||||
Loading…
x
Reference in New Issue
Block a user