fix bug
This commit is contained in:
parent
1609f19b9c
commit
37f4a99914
@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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('排序完成')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
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: '造价金额(万元)',
|
headerName: '人员名称',
|
||||||
field: 'amount',
|
minWidth: 200,
|
||||||
minWidth: 170,
|
width: 220,
|
||||||
flex: 1, // 核心:开启弹性布局,自动占满剩余空间
|
pinned: 'left',
|
||||||
|
valueGetter: params => {
|
||||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
if (params.node?.rowPinned) return ''
|
||||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
return params.node?.group ? params.data?.groupName || '' : params.data?.majorName || ''
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
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"
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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 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,
|
||||||
minWidth: 170,
|
width: 240,
|
||||||
flex: 1, // 核心:开启弹性布局,自动占满剩余空间
|
pinned: 'left',
|
||||||
|
valueGetter: params => {
|
||||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
if (params.node?.rowPinned) return ''
|
||||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
return params.node?.group ? params.data?.groupName || '' : params.data?.majorName || ''
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
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"
|
||||||
|
|||||||
@ -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,45 +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,
|
||||||
minWidth: 170,
|
width: 240,
|
||||||
flex: 1, // 核心:开启弹性布局,自动占满剩余空间
|
pinned: 'left',
|
||||||
|
valueGetter: params => {
|
||||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
if (params.node?.rowPinned) return ''
|
||||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
return params.node?.group ? params.data?.groupName || '' : params.data?.majorName || ''
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headerName: '用地面积(亩)',
|
headerName: '工作量',
|
||||||
field: 'landArea',
|
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' : ''),
|
||||||
@ -208,26 +212,89 @@ 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: '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 => {
|
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"
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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
30
src/lib/diyAgGridTheme.ts
Normal 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"
|
||||||
|
});
|
||||||
78
src/sql.ts
78
src/sql.ts
@ -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 } },
|
||||||
|
];
|
||||||
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user