This commit is contained in:
wintsa 2026-02-26 15:55:36 +08:00
parent 1609f19b9c
commit 37f4a99914
14 changed files with 953 additions and 427 deletions

View File

@ -3,7 +3,7 @@
<TypeLine <TypeLine
scene="ht-tab" scene="ht-tab"
:title="`合同段:${contractName}`" :title="`合同段:${contractName}`"
storage-key="project-active-cat" :storage-key="`project-active-cat-${contractId}`"
default-category="info" default-category="info"
:categories="xmCategories" :categories="xmCategories"
/> />

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue' import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
import draggable from 'vuedraggable' import draggable from 'vuedraggable'
import localforage from 'localforage' import localforage from 'localforage'
import { Card, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardHeader, CardTitle } from '@/components/ui/card'
@ -23,6 +23,10 @@ interface ContractItem {
} }
const STORAGE_KEY = 'ht-card-v1' const STORAGE_KEY = 'ht-card-v1'
const formStore = localforage.createInstance({
name: 'jgjs-pricing-db',
storeName: 'form_state'
})
const tabStore = useTabStore() const tabStore = useTabStore()
@ -79,6 +83,40 @@ const saveContracts = async () => {
} }
} }
const removeForageKeysByContractId = async (store: typeof localforage, contractId: string) => {
try {
const keys = await store.keys()
const targetKeys = keys.filter(key => key.includes(contractId))
await Promise.all(targetKeys.map(key => store.removeItem(key)))
} catch (error) {
console.error('remove forage keys by contract id failed:', contractId, error)
}
}
const removeRelatedTabsByContractId = (contractId: string) => {
const relatedTabIds = tabStore.tabs
.filter(tab => {
const propsContractId = tab?.props?.contractId
return (
tab.id === `contract-${contractId}` ||
tab.id.startsWith(`zxfw-edit-${contractId}-`) ||
propsContractId === contractId
)
})
.map(tab => tab.id)
for (const tabId of relatedTabIds) {
tabStore.removeTab(tabId)
}
}
const cleanupContractRelatedData = async (contractId: string) => {
await Promise.all([
removeForageKeysByContractId(localforage, contractId),
removeForageKeysByContractId(formStore as any, contractId)
])
}
const loadContracts = async () => { const loadContracts = async () => {
try { try {
const saved = await localforage.getItem<ContractItem[]>(STORAGE_KEY) const saved = await localforage.getItem<ContractItem[]>(STORAGE_KEY)
@ -122,6 +160,13 @@ const createContract = async () => {
if (!name) return if (!name) return
if (editingContractId.value) { if (editingContractId.value) {
const current = contracts.value.find(item => item.id === editingContractId.value)
if (!current) return
if (current.name === name) {
closeCreateModal()
return
}
contracts.value = contracts.value.map(item => contracts.value = contracts.value.map(item =>
item.id === editingContractId.value ? { ...item, name } : item item.id === editingContractId.value ? { ...item, name } : item
) )
@ -147,13 +192,28 @@ const createContract = async () => {
} }
const deleteContract = async (id: string) => { const deleteContract = async (id: string) => {
// tab
removeRelatedTabsByContractId(id)
await nextTick()
await cleanupContractRelatedData(id)
//
await new Promise(resolve => setTimeout(resolve, 80))
await cleanupContractRelatedData(id)
contracts.value = contracts.value.filter(item => item.id !== id) contracts.value = contracts.value.filter(item => item.id !== id)
await saveContracts() await saveContracts()
tabStore.removeTab(`contract-${id}`)
notify('删除成功') notify('删除成功')
} }
const handleDragEnd = async () => { const handleDragEnd = async (event: { oldIndex?: number; newIndex?: number }) => {
if (
event.oldIndex == null ||
event.newIndex == null ||
event.oldIndex === event.newIndex
) {
return
}
await saveContracts() await saveContracts()
notify('排序完成') notify('排序完成')
} }

View File

@ -2,7 +2,7 @@
<TypeLine <TypeLine
scene="zxfw-pricing-tab" scene="zxfw-pricing-tab"
title="咨询服务计算" title="咨询服务计算"
storage-key="zxfw-pricing-active-cat" :storage-key="`zxfw-pricing-active-cat-${contractId}-${serviceId}`"
default-category="investment-scale-method" default-category="investment-scale-method"
:categories="pricingCategories" :categories="pricingCategories"
/> />
@ -15,6 +15,7 @@ import TypeLine from '@/layout/typeLine.vue'
const props = defineProps<{ const props = defineProps<{
contractId: string contractId: string
contractName?: string contractName?: string
serviceId: string|number
}>() }>()
interface PricingCategoryItem { interface PricingCategoryItem {
@ -35,7 +36,7 @@ const createPricingPane = (name: string) =>
} }
}) })
return () => h(AsyncPricingView, { contractId: props.contractId }) return () => h(AsyncPricingView, { contractId: props.contractId, serviceId: props.serviceId })
} }
}) })
) )

View File

@ -5,38 +5,11 @@ import type { ColDef, GridOptions } from 'ag-grid-community'
import localforage from 'localforage' import localforage from 'localforage'
import { majorList } from '@/sql' import { majorList } from '@/sql'
import 'ag-grid-enterprise' import 'ag-grid-enterprise'
import { import { myTheme } from '@/lib/diyAgGridTheme'
themeQuartz
} from "ag-grid-community"
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'; import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale';
// 线+线 // 线+线
const borderConfig = {
style: "solid", // 线线 dotted solid
width: 0.5, //
color: "#e5e7eb" //
};
//
const myTheme = themeQuartz.withParams({
//
wrapperBorder: false,
//
headerBackgroundColor: "#f9fafb", // #e7f3fc
headerTextColor: "#374151", //
headerFontSize: 15, //
headerFontWeight: "normal", //
// //
rowBorder: borderConfig,
columnBorder: borderConfig,
headerRowBorder: borderConfig,
//
dataBackgroundColor: "#fefefe"
});
interface DictLeaf { interface DictLeaf {
id: string id: string
code: string code: string
@ -70,6 +43,7 @@ const props = defineProps<{
contractId: string contractId: string
}>() }>()
const DB_KEY = computed(() => `ht-info-v3-${props.contractId}`) const DB_KEY = computed(() => `ht-info-v3-${props.contractId}`)
const XM_DB_KEY = 'xm-info-v3'
const detailRows = ref<DetailRow[]>([]) const detailRows = ref<DetailRow[]>([])
@ -300,6 +274,13 @@ const loadFromIndexedDB = async () => {
return return
} }
// id
const xmData = await localforage.getItem<XmInfoState>(XM_DB_KEY)
if (xmData?.detailRows) {
detailRows.value = mergeWithDictRows(xmData.detailRows)
return
}
detailRows.value = buildDefaultRows() detailRows.value = buildDefaultRows()
} catch (error) { } catch (error) {
console.error('loadFromIndexedDB failed:', error) console.error('loadFromIndexedDB failed:', error)
@ -349,16 +330,16 @@ const processCellFromClipboard = (params:any) => {
</script> </script>
<template> <template>
<div class="space-y-6"> <div class="h-full min-h-0 flex flex-col">
<div class="rounded-lg border bg-card xmMx"> <div class="rounded-lg border bg-card xmMx flex min-h-0 flex-1 flex-col">
<div class="flex items-center justify-between border-b px-4 py-3"> <div class="flex items-center justify-between border-b px-4 py-3">
<h3 class="text-sm font-semibold text-foreground">合同规模明细</h3> <h3 class="text-sm font-semibold text-foreground">合同规模明细</h3>
<div class="text-xs text-muted-foreground">导入导出</div> <div class="text-xs text-muted-foreground">导入导出</div>
</div> </div>
<div class="ag-theme-quartz h-[580px] w-full"> <div class="ag-theme-quartz h-full min-h-0 w-full flex-1">
<AgGridVue <AgGridVue
:style="{ height: '100%' }" :style="{ height: '100%' }"
:rowData="detailRows" :rowData="detailRows"

View File

@ -1,42 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { AgGridVue } from 'ag-grid-vue3' import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef, GridOptions } from 'ag-grid-community' import type { ColDef, ColGroupDef, GridOptions } from 'ag-grid-community'
import localforage from 'localforage' import localforage from 'localforage'
import { majorList } from '@/sql' import { majorList } from '@/sql'
import 'ag-grid-enterprise' import 'ag-grid-enterprise'
import { import { myTheme } from '@/lib/diyAgGridTheme'
themeQuartz
} from "ag-grid-community"
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'; import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale';
// 线+线
const borderConfig = {
style: "solid", // 线线 dotted solid
width: 0.5, //
color: "#e5e7eb" //
};
//
const myTheme = themeQuartz.withParams({
//
wrapperBorder: false,
//
headerBackgroundColor: "#f9fafb", // #e7f3fc
headerTextColor: "#374151", //
headerFontSize: 15, //
headerFontWeight: "normal", //
// //
rowBorder: borderConfig,
columnBorder: borderConfig,
headerRowBorder: borderConfig,
//
dataBackgroundColor: "#fefefe"
});
interface DictLeaf { interface DictLeaf {
id: string id: string
code: string code: string
@ -56,8 +28,13 @@ interface DetailRow {
groupName: string groupName: string
majorCode: string majorCode: string
majorName: string majorName: string
amount: number | null laborBudgetUnitPrice: number | null
landArea: number | null compositeBudgetUnitPrice: number | null
adoptedBudgetUnitPrice: number | null
personnelCount: number | null
workdayCount: number | null
serviceBudget: number | null
remark: string
path: string[] path: string[]
} }
@ -67,9 +44,11 @@ interface XmInfoState {
} }
const props = defineProps<{ const props = defineProps<{
contractId: string contractId: string,
serviceId: string|number
}>() }>()
const DB_KEY = computed(() => `ht-info-v3-${props.contractId}`) const DB_KEY = computed(() => `hourlyPricing-${props.contractId}-${props.serviceId}`)
const detailRows = ref<DetailRow[]>([]) const detailRows = ref<DetailRow[]>([])
@ -140,8 +119,13 @@ const buildDefaultRows = (): DetailRow[] => {
groupName: group.name, groupName: group.name,
majorCode: child.code, majorCode: child.code,
majorName: child.name, majorName: child.name,
amount: null, laborBudgetUnitPrice: null,
landArea: null, compositeBudgetUnitPrice: null,
adoptedBudgetUnitPrice: null,
personnelCount: null,
workdayCount: null,
serviceBudget: null,
remark: '',
path: [group.id, child.id] path: [group.id, child.id]
}) })
} }
@ -161,73 +145,100 @@ const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] =>
return { return {
...row, ...row,
amount: typeof fromDb.amount === 'number' ? fromDb.amount : null, laborBudgetUnitPrice:
landArea: typeof fromDb.landArea === 'number' ? fromDb.landArea : null typeof fromDb.laborBudgetUnitPrice === 'number' ? fromDb.laborBudgetUnitPrice : null,
compositeBudgetUnitPrice:
typeof fromDb.compositeBudgetUnitPrice === 'number' ? fromDb.compositeBudgetUnitPrice : null,
adoptedBudgetUnitPrice:
typeof fromDb.adoptedBudgetUnitPrice === 'number' ? fromDb.adoptedBudgetUnitPrice : null,
personnelCount: typeof fromDb.personnelCount === 'number' ? fromDb.personnelCount : null,
workdayCount: typeof fromDb.workdayCount === 'number' ? fromDb.workdayCount : null,
serviceBudget: typeof fromDb.serviceBudget === 'number' ? fromDb.serviceBudget : null,
remark: typeof fromDb.remark === 'string' ? fromDb.remark : ''
} }
}) })
} }
const columnDefs: ColDef<DetailRow>[] = [ const parseNumberOrNull = (value: unknown) => {
if (value === '' || value == null) return null
const v = Number(value)
return Number.isFinite(v) ? v : null
}
{ const formatEditableNumber = (params: any) => {
headerName: '造价金额(万元)', if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
field: 'amount', return '点击输入'
minWidth: 170, }
flex: 1, // if (params.value == null) return ''
return Number(params.value).toFixed(2)
}
const editableNumberCol = <K extends keyof DetailRow>(
field: K,
headerName: string,
extra: Partial<ColDef<DetailRow>> = {}
): ColDef<DetailRow> => ({
headerName,
field,
minWidth: 150,
flex: 1,
editable: params => !params.node?.group && !params.node?.rowPinned, editable: params => !params.node?.group && !params.node?.rowPinned,
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''), cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
cellClassRules: { cellClassRules: {
'editable-cell-empty': params => 'editable-cell-empty': params =>
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '') !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
}, },
aggFunc: 'sum', valueParser: params => parseNumberOrNull(params.newValue),
valueParser: params => { valueFormatter: formatEditableNumber,
if (params.newValue === '' || params.newValue == null) return null ...extra
const v = Number(params.newValue) })
return Number.isFinite(v) ? v : null
}, const columnDefs: (ColDef<DetailRow> | ColGroupDef<DetailRow>)[] = [
valueFormatter: params => { {
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) { headerName: '人员名称',
return '点击输入' minWidth: 200,
} width: 220,
if (params.value == null) return '' pinned: 'left',
return Number(params.value).toFixed(2) valueGetter: params => {
if (params.node?.rowPinned) return ''
return params.node?.group ? params.data?.groupName || '' : params.data?.majorName || ''
} }
}, },
{ {
headerName: '用地面积(亩)', headerName: '预算参考单价',
field: 'landArea', marryChildren: true,
minWidth: 170, children: [
flex: 1, // editableNumberCol('laborBudgetUnitPrice', '人工预算单价(元/工日)'),
editableNumberCol('compositeBudgetUnitPrice', '综合预算单价(元/工日)')
]
},
editableNumberCol('adoptedBudgetUnitPrice', '预算采用单价(元/工日)'),
editableNumberCol('personnelCount', '人员数量(人)', { aggFunc: 'sum' }),
editableNumberCol('workdayCount', '工日数量(工日)', { aggFunc: 'sum' }),
editableNumberCol('serviceBudget', '服务预算(元)', { aggFunc: 'sum' }),
{
headerName: '说明',
field: 'remark',
minWidth: 180,
flex: 1.2,
editable: params => !params.node?.group && !params.node?.rowPinned, 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' : ''),
cellClassRules: { cellClassRules: {
'editable-cell-empty': params => 'editable-cell-empty': params =>
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '') !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
},
aggFunc: 'sum',
valueParser: params => {
if (params.newValue === '' || params.newValue == null) return null
const v = Number(params.newValue)
return Number.isFinite(v) ? v : null
},
valueFormatter: params => {
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
return '点击输入'
}
if (params.value == null) return ''
return Number(params.value).toFixed(2)
} }
} }
] ]
const autoGroupColumnDef: ColDef = { const autoGroupColumnDef: ColDef = {
headerName: '专业编码以及工程专业名称', headerName: '编码',
minWidth: 320, minWidth: 150,
pinned: 'left', pinned: 'left',
flex:2, // width: 160,
cellRendererParams: { cellRendererParams: {
suppressCount: true suppressCount: true
@ -237,7 +248,8 @@ const autoGroupColumnDef: ColDef = {
return '总合计' return '总合计'
} }
const nodeId = String(params.value || '') const nodeId = String(params.value || '')
return idLabelMap.get(nodeId) || nodeId const label = idLabelMap.get(nodeId) || nodeId
return label.includes(' ') ? label.split(' ')[0] : label
} }
} }
@ -258,12 +270,16 @@ const gridOptions: GridOptions<DetailRow> = {
} }
} }
const totalAmount = computed(() => const totalPersonnelCount = computed(() =>
detailRows.value.reduce((sum, row) => sum + (row.amount || 0), 0) detailRows.value.reduce((sum, row) => sum + (row.personnelCount || 0), 0)
) )
const totalLandArea = computed(() => const totalWorkdayCount = computed(() =>
detailRows.value.reduce((sum, row) => sum + (row.landArea || 0), 0) detailRows.value.reduce((sum, row) => sum + (row.workdayCount || 0), 0)
)
const totalServiceBudget = computed(() =>
detailRows.value.reduce((sum, row) => sum + (row.serviceBudget || 0), 0)
) )
const pinnedTopRowData = computed(() => [ const pinnedTopRowData = computed(() => [
{ {
@ -272,8 +288,13 @@ const pinnedTopRowData = computed(() => [
groupName: '', groupName: '',
majorCode: '', majorCode: '',
majorName: '', majorName: '',
amount: totalAmount.value, laborBudgetUnitPrice: null,
landArea: totalLandArea.value, compositeBudgetUnitPrice: null,
adoptedBudgetUnitPrice: null,
personnelCount: totalPersonnelCount.value,
workdayCount: totalWorkdayCount.value,
serviceBudget: totalServiceBudget.value,
remark: '',
path: ['TOTAL'] path: ['TOTAL']
} }
]) ])
@ -349,16 +370,16 @@ const processCellFromClipboard = (params:any) => {
</script> </script>
<template> <template>
<div class="space-y-6"> <div class="h-full min-h-0 flex flex-col">
<div class="rounded-lg border bg-card xmMx"> <div class="rounded-lg border bg-card xmMx flex min-h-0 flex-1 flex-col">
<div class="flex items-center justify-between border-b px-4 py-3"> <div class="flex items-center justify-between border-b px-4 py-3">
<h3 class="text-sm font-semibold text-foreground">合同规模明细</h3> <h3 class="text-sm font-semibold text-foreground">工时法明细</h3>
<div class="text-xs text-muted-foreground">导入导出</div> <div class="text-xs text-muted-foreground">导入导出</div>
</div> </div>
<div class="ag-theme-quartz h-[580px] w-full"> <div class="ag-theme-quartz h-full min-h-0 w-full flex-1">
<AgGridVue <AgGridVue
:style="{ height: '100%' }" :style="{ height: '100%' }"
:rowData="detailRows" :rowData="detailRows"

View File

@ -5,38 +5,11 @@ import type { ColDef, GridOptions } from 'ag-grid-community'
import localforage from 'localforage' import localforage from 'localforage'
import { majorList } from '@/sql' import { majorList } from '@/sql'
import 'ag-grid-enterprise' import 'ag-grid-enterprise'
import { import { myTheme } from '@/lib/diyAgGridTheme'
themeQuartz
} from "ag-grid-community"
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'; import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale';
// 线+线 // 线+线
const borderConfig = {
style: "solid", // 线线 dotted solid
width: 0.5, //
color: "#e5e7eb" //
};
//
const myTheme = themeQuartz.withParams({
//
wrapperBorder: false,
//
headerBackgroundColor: "#f9fafb", // #e7f3fc
headerTextColor: "#374151", //
headerFontSize: 15, //
headerFontWeight: "normal", //
// //
rowBorder: borderConfig,
columnBorder: borderConfig,
headerRowBorder: borderConfig,
//
dataBackgroundColor: "#fefefe"
});
interface DictLeaf { interface DictLeaf {
id: string id: string
code: string code: string
@ -57,7 +30,11 @@ interface DetailRow {
majorCode: string majorCode: string
majorName: string majorName: string
amount: number | null amount: number | null
landArea: number | null benchmarkBudget: number | null
consultCategoryFactor: number | null
majorFactor: number | null
budgetFee: number | null
remark: string
path: string[] path: string[]
} }
@ -67,9 +44,10 @@ interface XmInfoState {
} }
const props = defineProps<{ const props = defineProps<{
contractId: string contractId: string,
serviceId: string|number
}>() }>()
const DB_KEY = computed(() => `ht-info-v3-${props.contractId}`) const DB_KEY = computed(() => `tzGMF-${props.contractId}-${props.serviceId}`)
const detailRows = ref<DetailRow[]>([]) const detailRows = ref<DetailRow[]>([])
@ -141,7 +119,11 @@ const buildDefaultRows = (): DetailRow[] => {
majorCode: child.code, majorCode: child.code,
majorName: child.name, majorName: child.name,
amount: null, amount: null,
landArea: null, benchmarkBudget: null,
consultCategoryFactor: null,
majorFactor: null,
budgetFee: null,
remark: '',
path: [group.id, child.id] path: [group.id, child.id]
}) })
} }
@ -162,18 +144,46 @@ const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] =>
return { return {
...row, ...row,
amount: typeof fromDb.amount === 'number' ? fromDb.amount : null, amount: typeof fromDb.amount === 'number' ? fromDb.amount : null,
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,
budgetFee: typeof fromDb.budgetFee === 'number' ? fromDb.budgetFee : null,
remark: typeof fromDb.remark === 'string' ? fromDb.remark : ''
} }
}) })
} }
const columnDefs: ColDef<DetailRow>[] = [ const parseNumberOrNull = (value: unknown) => {
if (value === '' || value == null) return null
const v = Number(value)
return Number.isFinite(v) ? v : null
}
const formatEditableNumber = (params: any) => {
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
return '点击输入'
}
if (params.value == null) return ''
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 || ''
}
},
{ {
headerName: '造价金额(万元)', headerName: '造价金额(万元)',
field: 'amount', field: 'amount',
minWidth: 170, minWidth: 170,
flex: 1, // flex: 1,
editable: params => !params.node?.group && !params.node?.rowPinned, editable: params => !params.node?.group && !params.node?.rowPinned,
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''), cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
@ -182,24 +192,14 @@ const columnDefs: ColDef<DetailRow>[] = [
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '') !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
}, },
aggFunc: 'sum', aggFunc: 'sum',
valueParser: params => { valueParser: params => parseNumberOrNull(params.newValue),
if (params.newValue === '' || params.newValue == null) return null valueFormatter: formatEditableNumber
const v = Number(params.newValue)
return Number.isFinite(v) ? v : null
},
valueFormatter: params => {
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
return '点击输入'
}
if (params.value == null) return ''
return Number(params.value).toFixed(2)
}
}, },
{ {
headerName: '用地面积(亩)', headerName: '基准预算(元)',
field: 'landArea', field: 'benchmarkBudget',
minWidth: 170, minWidth: 170,
flex: 1, // flex: 1,
editable: params => !params.node?.group && !params.node?.rowPinned, editable: params => !params.node?.group && !params.node?.rowPinned,
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''), cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
@ -208,26 +208,75 @@ const columnDefs: ColDef<DetailRow>[] = [
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '') !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
}, },
aggFunc: 'sum', aggFunc: 'sum',
valueParser: params => { valueParser: params => parseNumberOrNull(params.newValue),
if (params.newValue === '' || params.newValue == null) return null valueFormatter: formatEditableNumber
const v = Number(params.newValue)
return Number.isFinite(v) ? v : null
}, },
{
headerName: '咨询分类系数',
field: 'consultCategoryFactor',
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 === '')
},
valueParser: params => parseNumberOrNull(params.newValue),
valueFormatter: formatEditableNumber
},
{
headerName: '专业系数',
field: 'majorFactor',
minWidth: 130,
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: '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',
valueParser: params => parseNumberOrNull(params.newValue),
valueFormatter: formatEditableNumber
},
{
headerName: '说明',
field: 'remark',
minWidth: 180,
flex: 1.2,
editable: params => !params.node?.group && !params.node?.rowPinned,
valueFormatter: params => { valueFormatter: params => {
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) { if (!params.node?.group && !params.node?.rowPinned && !params.value) return '点击输入'
return '点击输入' return params.value || ''
} },
if (params.value == null) return '' cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
return Number(params.value).toFixed(2) cellClassRules: {
'editable-cell-empty': params =>
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
} }
} }
] ]
const autoGroupColumnDef: ColDef = { const autoGroupColumnDef: ColDef = {
headerName: '专业编码以及工程专业名称', headerName: '专业编码',
minWidth: 320, minWidth: 160,
pinned: 'left', pinned: 'left',
flex:2, // width: 170,
cellRendererParams: { cellRendererParams: {
suppressCount: true suppressCount: true
@ -237,7 +286,8 @@ const autoGroupColumnDef: ColDef = {
return '总合计' return '总合计'
} }
const nodeId = String(params.value || '') const nodeId = String(params.value || '')
return idLabelMap.get(nodeId) || nodeId const label = idLabelMap.get(nodeId) || nodeId
return label.includes(' ') ? label.split(' ')[0] : label
} }
} }
@ -262,8 +312,12 @@ const totalAmount = computed(() =>
detailRows.value.reduce((sum, row) => sum + (row.amount || 0), 0) detailRows.value.reduce((sum, row) => sum + (row.amount || 0), 0)
) )
const totalLandArea = computed(() => const totalBenchmarkBudget = computed(() =>
detailRows.value.reduce((sum, row) => sum + (row.landArea || 0), 0) detailRows.value.reduce((sum, row) => sum + (row.benchmarkBudget || 0), 0)
)
const totalBudgetFee = computed(() =>
detailRows.value.reduce((sum, row) => sum + (row.budgetFee || 0), 0)
) )
const pinnedTopRowData = computed(() => [ const pinnedTopRowData = computed(() => [
{ {
@ -273,7 +327,11 @@ const pinnedTopRowData = computed(() => [
majorCode: '', majorCode: '',
majorName: '', majorName: '',
amount: totalAmount.value, amount: totalAmount.value,
landArea: totalLandArea.value, benchmarkBudget: totalBenchmarkBudget.value,
consultCategoryFactor: null,
majorFactor: null,
budgetFee: totalBudgetFee.value,
remark: '',
path: ['TOTAL'] path: ['TOTAL']
} }
]) ])
@ -349,16 +407,16 @@ const processCellFromClipboard = (params:any) => {
</script> </script>
<template> <template>
<div class="space-y-6"> <div class="h-full min-h-0 flex flex-col">
<div class="rounded-lg border bg-card xmMx"> <div class="rounded-lg border bg-card xmMx flex min-h-0 flex-1 flex-col">
<div class="flex items-center justify-between border-b px-4 py-3"> <div class="flex items-center justify-between border-b px-4 py-3">
<h3 class="text-sm font-semibold text-foreground">合同规模明细</h3> <h3 class="text-sm font-semibold text-foreground">投资规模明细</h3>
<div class="text-xs text-muted-foreground">导入导出</div> <div class="text-xs text-muted-foreground">导入导出</div>
</div> </div>
<div class="ag-theme-quartz h-[580px] w-full"> <div class="ag-theme-quartz h-full min-h-0 w-full flex-1">
<AgGridVue <AgGridVue
:style="{ height: '100%' }" :style="{ height: '100%' }"
:rowData="detailRows" :rowData="detailRows"

View File

@ -5,38 +5,11 @@ import type { ColDef, GridOptions } from 'ag-grid-community'
import localforage from 'localforage' import localforage from 'localforage'
import { majorList } from '@/sql' import { majorList } from '@/sql'
import 'ag-grid-enterprise' import 'ag-grid-enterprise'
import { import { myTheme } from '@/lib/diyAgGridTheme'
themeQuartz
} from "ag-grid-community"
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'; import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale';
// 线+线 // 线+线
const borderConfig = {
style: "solid", // 线线 dotted solid
width: 0.5, //
color: "#e5e7eb" //
};
//
const myTheme = themeQuartz.withParams({
//
wrapperBorder: false,
//
headerBackgroundColor: "#f9fafb", // #e7f3fc
headerTextColor: "#374151", //
headerFontSize: 15, //
headerFontWeight: "normal", //
// //
rowBorder: borderConfig,
columnBorder: borderConfig,
headerRowBorder: borderConfig,
//
dataBackgroundColor: "#fefefe"
});
interface DictLeaf { interface DictLeaf {
id: string id: string
code: string code: string
@ -58,6 +31,11 @@ interface DetailRow {
majorName: string majorName: string
amount: number | null amount: number | null
landArea: number | null landArea: number | null
benchmarkBudget: number | null
consultCategoryFactor: number | null
majorFactor: number | null
budgetFee: number | null
remark: string
path: string[] path: string[]
} }
@ -67,9 +45,10 @@ interface XmInfoState {
} }
const props = defineProps<{ const props = defineProps<{
contractId: string contractId: string,
serviceId: string|number
}>() }>()
const DB_KEY = computed(() => `ht-info-v3-${props.contractId}`) const DB_KEY = computed(() => `ydGMF-${props.contractId}-${props.serviceId}`)
const detailRows = ref<DetailRow[]>([]) const detailRows = ref<DetailRow[]>([])
@ -142,6 +121,11 @@ const buildDefaultRows = (): DetailRow[] => {
majorName: child.name, majorName: child.name,
amount: null, amount: null,
landArea: null, landArea: null,
benchmarkBudget: null,
consultCategoryFactor: null,
majorFactor: null,
budgetFee: null,
remark: '',
path: [group.id, child.id] path: [group.id, child.id]
}) })
} }
@ -162,44 +146,47 @@ const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] =>
return { return {
...row, ...row,
amount: typeof fromDb.amount === 'number' ? fromDb.amount : null, amount: typeof fromDb.amount === 'number' ? fromDb.amount : null,
landArea: typeof fromDb.landArea === 'number' ? fromDb.landArea : null 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,
budgetFee: typeof fromDb.budgetFee === 'number' ? fromDb.budgetFee : null,
remark: typeof fromDb.remark === 'string' ? fromDb.remark : ''
} }
}) })
} }
const columnDefs: ColDef<DetailRow>[] = [ const parseNumberOrNull = (value: unknown) => {
if (value === '' || value == null) return null
{ const v = Number(value)
headerName: '造价金额(万元)',
field: 'amount',
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 => {
if (params.newValue === '' || params.newValue == null) return null
const v = Number(params.newValue)
return Number.isFinite(v) ? v : null return Number.isFinite(v) ? v : null
}, }
valueFormatter: params => {
const formatEditableNumber = (params: any) => {
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) { if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
return '点击输入' return '点击输入'
} }
if (params.value == null) return '' if (params.value == null) return ''
return Number(params.value).toFixed(2) 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 || ''
} }
}, },
{ {
headerName: '用地面积(亩)', headerName: '用地面积(亩)',
field: 'landArea', field: 'landArea',
minWidth: 170, minWidth: 170,
flex: 1, // flex: 1,
editable: params => !params.node?.group && !params.node?.rowPinned, editable: params => !params.node?.group && !params.node?.rowPinned,
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''), cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
@ -208,26 +195,90 @@ const columnDefs: ColDef<DetailRow>[] = [
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '') !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
}, },
aggFunc: 'sum', aggFunc: 'sum',
valueParser: params => { valueParser: params => parseNumberOrNull(params.newValue),
if (params.newValue === '' || params.newValue == null) return null valueFormatter: formatEditableNumber
const v = Number(params.newValue)
return Number.isFinite(v) ? v : null
}, },
{
headerName: '基准预算(元)',
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',
valueParser: params => parseNumberOrNull(params.newValue),
valueFormatter: formatEditableNumber
},
{
headerName: '咨询分类系数',
field: 'consultCategoryFactor',
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 === '')
},
valueParser: params => parseNumberOrNull(params.newValue),
valueFormatter: formatEditableNumber
},
{
headerName: '专业系数',
field: 'majorFactor',
minWidth: 130,
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: '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',
valueParser: params => parseNumberOrNull(params.newValue),
valueFormatter: formatEditableNumber
},
{
headerName: '说明',
field: 'remark',
minWidth: 180,
flex: 1.2,
editable: params => !params.node?.group && !params.node?.rowPinned,
valueFormatter: params => { valueFormatter: params => {
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) { if (!params.node?.group && !params.node?.rowPinned && !params.value) return '点击输入'
return '点击输入' return params.value || ''
} },
if (params.value == null) return '' cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
return Number(params.value).toFixed(2) cellClassRules: {
'editable-cell-empty': params =>
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
} }
} }
] ]
const autoGroupColumnDef: ColDef = { const autoGroupColumnDef: ColDef = {
headerName: '专业编码以及工程专业名称', headerName: '专业编码',
minWidth: 320, minWidth: 160,
pinned: 'left', pinned: 'left',
flex:2, // width: 170,
cellRendererParams: { cellRendererParams: {
suppressCount: true suppressCount: true
@ -237,7 +288,8 @@ const autoGroupColumnDef: ColDef = {
return '总合计' return '总合计'
} }
const nodeId = String(params.value || '') const nodeId = String(params.value || '')
return idLabelMap.get(nodeId) || nodeId const label = idLabelMap.get(nodeId) || nodeId
return label.includes(' ') ? label.split(' ')[0] : label
} }
} }
@ -265,6 +317,14 @@ const totalAmount = computed(() =>
const totalLandArea = computed(() => const totalLandArea = computed(() =>
detailRows.value.reduce((sum, row) => sum + (row.landArea || 0), 0) detailRows.value.reduce((sum, row) => sum + (row.landArea || 0), 0)
) )
const totalBenchmarkBudget = computed(() =>
detailRows.value.reduce((sum, row) => sum + (row.benchmarkBudget || 0), 0)
)
const totalBudgetFee = computed(() =>
detailRows.value.reduce((sum, row) => sum + (row.budgetFee || 0), 0)
)
const pinnedTopRowData = computed(() => [ const pinnedTopRowData = computed(() => [
{ {
id: 'pinned-total-row', id: 'pinned-total-row',
@ -274,6 +334,11 @@ const pinnedTopRowData = computed(() => [
majorName: '', majorName: '',
amount: totalAmount.value, amount: totalAmount.value,
landArea: totalLandArea.value, landArea: totalLandArea.value,
benchmarkBudget: totalBenchmarkBudget.value,
consultCategoryFactor: null,
majorFactor: null,
budgetFee: totalBudgetFee.value,
remark: '',
path: ['TOTAL'] path: ['TOTAL']
} }
]) ])
@ -349,16 +414,16 @@ const processCellFromClipboard = (params:any) => {
</script> </script>
<template> <template>
<div class="space-y-6"> <div class="h-full min-h-0 flex flex-col">
<div class="rounded-lg border bg-card xmMx"> <div class="rounded-lg border bg-card xmMx flex min-h-0 flex-1 flex-col">
<div class="flex items-center justify-between border-b px-4 py-3"> <div class="flex items-center justify-between border-b px-4 py-3">
<h3 class="text-sm font-semibold text-foreground">合同规模明细</h3> <h3 class="text-sm font-semibold text-foreground">用地规模明细</h3>
<div class="text-xs text-muted-foreground">导入导出</div> <div class="text-xs text-muted-foreground">导入导出</div>
</div> </div>
<div class="ag-theme-quartz h-[580px] w-full"> <div class="ag-theme-quartz h-full min-h-0 w-full flex-1">
<AgGridVue <AgGridVue
:style="{ height: '100%' }" :style="{ height: '100%' }"
:rowData="detailRows" :rowData="detailRows"

View File

@ -5,38 +5,10 @@ import type { ColDef, GridOptions } from 'ag-grid-community'
import localforage from 'localforage' import localforage from 'localforage'
import { majorList } from '@/sql' import { majorList } from '@/sql'
import 'ag-grid-enterprise' import 'ag-grid-enterprise'
import { import { myTheme } from '@/lib/diyAgGridTheme'
themeQuartz
} from "ag-grid-community"
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'; import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale';
// 线+线
const borderConfig = {
style: "solid", // 线线 dotted solid
width: 0.5, //
color: "#e5e7eb" //
};
//
const myTheme = themeQuartz.withParams({
//
wrapperBorder: false,
//
headerBackgroundColor: "#f9fafb", // #e7f3fc
headerTextColor: "#374151", //
headerFontSize: 15, //
headerFontWeight: "normal", //
// //
rowBorder: borderConfig,
columnBorder: borderConfig,
headerRowBorder: borderConfig,
//
dataBackgroundColor: "#fefefe"
});
interface DictLeaf { interface DictLeaf {
id: string id: string
code: string code: string
@ -56,8 +28,13 @@ interface DetailRow {
groupName: string groupName: string
majorCode: string majorCode: string
majorName: string majorName: string
amount: number | null workload: number | null
landArea: number | null budgetBase: number | null
budgetReferenceUnitPrice: number | null
budgetAdoptedUnitPrice: number | null
consultCategoryFactor: number | null
serviceFee: number | null
remark: string
path: string[] path: string[]
} }
@ -67,9 +44,11 @@ interface XmInfoState {
} }
const props = defineProps<{ const props = defineProps<{
contractId: string contractId: string,
serviceId: string|number
}>() }>()
const DB_KEY = computed(() => `ht-info-v3-${props.contractId}`) const DB_KEY = computed(() => `gzlF-${props.contractId}-${props.serviceId}`)
const detailRows = ref<DetailRow[]>([]) const detailRows = ref<DetailRow[]>([])
@ -140,8 +119,13 @@ const buildDefaultRows = (): DetailRow[] => {
groupName: group.name, groupName: group.name,
majorCode: child.code, majorCode: child.code,
majorName: child.name, majorName: child.name,
amount: null, workload: null,
landArea: null, budgetBase: null,
budgetReferenceUnitPrice: null,
budgetAdoptedUnitPrice: null,
consultCategoryFactor: null,
serviceFee: null,
remark: '',
path: [group.id, child.id] path: [group.id, child.id]
}) })
} }
@ -161,19 +145,65 @@ const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] =>
return { return {
...row, ...row,
amount: typeof fromDb.amount === 'number' ? fromDb.amount : null, workload: typeof fromDb.workload === 'number' ? fromDb.workload : null,
landArea: typeof fromDb.landArea === 'number' ? fromDb.landArea : 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:
typeof fromDb.consultCategoryFactor === 'number' ? fromDb.consultCategoryFactor : null,
serviceFee: typeof fromDb.serviceFee === 'number' ? fromDb.serviceFee : null,
remark: typeof fromDb.remark === 'string' ? fromDb.remark : ''
} }
}) })
} }
const parseNumberOrNull = (value: unknown) => {
if (value === '' || value == null) return null
const v = Number(value)
return Number.isFinite(v) ? v : null
}
const formatEditableNumber = (params: any) => {
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
return '点击输入'
}
if (params.value == null) return ''
return Number(params.value).toFixed(2)
}
const columnDefs: ColDef<DetailRow>[] = [ const columnDefs: ColDef<DetailRow>[] = [
{ {
headerName: '造价金额(万元)', headerName: '工作内容',
field: 'amount', 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: 'workload',
minWidth: 140,
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: 'budgetBase',
minWidth: 170, minWidth: 170,
flex: 1, // flex: 1,
editable: params => !params.node?.group && !params.node?.rowPinned, editable: params => !params.node?.group && !params.node?.rowPinned,
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''), cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
@ -182,25 +212,56 @@ const columnDefs: ColDef<DetailRow>[] = [
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '') !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
}, },
aggFunc: 'sum', aggFunc: 'sum',
valueParser: params => { valueParser: params => parseNumberOrNull(params.newValue),
if (params.newValue === '' || params.newValue == null) return null valueFormatter: formatEditableNumber
const v = Number(params.newValue)
return Number.isFinite(v) ? v : null
},
valueFormatter: params => {
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
return '点击输入'
}
if (params.value == null) return ''
return Number(params.value).toFixed(2)
}
}, },
{ {
headerName: '用地面积(亩)', headerName: '预算参考单价',
field: 'landArea', field: 'budgetReferenceUnitPrice',
minWidth: 170, minWidth: 170,
flex: 1, // 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 === '')
},
valueParser: params => parseNumberOrNull(params.newValue),
valueFormatter: formatEditableNumber
},
{
headerName: '咨询分类系数',
field: 'consultCategoryFactor',
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 === '')
},
valueParser: params => parseNumberOrNull(params.newValue),
valueFormatter: formatEditableNumber
},
{
headerName: '服务费用(元)',
field: 'serviceFee',
minWidth: 150,
flex: 1,
editable: params => !params.node?.group && !params.node?.rowPinned, editable: params => !params.node?.group && !params.node?.rowPinned,
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''), cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
cellClassRules: { cellClassRules: {
@ -208,26 +269,32 @@ const columnDefs: ColDef<DetailRow>[] = [
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '') !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
}, },
aggFunc: 'sum', aggFunc: 'sum',
valueParser: params => { valueParser: params => parseNumberOrNull(params.newValue),
if (params.newValue === '' || params.newValue == null) return null valueFormatter: formatEditableNumber
const v = Number(params.newValue)
return Number.isFinite(v) ? v : null
}, },
{
headerName: '说明',
field: 'remark',
minWidth: 180,
flex: 1.2,
editable: params => !params.node?.group && !params.node?.rowPinned,
valueFormatter: params => { valueFormatter: params => {
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) { if (!params.node?.group && !params.node?.rowPinned && !params.value) return '点击输入'
return '点击输入' return params.value || ''
} },
if (params.value == null) return '' cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
return Number(params.value).toFixed(2) cellClassRules: {
'editable-cell-empty': params =>
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
} }
} }
] ]
const autoGroupColumnDef: ColDef = { const autoGroupColumnDef: ColDef = {
headerName: '专业编码以及工程专业名称', headerName: '编码',
minWidth: 320, minWidth: 160,
pinned: 'left', pinned: 'left',
flex:2, // width: 170,
cellRendererParams: { cellRendererParams: {
suppressCount: true suppressCount: true
@ -237,7 +304,8 @@ const autoGroupColumnDef: ColDef = {
return '总合计' return '总合计'
} }
const nodeId = String(params.value || '') const nodeId = String(params.value || '')
return idLabelMap.get(nodeId) || nodeId const label = idLabelMap.get(nodeId) || nodeId
return label.includes(' ') ? label.split(' ')[0] : label
} }
} }
@ -258,12 +326,16 @@ const gridOptions: GridOptions<DetailRow> = {
} }
} }
const totalAmount = computed(() => const totalBudgetBase = computed(() =>
detailRows.value.reduce((sum, row) => sum + (row.amount || 0), 0) detailRows.value.reduce((sum, row) => sum + (row.budgetBase || 0), 0)
) )
const totalLandArea = computed(() => const totalWorkload = computed(() =>
detailRows.value.reduce((sum, row) => sum + (row.landArea || 0), 0) detailRows.value.reduce((sum, row) => sum + (row.workload || 0), 0)
)
const totalServiceFee = computed(() =>
detailRows.value.reduce((sum, row) => sum + (row.serviceFee || 0), 0)
) )
const pinnedTopRowData = computed(() => [ const pinnedTopRowData = computed(() => [
{ {
@ -272,8 +344,13 @@ const pinnedTopRowData = computed(() => [
groupName: '', groupName: '',
majorCode: '', majorCode: '',
majorName: '', majorName: '',
amount: totalAmount.value, workload: totalWorkload.value,
landArea: totalLandArea.value, budgetBase: totalBudgetBase.value,
budgetReferenceUnitPrice: null,
budgetAdoptedUnitPrice: null,
consultCategoryFactor: null,
serviceFee: totalServiceFee.value,
remark: '',
path: ['TOTAL'] path: ['TOTAL']
} }
]) ])
@ -349,16 +426,16 @@ const processCellFromClipboard = (params:any) => {
</script> </script>
<template> <template>
<div class="space-y-6"> <div class="h-full min-h-0 flex flex-col">
<div class="rounded-lg border bg-card xmMx"> <div class="rounded-lg border bg-card xmMx flex min-h-0 flex-1 flex-col">
<div class="flex items-center justify-between border-b px-4 py-3"> <div class="flex items-center justify-between border-b px-4 py-3">
<h3 class="text-sm font-semibold text-foreground">合同规模明细</h3> <h3 class="text-sm font-semibold text-foreground">工作流规模</h3>
<div class="text-xs text-muted-foreground">导入导出</div> <div class="text-xs text-muted-foreground">导入导出</div>
</div> </div>
<div class="ag-theme-quartz h-[580px] w-full"> <div class="ag-theme-quartz h-full min-h-0 w-full flex-1">
<AgGridVue <AgGridVue
:style="{ height: '100%' }" :style="{ height: '100%' }"
:rowData="detailRows" :rowData="detailRows"

View File

@ -5,38 +5,12 @@ import type { ColDef, GridOptions } from 'ag-grid-community'
import localforage from 'localforage' import localforage from 'localforage'
import { majorList } from '@/sql' import { majorList } from '@/sql'
import 'ag-grid-enterprise' import 'ag-grid-enterprise'
import { import { myTheme } from '@/lib/diyAgGridTheme'
themeQuartz
} from "ag-grid-community"
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'; import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale';
// 线+线 // 线+线
const borderConfig = {
style: "solid", // 线线 dotted solid
width: 0.5, //
color: "#e5e7eb" //
};
//
const myTheme = themeQuartz.withParams({
//
wrapperBorder: false,
//
headerBackgroundColor: "#f9fafb", // #e7f3fc
headerTextColor: "#374151", //
headerFontSize: 15, //
headerFontWeight: "normal", //
// //
rowBorder: borderConfig,
columnBorder: borderConfig,
headerRowBorder: borderConfig,
//
dataBackgroundColor: "#fefefe"
});
interface DictLeaf { interface DictLeaf {
id: string id: string
code: string code: string
@ -71,6 +45,73 @@ const DEFAULT_PROJECT_NAME = 'xxx造价咨询服务'
const projectName = ref(DEFAULT_PROJECT_NAME) const projectName = ref(DEFAULT_PROJECT_NAME)
const detailRows = ref<DetailRow[]>([]) const detailRows = ref<DetailRow[]>([])
const rootRef = ref<HTMLElement | null>(null)
const gridSectionRef = ref<HTMLElement | null>(null)
const agGridRef = ref<HTMLElement | null>(null)
const agGridHeight = ref(580)
let snapScrollHost: HTMLElement | null = null
let snapTimer: ReturnType<typeof setTimeout> | null = null
let snapLockTimer: ReturnType<typeof setTimeout> | null = null
let isSnapping = false
let hostResizeObserver: ResizeObserver | null = null
const updateGridCardHeight = () => {
if (!snapScrollHost || !rootRef.value) return
const contentWrap = rootRef.value.parentElement
const style = contentWrap ? window.getComputedStyle(contentWrap) : null
const paddingTop = style ? Number.parseFloat(style.paddingTop || '0') || 0 : 0
const paddingBottom = style ? Number.parseFloat(style.paddingBottom || '0') || 0 : 0
const nextHeight = Math.max(360, Math.floor(snapScrollHost.clientHeight - paddingTop - paddingBottom))
agGridHeight.value = nextHeight
}
const bindSnapScrollHost = () => {
snapScrollHost = rootRef.value?.closest('[data-slot="scroll-area-viewport"]') as HTMLElement | null
if (!snapScrollHost) return
snapScrollHost.addEventListener('scroll', handleSnapHostScroll, { passive: true })
hostResizeObserver?.disconnect()
hostResizeObserver = new ResizeObserver(() => {
updateGridCardHeight()
})
hostResizeObserver.observe(snapScrollHost)
updateGridCardHeight()
}
const unbindSnapScrollHost = () => {
if (snapScrollHost) {
snapScrollHost.removeEventListener('scroll', handleSnapHostScroll)
}
hostResizeObserver?.disconnect()
hostResizeObserver = null
snapScrollHost = null
}
const trySnapToGrid = () => {
if (isSnapping || !snapScrollHost || !agGridRef.value) return
const hostRect = snapScrollHost.getBoundingClientRect()
const gridRect = agGridRef.value.getBoundingClientRect()
const offsetTop = gridRect.top - hostRect.top
const inVisibleBand = gridRect.bottom > hostRect.top + 40 && gridRect.top < hostRect.bottom - 40
const inSnapRange = offsetTop > -120 && offsetTop < 180
if (!inVisibleBand || !inSnapRange) return
isSnapping = true
agGridRef.value.scrollIntoView({ behavior: 'smooth', block: 'start' })
if (snapLockTimer) clearTimeout(snapLockTimer)
snapLockTimer = setTimeout(() => {
isSnapping = false
}, 420)
}
function handleSnapHostScroll() {
if (isSnapping) return
if (snapTimer) clearTimeout(snapTimer)
snapTimer = setTimeout(() => {
trySnapToGrid()
}, 90)
}
type MajorLite = { code: string; name: string } type MajorLite = { code: string; name: string }
const majorEntries = Object.entries(majorList as Record<string, MajorLite>) const majorEntries = Object.entries(majorList as Record<string, MajorLite>)
.sort((a, b) => Number(a[0]) - Number(b[0])) .sort((a, b) => Number(a[0]) - Number(b[0]))
@ -330,13 +371,20 @@ watch(projectName, schedulePersist)
onMounted(async () => { onMounted(async () => {
await loadFromIndexedDB() await loadFromIndexedDB()
bindSnapScrollHost()
requestAnimationFrame(() => {
updateGridCardHeight()
})
// window.addEventListener('beforeunload', handleBeforeUnload) // window.addEventListener('beforeunload', handleBeforeUnload)
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
// window.removeEventListener('beforeunload', handleBeforeUnload) // window.removeEventListener('beforeunload', handleBeforeUnload)
unbindSnapScrollHost()
if (persistTimer) clearTimeout(persistTimer) if (persistTimer) clearTimeout(persistTimer)
if (gridPersistTimer) clearTimeout(gridPersistTimer) if (gridPersistTimer) clearTimeout(gridPersistTimer)
if (snapTimer) clearTimeout(snapTimer)
if (snapLockTimer) clearTimeout(snapLockTimer)
void saveToIndexedDB() void saveToIndexedDB()
}) })
const processCellForClipboard = (params:any) => { const processCellForClipboard = (params:any) => {
@ -358,9 +406,11 @@ const processCellFromClipboard = (params:any) => {
</script> </script>
<template> <template>
<div class="space-y-6"> <div ref="rootRef" class="space-y-6">
<div class="rounded-lg border bg-card p-4 shadow-sm"> <div class="rounded-lg border bg-card p-4 shadow-sm shrink-0">
<label class="mb-2 block text-sm font-medium text-foreground">项目名称</label> <div class="mb-2 flex items-center justify-between gap-3">
<label class="block text-sm font-medium text-foreground">项目名称</label>
</div>
<input <input
v-model="projectName" v-model="projectName"
type="text" type="text"
@ -369,13 +419,16 @@ const processCellFromClipboard = (params:any) => {
/> />
</div> </div>
<div class="rounded-lg border bg-card xmMx"> <div
ref="gridSectionRef"
class="rounded-lg border bg-card xmMx scroll-mt-3"
>
<div class="flex items-center justify-between border-b px-4 py-3"> <div class="flex items-center justify-between border-b px-4 py-3">
<h3 class="text-sm font-semibold text-foreground">项目明细</h3> <h3 class="text-sm font-semibold text-foreground">项目明细</h3>
<div class="text-xs text-muted-foreground">导入导出</div> <div class="text-xs text-muted-foreground">导入导出</div>
</div> </div>
<div class="ag-theme-quartz h-[580px] w-full"> <div ref="agGridRef" class="ag-theme-quartz w-full" :style="{ height: `${agGridHeight}px` }">
<AgGridVue <AgGridVue
:style="{ height: '100%' }" :style="{ height: '100%' }"
:rowData="detailRows" :rowData="detailRows"

View File

@ -1,11 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue' import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
import type { ComponentPublicInstance } from 'vue' import type { ComponentPublicInstance } from 'vue'
import { AgGridVue } from 'ag-grid-vue3' import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef, GridOptions, ICellRendererParams } from 'ag-grid-community' import type { ColDef, GridOptions, ICellRendererParams } from 'ag-grid-community'
import localforage from 'localforage' import localforage from 'localforage'
import 'ag-grid-enterprise' import 'ag-grid-enterprise'
import { themeQuartz } from 'ag-grid-community' import { myTheme } from '@/lib/diyAgGridTheme'
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale' import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
import { Search } from 'lucide-vue-next' import { Search } from 'lucide-vue-next'
import { import {
@ -52,24 +52,6 @@ const props = defineProps<{
const tabStore = useTabStore() const tabStore = useTabStore()
const DB_KEY = computed(() => `zxFW-${props.contractId}`) const DB_KEY = computed(() => `zxFW-${props.contractId}`)
const borderConfig = {
style: 'solid',
width: 0.5,
color: '#e5e7eb'
}
const myTheme = themeQuartz.withParams({
wrapperBorder: false,
headerBackgroundColor: '#f9fafb',
headerTextColor: '#374151',
headerFontSize: 15,
headerFontWeight: 'normal',
rowBorder: borderConfig,
columnBorder: borderConfig,
headerRowBorder: borderConfig,
dataBackgroundColor: '#fefefe'
})
type ServiceListItem = { code?: string; ref?: string; name: string; defCoe: number | null } type ServiceListItem = { code?: string; ref?: string; name: string; defCoe: number | null }
const serviceDict: ServiceItem[] = Object.entries(serviceList as Record<string, ServiceListItem>) const serviceDict: ServiceItem[] = Object.entries(serviceList as Record<string, ServiceListItem>)
.sort((a, b) => Number(a[0]) - Number(b[0])) .sort((a, b) => Number(a[0]) - Number(b[0]))
@ -91,6 +73,73 @@ const isFixedRow = (row?: DetailRow | null) => row?.id === fixedBudgetRow.id
const selectedIds = ref<string[]>([]) const selectedIds = ref<string[]>([])
const detailRows = ref<DetailRow[]>([]) const detailRows = ref<DetailRow[]>([])
const rootRef = ref<HTMLElement | null>(null)
const gridSectionRef = ref<HTMLElement | null>(null)
const agGridRef = ref<HTMLElement | null>(null)
const agGridHeight = ref(580)
let snapScrollHost: HTMLElement | null = null
let snapTimer: ReturnType<typeof setTimeout> | null = null
let snapLockTimer: ReturnType<typeof setTimeout> | null = null
let isSnapping = false
let hostResizeObserver: ResizeObserver | null = null
const updateGridCardHeight = () => {
if (!snapScrollHost || !rootRef.value) return
const contentWrap = rootRef.value.parentElement
const style = contentWrap ? window.getComputedStyle(contentWrap) : null
const paddingTop = style ? Number.parseFloat(style.paddingTop || '0') || 0 : 0
const paddingBottom = style ? Number.parseFloat(style.paddingBottom || '0') || 0 : 0
const nextHeight = Math.max(360, Math.floor(snapScrollHost.clientHeight - paddingTop - paddingBottom))
agGridHeight.value = nextHeight
}
const bindSnapScrollHost = () => {
snapScrollHost = rootRef.value?.closest('[data-slot="scroll-area-viewport"]') as HTMLElement | null
if (!snapScrollHost) return
snapScrollHost.addEventListener('scroll', handleSnapHostScroll, { passive: true })
hostResizeObserver?.disconnect()
hostResizeObserver = new ResizeObserver(() => {
updateGridCardHeight()
})
hostResizeObserver.observe(snapScrollHost)
updateGridCardHeight()
}
const unbindSnapScrollHost = () => {
if (snapScrollHost) {
snapScrollHost.removeEventListener('scroll', handleSnapHostScroll)
}
hostResizeObserver?.disconnect()
hostResizeObserver = null
snapScrollHost = null
}
const trySnapToGrid = () => {
if (isSnapping || !snapScrollHost || !agGridRef.value) return
const hostRect = snapScrollHost.getBoundingClientRect()
const gridRect = agGridRef.value.getBoundingClientRect()
const offsetTop = gridRect.top - hostRect.top
const inVisibleBand = gridRect.bottom > hostRect.top + 40 && gridRect.top < hostRect.bottom - 40
const inSnapRange = offsetTop > -120 && offsetTop < 180
if (!inVisibleBand || !inSnapRange) return
isSnapping = true
agGridRef.value.scrollIntoView({ behavior: 'smooth', block: 'start' })
if (snapLockTimer) clearTimeout(snapLockTimer)
snapLockTimer = setTimeout(() => {
isSnapping = false
}, 420)
}
function handleSnapHostScroll() {
if (isSnapping) return
if (snapTimer) clearTimeout(snapTimer)
snapTimer = setTimeout(() => {
trySnapToGrid()
}, 90)
}
const pickerOpen = ref(false) const pickerOpen = ref(false)
const pickerTempIds = ref<string[]>([]) const pickerTempIds = ref<string[]>([])
@ -139,13 +188,35 @@ const numericParser = (newValue: any): number | null => {
const valueOrZero = (v: number | null | undefined) => (typeof v === 'number' ? v : 0) const valueOrZero = (v: number | null | undefined) => (typeof v === 'number' ? v : 0)
const getPricingPaneStorageKeys = (serviceId: string) => [
`tzGMF-${props.contractId}-${serviceId}`,
`ydGMF-${props.contractId}-${serviceId}`,
`gzlF-${props.contractId}-${serviceId}`,
`hourlyPricing-${props.contractId}-${serviceId}`
]
const clearPricingPaneValues = async (serviceId: string) => {
const keys = getPricingPaneStorageKeys(serviceId)
await Promise.all(keys.map(key => localforage.removeItem(key)))
}
const clearRowValues = async (row: DetailRow) => { const clearRowValues = async (row: DetailRow) => {
if (isFixedRow(row)) return if (isFixedRow(row)) return
// <EFBFBD>? tabStore.removeTab(`zxfw-edit-${props.contractId}-${row.id}`)
await nextTick()
row.investScale = null row.investScale = null
row.landScale = null row.landScale = null
row.workload = null row.workload = null
row.hourly = null row.hourly = null
row.subtotal = null row.subtotal = null
detailRows.value = [...detailRows.value]
await clearPricingPaneValues(row.id)
// <EFBFBD>?
await new Promise(resolve => setTimeout(resolve, 80))
await clearPricingPaneValues(row.id)
await saveToIndexedDB() await saveToIndexedDB()
} }
@ -153,7 +224,7 @@ const clearRowValues = async (row: DetailRow) => {
const openEditTab = (row: DetailRow) => { const openEditTab = (row: DetailRow) => {
tabStore.openTab({ tabStore.openTab({
id: `zxfw-edit-${props.contractId}-${row.id}`, id: `zxfw-edit-${props.contractId}-${row.id}`,
title: `服务编辑${row.code}${row.name}`, title: `服务编辑-${row.code}${row.name}`,
componentName: 'ZxFwView', componentName: 'ZxFwView',
props: { contractId: props.contractId, contractName: row.name ,serviceId: row.id} props: { contractId: props.contractId, contractName: row.name ,serviceId: row.id}
}) })
@ -485,20 +556,29 @@ const handleCellValueChanged = () => {
onMounted(async () => { onMounted(async () => {
await loadFromIndexedDB() await loadFromIndexedDB()
bindSnapScrollHost()
requestAnimationFrame(() => {
updateGridCardHeight()
})
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
unbindSnapScrollHost()
stopDragSelect() stopDragSelect()
if (gridPersistTimer) clearTimeout(gridPersistTimer) if (gridPersistTimer) clearTimeout(gridPersistTimer)
if (snapTimer) clearTimeout(snapTimer)
if (snapLockTimer) clearTimeout(snapLockTimer)
void saveToIndexedDB() void saveToIndexedDB()
}) })
</script> </script>
<template> <template>
<div class="space-y-6"> <div ref="rootRef" class="space-y-6">
<DialogRoot v-model:open="pickerOpen" @update:open="handlePickerOpenChange"> <DialogRoot v-model:open="pickerOpen" @update:open="handlePickerOpenChange">
<div class="rounded-lg border bg-card p-4 shadow-sm"> <div class="rounded-lg border bg-card p-4 shadow-sm shrink-0">
<label class="mb-2 block text-sm font-medium text-foreground">选择服务</label> <div class="mb-2 flex items-center justify-between gap-3">
<label class="block text-sm font-medium text-foreground">选择服务</label>
</div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<input :value="selectedServiceText" readonly placeholder="请点击右侧“浏览”选择服务" <input :value="selectedServiceText" readonly placeholder="请点击右侧“浏览”选择服务"
class="h-10 w-full rounded-md border bg-background px-3 text-sm text-foreground outline-none" /> class="h-10 w-full rounded-md border bg-background px-3 text-sm text-foreground outline-none" />
@ -516,7 +596,7 @@ onBeforeUnmount(() => {
<DialogContent <DialogContent
class="fixed left-1/2 top-1/2 z-[60] w-[96vw] max-w-2xl -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background shadow-xl p-0"> class="fixed left-1/2 top-1/2 z-[60] w-[96vw] max-w-2xl -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background shadow-xl p-0">
<DialogTitle class="sr-only">选择服务词典</DialogTitle> <DialogTitle class="sr-only">选择服务词典</DialogTitle>
<DialogDescription class="sr-only">浏览并选择服务</DialogDescription> <DialogDescription class="sr-only">浏览并选择服务词典</DialogDescription>
<div class="flex items-center justify-between border-b px-5 py-4"> <div class="flex items-center justify-between border-b px-5 py-4">
<h4 class="text-base font-semibold">选择服务词典</h4> <h4 class="text-base font-semibold">选择服务词典</h4>
<DialogClose as-child> <DialogClose as-child>
@ -561,13 +641,16 @@ onBeforeUnmount(() => {
</DialogPortal> </DialogPortal>
</DialogRoot> </DialogRoot>
<div class="rounded-lg border bg-card xmMx"> <div
ref="gridSectionRef"
class="rounded-lg border bg-card xmMx scroll-mt-3"
>
<div class="flex items-center justify-between border-b px-4 py-3"> <div class="flex items-center justify-between border-b px-4 py-3">
<h3 class="text-sm font-semibold text-foreground">咨询服务明细</h3> <h3 class="text-sm font-semibold text-foreground">咨询服务明细</h3>
<div class="text-xs text-muted-foreground">按服务词典生成</div> <div class="text-xs text-muted-foreground">按服务词典生成</div>
</div> </div>
<div class="ag-theme-quartz h-[580px] w-full"> <div ref="agGridRef" class="ag-theme-quartz w-full" :style="{ height: `${agGridHeight}px` }">
<AgGridVue :style="{ height: '100%' }" :rowData="detailRows" :columnDefs="columnDefs" :gridOptions="gridOptions" <AgGridVue :style="{ height: '100%' }" :rowData="detailRows" :columnDefs="columnDefs" :gridOptions="gridOptions"
:theme="myTheme" @cell-value-changed="handleCellValueChanged" :enableClipboard="true" :theme="myTheme" @cell-value-changed="handleCellValueChanged" :enableClipboard="true"
:localeText="AG_GRID_LOCALE_CN" :tooltipShowDelay="500" :headerHeight="50" :undoRedoCellEditing="true" :localeText="AG_GRID_LOCALE_CN" :tooltipShowDelay="500" :headerHeight="50" :undoRedoCellEditing="true"

View File

@ -92,9 +92,9 @@ const activeComponent = computed(() => {
</div> </div>
</div> </div>
<div class="w-88/100 min-h-0"> <div class="w-88/100 min-h-0 h-full flex flex-col">
<ScrollArea class="h-full w-full"> <ScrollArea class="h-full w-full min-h-0 rightMain">
<div class="p-3"> <div class="p-3 h-full min-h-0 flex flex-col">
<keep-alive> <keep-alive>
<component :is="activeComponent" /> <component :is="activeComponent" />
</keep-alive> </keep-alive>
@ -103,3 +103,11 @@ const activeComponent = computed(() => {
</div> </div>
</div> </div>
</template> </template>
<style scoped>
/* 核心修改:添加 :deep() 穿透 scoped 作用域 */
:deep(.rightMain > div > div) {
height: 100%;
min-height: 0;
box-sizing: border-box;
}
</style>

30
src/lib/diyAgGridTheme.ts Normal file
View File

@ -0,0 +1,30 @@
import {
themeQuartz
} from "ag-grid-community"
const borderConfig = {
style: "solid", // 虚线改实线更简洁,也可保留 dotted 但建议用 solid
width: 0.5, // 更细的边框,减少视觉干扰
color: "#e5e7eb" // 浅灰色边框,清新不刺眼
};
// 简洁清新风格的主题配置
export const myTheme = themeQuartz.withParams({
// 核心:移除外边框,减少视觉包裹感
wrapperBorder: false,
// 表头样式(柔和浅蓝,无加粗,更轻盈)
headerBackgroundColor: "#f9fafb", // 极浅的背景色,替代深一点的 #e7f3fc
headerTextColor: "#374151", // 深灰色文字,比纯黑更柔和
headerFontSize: 15, // 字体稍大一点,更易读
headerFontWeight: "normal", // 取消加粗,降低视觉重量
// 行/列/表头边框(统一浅灰细边框)
rowBorder: borderConfig,
columnBorder: borderConfig,
headerRowBorder: borderConfig,
// 可选:偶数行背景色(轻微区分,更清新)
dataBackgroundColor: "#fefefe"
});

View File

@ -64,3 +64,81 @@ export const serviceList = {
28: { code: 'D4-14', name: '其他专项咨询', maxCoe: null, minCoe: null, defCoe: null, desc: '可参照相同或相似服务的系数。', taskList: null }, 28: { code: 'D4-14', name: '其他专项咨询', maxCoe: null, minCoe: null, defCoe: null, desc: '可参照相同或相似服务的系数。', taskList: null },
}; };
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 } },
];
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 } },
];

View File

@ -111,12 +111,23 @@
--sidebar-border: oklch(1 0 0 / 10%); --sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0); --sidebar-ring: oklch(0.556 0 0);
} }
.ag-horizontal-left-spacer{
overflow-x: auto
}
@layer base { @layer base {
* { * {
@apply border-border outline-ring/50; @apply border-border outline-ring/50;
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
input,
textarea,
[contenteditable='true'] {
-webkit-user-select: text;
user-select: text;
}
} }