fix bug
This commit is contained in:
1 parent
1609f19b9c
commit
37f4a99914
14 files changed
+953
-427
No files matched your search
@@ -3,7 +3,7 @@
|
||||
<TypeLine
|
||||
scene="ht-tab"
|
||||
:title="`合同段:${contractName}`"
|
||||
storage-key="project-active-cat"
|
||||
:storage-key="`project-active-cat-${contractId}`"
|
||||
default-category="info"
|
||||
:categories="xmCategories"
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import draggable from 'vuedraggable'
|
||||
import localforage from 'localforage'
|
||||
import { Card, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
@@ -23,6 +23,10 @@ interface ContractItem {
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'ht-card-v1'
|
||||
const formStore = localforage.createInstance({
|
||||
name: 'jgjs-pricing-db',
|
||||
storeName: 'form_state'
|
||||
})
|
||||
|
||||
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 () => {
|
||||
try {
|
||||
const saved = await localforage.getItem<ContractItem[]>(STORAGE_KEY)
|
||||
@@ -122,6 +160,13 @@ const createContract = async () => {
|
||||
if (!name) return
|
||||
|
||||
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 =>
|
||||
item.id === editingContractId.value ? { ...item, name } : item
|
||||
)
|
||||
@@ -147,13 +192,28 @@ const createContract = async () => {
|
||||
}
|
||||
|
||||
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)
|
||||
await saveContracts()
|
||||
tabStore.removeTab(`contract-${id}`)
|
||||
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()
|
||||
notify('排序完成')
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<TypeLine
|
||||
scene="zxfw-pricing-tab"
|
||||
title="咨询服务计算"
|
||||
storage-key="zxfw-pricing-active-cat"
|
||||
:storage-key="`zxfw-pricing-active-cat-${contractId}-${serviceId}`"
|
||||
default-category="investment-scale-method"
|
||||
:categories="pricingCategories"
|
||||
/>
|
||||
@@ -15,6 +15,7 @@ import TypeLine from '@/layout/typeLine.vue'
|
||||
const props = defineProps<{
|
||||
contractId: string
|
||||
contractName?: string
|
||||
serviceId: string|number
|
||||
}>()
|
||||
|
||||
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 })
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
@@ -5,38 +5,11 @@ import type { ColDef, GridOptions } from 'ag-grid-community'
|
||||
import localforage from 'localforage'
|
||||
import { majorList } from '@/sql'
|
||||
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';
|
||||
// 精简的边框配置(细线条+浅灰色,弱化分割线视觉)
|
||||
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 {
|
||||
id: string
|
||||
code: string
|
||||
@@ -70,6 +43,7 @@ const props = defineProps<{
|
||||
contractId: string
|
||||
}>()
|
||||
const DB_KEY = computed(() => `ht-info-v3-${props.contractId}`)
|
||||
const XM_DB_KEY = 'xm-info-v3'
|
||||
|
||||
const detailRows = ref<DetailRow[]>([])
|
||||
|
||||
@@ -300,6 +274,13 @@ const loadFromIndexedDB = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
// 首次创建合同段时,默认继承项目规模信息(同一套专业字典,按 id 对齐)
|
||||
const xmData = await localforage.getItem<XmInfoState>(XM_DB_KEY)
|
||||
if (xmData?.detailRows) {
|
||||
detailRows.value = mergeWithDictRows(xmData.detailRows)
|
||||
return
|
||||
}
|
||||
|
||||
detailRows.value = buildDefaultRows()
|
||||
} catch (error) {
|
||||
console.error('loadFromIndexedDB failed:', error)
|
||||
@@ -349,16 +330,16 @@ const processCellFromClipboard = (params:any) => {
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<h3 class="text-sm font-semibold text-foreground">合同规模明细</h3>
|
||||
<div class="text-xs text-muted-foreground">导入导出</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
|
||||
:style="{ height: '100%' }"
|
||||
:rowData="detailRows"
|
||||
|
||||
@@ -1,42 +1,14 @@
|
||||
<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, ColGroupDef, GridOptions } from 'ag-grid-community'
|
||||
import localforage from 'localforage'
|
||||
import { majorList } from '@/sql'
|
||||
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';
|
||||
// 精简的边框配置(细线条+浅灰色,弱化分割线视觉)
|
||||
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 {
|
||||
id: string
|
||||
code: string
|
||||
@@ -56,8 +28,13 @@ interface DetailRow {
|
||||
groupName: string
|
||||
majorCode: string
|
||||
majorName: string
|
||||
amount: number | null
|
||||
landArea: number | null
|
||||
laborBudgetUnitPrice: number | null
|
||||
compositeBudgetUnitPrice: number | null
|
||||
adoptedBudgetUnitPrice: number | null
|
||||
personnelCount: number | null
|
||||
workdayCount: number | null
|
||||
serviceBudget: number | null
|
||||
remark: string
|
||||
path: string[]
|
||||
}
|
||||
|
||||
@@ -67,9 +44,11 @@ interface XmInfoState {
|
||||
}
|
||||
|
||||
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[]>([])
|
||||
|
||||
@@ -140,8 +119,13 @@ const buildDefaultRows = (): DetailRow[] => {
|
||||
groupName: group.name,
|
||||
majorCode: child.code,
|
||||
majorName: child.name,
|
||||
amount: null,
|
||||
landArea: null,
|
||||
laborBudgetUnitPrice: null,
|
||||
compositeBudgetUnitPrice: null,
|
||||
adoptedBudgetUnitPrice: null,
|
||||
personnelCount: null,
|
||||
workdayCount: null,
|
||||
serviceBudget: null,
|
||||
remark: '',
|
||||
path: [group.id, child.id]
|
||||
})
|
||||
}
|
||||
@@ -161,73 +145,100 @@ const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] =>
|
||||
|
||||
return {
|
||||
...row,
|
||||
amount: typeof fromDb.amount === 'number' ? fromDb.amount : null,
|
||||
landArea: typeof fromDb.landArea === 'number' ? fromDb.landArea : null
|
||||
laborBudgetUnitPrice:
|
||||
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>[] = [
|
||||
|
||||
{
|
||||
headerName: '造价金额(万元)',
|
||||
field: 'amount',
|
||||
minWidth: 170,
|
||||
flex: 1, // 核心:开启弹性布局,自动占满剩余空间
|
||||
const parseNumberOrNull = (value: unknown) => {
|
||||
if (value === '' || value == null) return null
|
||||
const v = Number(value)
|
||||
return Number.isFinite(v) ? v : null
|
||||
}
|
||||
|
||||
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
|
||||
},
|
||||
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 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 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,
|
||||
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,
|
||||
...extra
|
||||
})
|
||||
|
||||
const columnDefs: (ColDef<DetailRow> | ColGroupDef<DetailRow>)[] = [
|
||||
{
|
||||
headerName: '人员名称',
|
||||
minWidth: 200,
|
||||
width: 220,
|
||||
pinned: 'left',
|
||||
valueGetter: params => {
|
||||
if (params.node?.rowPinned) return ''
|
||||
return params.node?.group ? params.data?.groupName || '' : params.data?.majorName || ''
|
||||
}
|
||||
},
|
||||
{
|
||||
headerName: '用地面积(亩)',
|
||||
field: 'landArea',
|
||||
minWidth: 170,
|
||||
flex: 1, // 核心:开启弹性布局,自动占满剩余空间
|
||||
|
||||
headerName: '预算参考单价',
|
||||
marryChildren: true,
|
||||
children: [
|
||||
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,
|
||||
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' : ''),
|
||||
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
|
||||
},
|
||||
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 = {
|
||||
headerName: '专业编码以及工程专业名称',
|
||||
minWidth: 320,
|
||||
headerName: '编码',
|
||||
minWidth: 150,
|
||||
pinned: 'left',
|
||||
flex:2, // 核心:开启弹性布局,自动占满剩余空间
|
||||
width: 160,
|
||||
|
||||
cellRendererParams: {
|
||||
suppressCount: true
|
||||
@@ -237,7 +248,8 @@ const autoGroupColumnDef: ColDef = {
|
||||
return '总合计'
|
||||
}
|
||||
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(() =>
|
||||
detailRows.value.reduce((sum, row) => sum + (row.amount || 0), 0)
|
||||
const totalPersonnelCount = computed(() =>
|
||||
detailRows.value.reduce((sum, row) => sum + (row.personnelCount || 0), 0)
|
||||
)
|
||||
|
||||
const totalLandArea = computed(() =>
|
||||
detailRows.value.reduce((sum, row) => sum + (row.landArea || 0), 0)
|
||||
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 pinnedTopRowData = computed(() => [
|
||||
{
|
||||
@@ -272,8 +288,13 @@ const pinnedTopRowData = computed(() => [
|
||||
groupName: '',
|
||||
majorCode: '',
|
||||
majorName: '',
|
||||
amount: totalAmount.value,
|
||||
landArea: totalLandArea.value,
|
||||
laborBudgetUnitPrice: null,
|
||||
compositeBudgetUnitPrice: null,
|
||||
adoptedBudgetUnitPrice: null,
|
||||
personnelCount: totalPersonnelCount.value,
|
||||
workdayCount: totalWorkdayCount.value,
|
||||
serviceBudget: totalServiceBudget.value,
|
||||
remark: '',
|
||||
path: ['TOTAL']
|
||||
}
|
||||
])
|
||||
@@ -349,16 +370,16 @@ const processCellFromClipboard = (params:any) => {
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<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>
|
||||
|
||||
<div class="ag-theme-quartz h-[580px] w-full">
|
||||
<div class="ag-theme-quartz h-full min-h-0 w-full flex-1">
|
||||
<AgGridVue
|
||||
:style="{ height: '100%' }"
|
||||
:rowData="detailRows"
|
||||
|
||||
@@ -5,38 +5,11 @@ import type { ColDef, GridOptions } from 'ag-grid-community'
|
||||
import localforage from 'localforage'
|
||||
import { majorList } from '@/sql'
|
||||
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';
|
||||
// 精简的边框配置(细线条+浅灰色,弱化分割线视觉)
|
||||
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 {
|
||||
id: string
|
||||
code: string
|
||||
@@ -57,7 +30,11 @@ interface DetailRow {
|
||||
majorCode: string
|
||||
majorName: string
|
||||
amount: number | null
|
||||
landArea: number | null
|
||||
benchmarkBudget: number | null
|
||||
consultCategoryFactor: number | null
|
||||
majorFactor: number | null
|
||||
budgetFee: number | null
|
||||
remark: string
|
||||
path: string[]
|
||||
}
|
||||
|
||||
@@ -67,9 +44,10 @@ interface XmInfoState {
|
||||
}
|
||||
|
||||
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[]>([])
|
||||
|
||||
@@ -141,7 +119,11 @@ const buildDefaultRows = (): DetailRow[] => {
|
||||
majorCode: child.code,
|
||||
majorName: child.name,
|
||||
amount: null,
|
||||
landArea: null,
|
||||
benchmarkBudget: null,
|
||||
consultCategoryFactor: null,
|
||||
majorFactor: null,
|
||||
budgetFee: null,
|
||||
remark: '',
|
||||
path: [group.id, child.id]
|
||||
})
|
||||
}
|
||||
@@ -162,18 +144,46 @@ const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] =>
|
||||
return {
|
||||
...row,
|
||||
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 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: '造价金额(万元)',
|
||||
field: 'amount',
|
||||
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' : ''),
|
||||
@@ -182,24 +192,14 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
!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)
|
||||
}
|
||||
valueParser: params => parseNumberOrNull(params.newValue),
|
||||
valueFormatter: formatEditableNumber
|
||||
},
|
||||
{
|
||||
headerName: '用地面积(亩)',
|
||||
field: 'landArea',
|
||||
headerName: '基准预算(元)',
|
||||
field: 'benchmarkBudget',
|
||||
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' : ''),
|
||||
@@ -208,26 +208,75 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
!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
|
||||
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 => {
|
||||
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
|
||||
return '点击输入'
|
||||
}
|
||||
if (params.value == null) return ''
|
||||
return Number(params.value).toFixed(2)
|
||||
if (!params.node?.group && !params.node?.rowPinned && !params.value) return '点击输入'
|
||||
return params.value || ''
|
||||
},
|
||||
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 === '')
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const autoGroupColumnDef: ColDef = {
|
||||
headerName: '专业编码以及工程专业名称',
|
||||
minWidth: 320,
|
||||
headerName: '专业编码',
|
||||
minWidth: 160,
|
||||
pinned: 'left',
|
||||
flex:2, // 核心:开启弹性布局,自动占满剩余空间
|
||||
width: 170,
|
||||
|
||||
cellRendererParams: {
|
||||
suppressCount: true
|
||||
@@ -237,7 +286,8 @@ const autoGroupColumnDef: ColDef = {
|
||||
return '总合计'
|
||||
}
|
||||
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)
|
||||
)
|
||||
|
||||
const totalLandArea = computed(() =>
|
||||
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(() => [
|
||||
{
|
||||
@@ -273,7 +327,11 @@ const pinnedTopRowData = computed(() => [
|
||||
majorCode: '',
|
||||
majorName: '',
|
||||
amount: totalAmount.value,
|
||||
landArea: totalLandArea.value,
|
||||
benchmarkBudget: totalBenchmarkBudget.value,
|
||||
consultCategoryFactor: null,
|
||||
majorFactor: null,
|
||||
budgetFee: totalBudgetFee.value,
|
||||
remark: '',
|
||||
path: ['TOTAL']
|
||||
}
|
||||
])
|
||||
@@ -349,16 +407,16 @@ const processCellFromClipboard = (params:any) => {
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<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>
|
||||
|
||||
<div class="ag-theme-quartz h-[580px] w-full">
|
||||
<div class="ag-theme-quartz h-full min-h-0 w-full flex-1">
|
||||
<AgGridVue
|
||||
:style="{ height: '100%' }"
|
||||
:rowData="detailRows"
|
||||
|
||||
@@ -5,38 +5,11 @@ import type { ColDef, GridOptions } from 'ag-grid-community'
|
||||
import localforage from 'localforage'
|
||||
import { majorList } from '@/sql'
|
||||
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';
|
||||
// 精简的边框配置(细线条+浅灰色,弱化分割线视觉)
|
||||
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 {
|
||||
id: string
|
||||
code: string
|
||||
@@ -58,6 +31,11 @@ interface DetailRow {
|
||||
majorName: string
|
||||
amount: number | null
|
||||
landArea: number | null
|
||||
benchmarkBudget: number | null
|
||||
consultCategoryFactor: number | null
|
||||
majorFactor: number | null
|
||||
budgetFee: number | null
|
||||
remark: string
|
||||
path: string[]
|
||||
}
|
||||
|
||||
@@ -67,9 +45,10 @@ interface XmInfoState {
|
||||
}
|
||||
|
||||
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[]>([])
|
||||
|
||||
@@ -142,6 +121,11 @@ const buildDefaultRows = (): DetailRow[] => {
|
||||
majorName: child.name,
|
||||
amount: null,
|
||||
landArea: null,
|
||||
benchmarkBudget: null,
|
||||
consultCategoryFactor: null,
|
||||
majorFactor: null,
|
||||
budgetFee: null,
|
||||
remark: '',
|
||||
path: [group.id, child.id]
|
||||
})
|
||||
}
|
||||
@@ -162,44 +146,47 @@ const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] =>
|
||||
return {
|
||||
...row,
|
||||
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>[] = [
|
||||
|
||||
{
|
||||
headerName: '造价金额(万元)',
|
||||
field: 'amount',
|
||||
minWidth: 170,
|
||||
flex: 1, // 核心:开启弹性布局,自动占满剩余空间
|
||||
const parseNumberOrNull = (value: unknown) => {
|
||||
if (value === '' || value == null) return null
|
||||
const v = Number(value)
|
||||
return Number.isFinite(v) ? v : null
|
||||
}
|
||||
|
||||
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
|
||||
},
|
||||
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 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: '用地面积(亩)',
|
||||
field: 'landArea',
|
||||
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' : ''),
|
||||
@@ -208,26 +195,90 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
!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
|
||||
valueParser: params => parseNumberOrNull(params.newValue),
|
||||
valueFormatter: formatEditableNumber
|
||||
},
|
||||
{
|
||||
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 => {
|
||||
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
|
||||
return '点击输入'
|
||||
}
|
||||
if (params.value == null) return ''
|
||||
return Number(params.value).toFixed(2)
|
||||
if (!params.node?.group && !params.node?.rowPinned && !params.value) return '点击输入'
|
||||
return params.value || ''
|
||||
},
|
||||
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 === '')
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const autoGroupColumnDef: ColDef = {
|
||||
headerName: '专业编码以及工程专业名称',
|
||||
minWidth: 320,
|
||||
headerName: '专业编码',
|
||||
minWidth: 160,
|
||||
pinned: 'left',
|
||||
flex:2, // 核心:开启弹性布局,自动占满剩余空间
|
||||
width: 170,
|
||||
|
||||
cellRendererParams: {
|
||||
suppressCount: true
|
||||
@@ -237,7 +288,8 @@ const autoGroupColumnDef: ColDef = {
|
||||
return '总合计'
|
||||
}
|
||||
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(() =>
|
||||
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(() => [
|
||||
{
|
||||
id: 'pinned-total-row',
|
||||
@@ -274,6 +334,11 @@ const pinnedTopRowData = computed(() => [
|
||||
majorName: '',
|
||||
amount: totalAmount.value,
|
||||
landArea: totalLandArea.value,
|
||||
benchmarkBudget: totalBenchmarkBudget.value,
|
||||
consultCategoryFactor: null,
|
||||
majorFactor: null,
|
||||
budgetFee: totalBudgetFee.value,
|
||||
remark: '',
|
||||
path: ['TOTAL']
|
||||
}
|
||||
])
|
||||
@@ -349,16 +414,16 @@ const processCellFromClipboard = (params:any) => {
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<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>
|
||||
|
||||
<div class="ag-theme-quartz h-[580px] w-full">
|
||||
<div class="ag-theme-quartz h-full min-h-0 w-full flex-1">
|
||||
<AgGridVue
|
||||
:style="{ height: '100%' }"
|
||||
:rowData="detailRows"
|
||||
|
||||
@@ -5,38 +5,10 @@ import type { ColDef, GridOptions } from 'ag-grid-community'
|
||||
import localforage from 'localforage'
|
||||
import { majorList } from '@/sql'
|
||||
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';
|
||||
// 精简的边框配置(细线条+浅灰色,弱化分割线视觉)
|
||||
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 {
|
||||
id: string
|
||||
code: string
|
||||
@@ -56,8 +28,13 @@ interface DetailRow {
|
||||
groupName: string
|
||||
majorCode: string
|
||||
majorName: string
|
||||
amount: number | null
|
||||
landArea: number | null
|
||||
workload: number | null
|
||||
budgetBase: number | null
|
||||
budgetReferenceUnitPrice: number | null
|
||||
budgetAdoptedUnitPrice: number | null
|
||||
consultCategoryFactor: number | null
|
||||
serviceFee: number | null
|
||||
remark: string
|
||||
path: string[]
|
||||
}
|
||||
|
||||
@@ -67,9 +44,11 @@ interface XmInfoState {
|
||||
}
|
||||
|
||||
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[]>([])
|
||||
|
||||
@@ -140,8 +119,13 @@ const buildDefaultRows = (): DetailRow[] => {
|
||||
groupName: group.name,
|
||||
majorCode: child.code,
|
||||
majorName: child.name,
|
||||
amount: null,
|
||||
landArea: null,
|
||||
workload: null,
|
||||
budgetBase: null,
|
||||
budgetReferenceUnitPrice: null,
|
||||
budgetAdoptedUnitPrice: null,
|
||||
consultCategoryFactor: null,
|
||||
serviceFee: null,
|
||||
remark: '',
|
||||
path: [group.id, child.id]
|
||||
})
|
||||
}
|
||||
@@ -161,45 +145,65 @@ const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] =>
|
||||
|
||||
return {
|
||||
...row,
|
||||
amount: typeof fromDb.amount === 'number' ? fromDb.amount : null,
|
||||
landArea: typeof fromDb.landArea === 'number' ? fromDb.landArea : null
|
||||
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:
|
||||
typeof fromDb.consultCategoryFactor === 'number' ? fromDb.consultCategoryFactor : null,
|
||||
serviceFee: typeof fromDb.serviceFee === 'number' ? fromDb.serviceFee : null,
|
||||
remark: typeof fromDb.remark === 'string' ? fromDb.remark : ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const columnDefs: ColDef<DetailRow>[] = [
|
||||
|
||||
{
|
||||
headerName: '造价金额(万元)',
|
||||
field: 'amount',
|
||||
minWidth: 170,
|
||||
flex: 1, // 核心:开启弹性布局,自动占满剩余空间
|
||||
const parseNumberOrNull = (value: unknown) => {
|
||||
if (value === '' || value == null) return null
|
||||
const v = Number(value)
|
||||
return Number.isFinite(v) ? v : null
|
||||
}
|
||||
|
||||
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
|
||||
},
|
||||
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 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: '用地面积(亩)',
|
||||
field: 'landArea',
|
||||
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,
|
||||
flex: 1, // 核心:开启弹性布局,自动占满剩余空间
|
||||
flex: 1,
|
||||
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
@@ -208,26 +212,89 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
!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
|
||||
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 === '')
|
||||
},
|
||||
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,
|
||||
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 => {
|
||||
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
|
||||
return '点击输入'
|
||||
}
|
||||
if (params.value == null) return ''
|
||||
return Number(params.value).toFixed(2)
|
||||
if (!params.node?.group && !params.node?.rowPinned && !params.value) return '点击输入'
|
||||
return params.value || ''
|
||||
},
|
||||
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 === '')
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const autoGroupColumnDef: ColDef = {
|
||||
headerName: '专业编码以及工程专业名称',
|
||||
minWidth: 320,
|
||||
headerName: '编码',
|
||||
minWidth: 160,
|
||||
pinned: 'left',
|
||||
flex:2, // 核心:开启弹性布局,自动占满剩余空间
|
||||
width: 170,
|
||||
|
||||
cellRendererParams: {
|
||||
suppressCount: true
|
||||
@@ -237,7 +304,8 @@ const autoGroupColumnDef: ColDef = {
|
||||
return '总合计'
|
||||
}
|
||||
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(() =>
|
||||
detailRows.value.reduce((sum, row) => sum + (row.amount || 0), 0)
|
||||
const totalBudgetBase = computed(() =>
|
||||
detailRows.value.reduce((sum, row) => sum + (row.budgetBase || 0), 0)
|
||||
)
|
||||
|
||||
const totalLandArea = computed(() =>
|
||||
detailRows.value.reduce((sum, row) => sum + (row.landArea || 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 pinnedTopRowData = computed(() => [
|
||||
{
|
||||
@@ -272,8 +344,13 @@ const pinnedTopRowData = computed(() => [
|
||||
groupName: '',
|
||||
majorCode: '',
|
||||
majorName: '',
|
||||
amount: totalAmount.value,
|
||||
landArea: totalLandArea.value,
|
||||
workload: totalWorkload.value,
|
||||
budgetBase: totalBudgetBase.value,
|
||||
budgetReferenceUnitPrice: null,
|
||||
budgetAdoptedUnitPrice: null,
|
||||
consultCategoryFactor: null,
|
||||
serviceFee: totalServiceFee.value,
|
||||
remark: '',
|
||||
path: ['TOTAL']
|
||||
}
|
||||
])
|
||||
@@ -349,16 +426,16 @@ const processCellFromClipboard = (params:any) => {
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<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>
|
||||
|
||||
<div class="ag-theme-quartz h-[580px] w-full">
|
||||
<div class="ag-theme-quartz h-full min-h-0 w-full flex-1">
|
||||
<AgGridVue
|
||||
:style="{ height: '100%' }"
|
||||
:rowData="detailRows"
|
||||
|
||||
@@ -5,38 +5,12 @@ import type { ColDef, GridOptions } from 'ag-grid-community'
|
||||
import localforage from 'localforage'
|
||||
import { majorList } from '@/sql'
|
||||
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';
|
||||
// 精简的边框配置(细线条+浅灰色,弱化分割线视觉)
|
||||
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 {
|
||||
id: string
|
||||
code: string
|
||||
@@ -71,6 +45,73 @@ const DEFAULT_PROJECT_NAME = 'xxx造价咨询服务'
|
||||
|
||||
const projectName = ref(DEFAULT_PROJECT_NAME)
|
||||
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 }
|
||||
const majorEntries = Object.entries(majorList as Record<string, MajorLite>)
|
||||
.sort((a, b) => Number(a[0]) - Number(b[0]))
|
||||
@@ -330,13 +371,20 @@ watch(projectName, schedulePersist)
|
||||
|
||||
onMounted(async () => {
|
||||
await loadFromIndexedDB()
|
||||
bindSnapScrollHost()
|
||||
requestAnimationFrame(() => {
|
||||
updateGridCardHeight()
|
||||
})
|
||||
// window.addEventListener('beforeunload', handleBeforeUnload)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// window.removeEventListener('beforeunload', handleBeforeUnload)
|
||||
unbindSnapScrollHost()
|
||||
if (persistTimer) clearTimeout(persistTimer)
|
||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||
if (snapTimer) clearTimeout(snapTimer)
|
||||
if (snapLockTimer) clearTimeout(snapLockTimer)
|
||||
void saveToIndexedDB()
|
||||
})
|
||||
const processCellForClipboard = (params:any) => {
|
||||
@@ -358,9 +406,11 @@ const processCellFromClipboard = (params:any) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<div class="rounded-lg border bg-card p-4 shadow-sm">
|
||||
<label class="mb-2 block text-sm font-medium text-foreground">项目名称</label>
|
||||
<div ref="rootRef" class="space-y-6">
|
||||
<div class="rounded-lg border bg-card p-4 shadow-sm shrink-0">
|
||||
<div class="mb-2 flex items-center justify-between gap-3">
|
||||
<label class="block text-sm font-medium text-foreground">项目名称</label>
|
||||
</div>
|
||||
<input
|
||||
v-model="projectName"
|
||||
type="text"
|
||||
@@ -369,13 +419,16 @@ const processCellFromClipboard = (params:any) => {
|
||||
/>
|
||||
</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">
|
||||
<h3 class="text-sm font-semibold text-foreground">项目明细</h3>
|
||||
<div class="text-xs text-muted-foreground">导入导出</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"
|
||||
|
||||
+110
-27
@@ -1,11 +1,11 @@
|
||||
<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 { AgGridVue } from 'ag-grid-vue3'
|
||||
import type { ColDef, GridOptions, ICellRendererParams } from 'ag-grid-community'
|
||||
import localforage from 'localforage'
|
||||
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 { Search } from 'lucide-vue-next'
|
||||
import {
|
||||
@@ -52,24 +52,6 @@ const props = defineProps<{
|
||||
const tabStore = useTabStore()
|
||||
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 }
|
||||
const serviceDict: ServiceItem[] = Object.entries(serviceList as Record<string, ServiceListItem>)
|
||||
.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 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 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 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) => {
|
||||
if (isFixedRow(row)) return
|
||||
|
||||
// 若该服务编辑页已打开,先关闭,避免子页面卸载时把旧数据写回缓�? tabStore.removeTab(`zxfw-edit-${props.contractId}-${row.id}`)
|
||||
await nextTick()
|
||||
|
||||
row.investScale = null
|
||||
row.landScale = null
|
||||
row.workload = null
|
||||
row.hourly = null
|
||||
row.subtotal = null
|
||||
detailRows.value = [...detailRows.value]
|
||||
|
||||
await clearPricingPaneValues(row.id)
|
||||
// 定时�?卸载保存可能产生回写,延迟再清一次更稳妥
|
||||
await new Promise(resolve => setTimeout(resolve, 80))
|
||||
await clearPricingPaneValues(row.id)
|
||||
|
||||
await saveToIndexedDB()
|
||||
}
|
||||
@@ -153,7 +224,7 @@ const clearRowValues = async (row: DetailRow) => {
|
||||
const openEditTab = (row: DetailRow) => {
|
||||
tabStore.openTab({
|
||||
id: `zxfw-edit-${props.contractId}-${row.id}`,
|
||||
title: `服务编辑:${row.code}${row.name}`,
|
||||
title: `服务编辑-${row.code}${row.name}`,
|
||||
componentName: 'ZxFwView',
|
||||
props: { contractId: props.contractId, contractName: row.name ,serviceId: row.id}
|
||||
})
|
||||
@@ -485,20 +556,29 @@ const handleCellValueChanged = () => {
|
||||
|
||||
onMounted(async () => {
|
||||
await loadFromIndexedDB()
|
||||
bindSnapScrollHost()
|
||||
requestAnimationFrame(() => {
|
||||
updateGridCardHeight()
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
unbindSnapScrollHost()
|
||||
stopDragSelect()
|
||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||
if (snapTimer) clearTimeout(snapTimer)
|
||||
if (snapLockTimer) clearTimeout(snapLockTimer)
|
||||
void saveToIndexedDB()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<div ref="rootRef" class="space-y-6">
|
||||
<DialogRoot v-model:open="pickerOpen" @update:open="handlePickerOpenChange">
|
||||
<div class="rounded-lg border bg-card p-4 shadow-sm">
|
||||
<label class="mb-2 block text-sm font-medium text-foreground">选择服务</label>
|
||||
<div class="rounded-lg border bg-card p-4 shadow-sm shrink-0">
|
||||
<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">
|
||||
<input :value="selectedServiceText" readonly placeholder="请点击右侧“浏览”选择服务"
|
||||
class="h-10 w-full rounded-md border bg-background px-3 text-sm text-foreground outline-none" />
|
||||
@@ -516,7 +596,7 @@ onBeforeUnmount(() => {
|
||||
<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">
|
||||
<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">
|
||||
<h4 class="text-base font-semibold">选择服务词典</h4>
|
||||
<DialogClose as-child>
|
||||
@@ -561,13 +641,16 @@ onBeforeUnmount(() => {
|
||||
</DialogPortal>
|
||||
</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">
|
||||
<h3 class="text-sm font-semibold text-foreground">咨询服务明细</h3>
|
||||
<div class="text-xs text-muted-foreground">按服务词典生成</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"
|
||||
:theme="myTheme" @cell-value-changed="handleCellValueChanged" :enableClipboard="true"
|
||||
:localeText="AG_GRID_LOCALE_CN" :tooltipShowDelay="500" :headerHeight="50" :undoRedoCellEditing="true"
|
||||
|
||||
Reference in new issue
Block a user