This commit is contained in:
wintsa 2026-03-20 18:08:36 +08:00
parent 3dce3646e1
commit c4d04cbee3
11 changed files with 187 additions and 40 deletions

View File

@ -11,7 +11,6 @@
"@internationalized/number": "^3.6.5", "@internationalized/number": "^3.6.5",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@vueuse/core": "^14.2.1", "@vueuse/core": "^14.2.1",
"ag-grid-community": "^35.1.0",
"ag-grid-enterprise": "^35.1.0", "ag-grid-enterprise": "^35.1.0",
"ag-grid-vue3": "^35.1.0", "ag-grid-vue3": "^35.1.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",

View File

@ -16,7 +16,6 @@
"@internationalized/number": "^3.6.5", "@internationalized/number": "^3.6.5",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@vueuse/core": "^14.2.1", "@vueuse/core": "^14.2.1",
"ag-grid-community": "^35.1.0",
"ag-grid-enterprise": "^35.1.0", "ag-grid-enterprise": "^35.1.0",
"ag-grid-vue3": "^35.1.0", "ag-grid-vue3": "^35.1.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onActivated, onBeforeUnmount, onMounted, ref } from 'vue' import { computed, onActivated, onBeforeUnmount, onMounted, ref } from 'vue'
import { AgGridVue } from 'ag-grid-vue3' import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef, ColGroupDef } from 'ag-grid-community' import type { ColDef, ColGroupDef, GridApi, GridReadyEvent } from 'ag-grid-community'
import { getMajorDictEntries, getServiceDictItemById, industryTypeList, isMajorIdInIndustryScope } from '@/sql' import { getMajorDictEntries, getServiceDictItemById, industryTypeList, isMajorIdInIndustryScope } from '@/sql'
import { myTheme, gridOptions, agGridWrapClass, agGridStyle } from '@/lib/diyAgGridOptions' import { myTheme, gridOptions, agGridWrapClass, agGridStyle } from '@/lib/diyAgGridOptions'
import { addNumbers, decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal' import { addNumbers, decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
@ -108,6 +108,7 @@ const consultCategoryFactorMap = ref<Map<string, number | null>>(new Map())
const majorFactorMap = ref<Map<string, number | null>>(new Map()) const majorFactorMap = ref<Map<string, number | null>>(new Map())
let factorDefaultsLoaded = false let factorDefaultsLoaded = false
const paneInstanceCreatedAt = Date.now() const paneInstanceCreatedAt = Date.now()
const gridApi = ref<GridApi<DetailRow> | null>(null)
const ONLY_COST_SCALE_ROW_ID = '__only-cost-scale-total__' const ONLY_COST_SCALE_ROW_ID = '__only-cost-scale-total__'
const industryNameMap = new Map( const industryNameMap = new Map(
industryTypeList.flatMap(item => [ industryTypeList.flatMap(item => [
@ -832,13 +833,14 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
headerName: '咨询分类系数', headerName: '咨询分类系数',
field: 'consultCategoryFactor', field: 'consultCategoryFactor',
colId: 'consultCategoryFactor', colId: 'consultCategoryFactor',
headerClass: 'ag-right-aligned-header',
minWidth: 80, minWidth: 80,
flex: 1, flex: 1,
editable: params => !params.node?.group && !params.node?.rowPinned, editable: params => !params.node?.group && !params.node?.rowPinned,
cellClass: params => cellClass: params =>
!params.node?.group && !params.node?.rowPinned !params.node?.group && !params.node?.rowPinned
? 'editable-cell-line' ? 'ag-right-aligned-cell editable-cell-line'
: '', : 'ag-right-aligned-cell',
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 === '')
@ -850,13 +852,14 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
headerName: '专业系数', headerName: '专业系数',
field: 'majorFactor', field: 'majorFactor',
colId: 'majorFactor', colId: 'majorFactor',
headerClass: 'ag-right-aligned-header',
minWidth: 80, minWidth: 80,
flex: 1, flex: 1,
editable: params => !params.node?.group && !params.node?.rowPinned, editable: params => !params.node?.group && !params.node?.rowPinned,
cellClass: params => cellClass: params =>
!params.node?.group && !params.node?.rowPinned !params.node?.group && !params.node?.rowPinned
? 'editable-cell-line' ? 'ag-right-aligned-cell editable-cell-line'
: '', : 'ag-right-aligned-cell',
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 === '')
@ -868,13 +871,14 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
headerName: '工作环节系数(编审系数)', headerName: '工作环节系数(编审系数)',
field: 'workStageFactor', field: 'workStageFactor',
colId: 'workStageFactor', colId: 'workStageFactor',
headerClass: 'ag-right-aligned-header',
minWidth: 80, minWidth: 80,
flex: 1, flex: 1,
editable: params => !params.node?.group && !params.node?.rowPinned, editable: params => !params.node?.group && !params.node?.rowPinned,
cellClass: params => cellClass: params =>
!params.node?.group && !params.node?.rowPinned !params.node?.group && !params.node?.rowPinned
? 'editable-cell-line' ? 'ag-right-aligned-cell editable-cell-line'
: '', : 'ag-right-aligned-cell',
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 === '')
@ -886,13 +890,14 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
headerName: '工作占比(%)', headerName: '工作占比(%)',
field: 'workRatio', field: 'workRatio',
colId: 'workRatio', colId: 'workRatio',
headerClass: 'ag-right-aligned-header',
minWidth: 80, minWidth: 80,
flex: 1, flex: 1,
editable: params => !params.node?.group && !params.node?.rowPinned, editable: params => !params.node?.group && !params.node?.rowPinned,
cellClass: params => cellClass: params =>
!params.node?.group && !params.node?.rowPinned !params.node?.group && !params.node?.rowPinned
? 'editable-cell-line' ? 'ag-right-aligned-cell editable-cell-line'
: '', : 'ag-right-aligned-cell',
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 === '')
@ -1065,7 +1070,13 @@ const saveToIndexedDB = async () => {
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows())), detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows())),
projectCount: getTargetProjectCount() projectCount: getTargetProjectCount()
} }
zxFwPricingStore.setServicePricingMethodState(props.contractId, props.serviceId, 'investScale', payload) zxFwPricingStore.setServicePricingMethodState(
props.contractId,
props.serviceId,
'investScale',
payload,
{ force: true }
)
const synced = await syncPricingTotalToZxFw({ const synced = await syncPricingTotalToZxFw({
contractId: props.contractId, contractId: props.contractId,
serviceId: props.serviceId, serviceId: props.serviceId,
@ -1274,6 +1285,10 @@ const handleCellValueChanged = () => {
void saveToIndexedDB() void saveToIndexedDB()
} }
const handleGridReady = (event: GridReadyEvent<DetailRow>) => {
gridApi.value = event.api
}
onMounted(async () => { onMounted(async () => {
await loadFromIndexedDB() await loadFromIndexedDB()
}) })
@ -1283,6 +1298,8 @@ onActivated(() => {
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
gridApi.value?.stopEditing()
gridApi.value = null
void saveToIndexedDB() void saveToIndexedDB()
}) })
const processCellForClipboard = (params: any) => { const processCellForClipboard = (params: any) => {
@ -1380,6 +1397,7 @@ const processCellFromClipboard = (params: any) => {
<AgGridVue :style="agGridStyle" :rowData="detailRows" :pinnedTopRowData="pinnedTopRowData" <AgGridVue :style="agGridStyle" :rowData="detailRows" :pinnedTopRowData="pinnedTopRowData"
:columnDefs="columnDefs" :autoGroupColumnDef="autoGroupColumnDef" :gridOptions="gridOptions" :theme="myTheme" :columnDefs="columnDefs" :autoGroupColumnDef="autoGroupColumnDef" :gridOptions="gridOptions" :theme="myTheme"
:animateRows="true" :animateRows="true"
@grid-ready="handleGridReady"
@cell-value-changed="handleCellValueChanged" :suppressColumnVirtualisation="true" @cell-value-changed="handleCellValueChanged" :suppressColumnVirtualisation="true"
:suppressRowVirtualisation="true" :cellSelection="{ handle: { mode: 'range' } }" :enableClipboard="true" :suppressRowVirtualisation="true" :cellSelection="{ handle: { mode: 'range' } }" :enableClipboard="true"
:localeText="AG_GRID_LOCALE_CN" :tooltipShowDelay="500" :headerHeight="50" :localeText="AG_GRID_LOCALE_CN" :tooltipShowDelay="500" :headerHeight="50"

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onActivated, onBeforeUnmount, onMounted, ref } from 'vue' import { computed, onActivated, onBeforeUnmount, onMounted, ref } from 'vue'
import { AgGridVue } from 'ag-grid-vue3' import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef, ColGroupDef } from 'ag-grid-community' import type { ColDef, ColGroupDef, GridApi, GridReadyEvent } from 'ag-grid-community'
import { getMajorDictEntries, getServiceDictItemById, industryTypeList, isMajorIdInIndustryScope } from '@/sql' import { getMajorDictEntries, getServiceDictItemById, industryTypeList, isMajorIdInIndustryScope } from '@/sql'
import { myTheme, gridOptions, agGridWrapClass, agGridStyle } from '@/lib/diyAgGridOptions' import { myTheme, gridOptions, agGridWrapClass, agGridStyle } from '@/lib/diyAgGridOptions'
import { addNumbers, decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal' import { addNumbers, decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
@ -108,6 +108,7 @@ const consultCategoryFactorMap = ref<Map<string, number | null>>(new Map())
const majorFactorMap = ref<Map<string, number | null>>(new Map()) const majorFactorMap = ref<Map<string, number | null>>(new Map())
let factorDefaultsLoaded = false let factorDefaultsLoaded = false
const paneInstanceCreatedAt = Date.now() const paneInstanceCreatedAt = Date.now()
const gridApi = ref<GridApi<DetailRow> | null>(null)
const industryNameMap = new Map( const industryNameMap = new Map(
industryTypeList.flatMap(item => [ industryTypeList.flatMap(item => [
[String(item.id).trim(), item.name], [String(item.id).trim(), item.name],
@ -708,10 +709,14 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
headerName: '咨询分类系数', headerName: '咨询分类系数',
field: 'consultCategoryFactor', field: 'consultCategoryFactor',
colId: 'consultCategoryFactor', colId: 'consultCategoryFactor',
headerClass: 'ag-right-aligned-header',
minWidth: 80, minWidth: 80,
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
? 'ag-right-aligned-cell editable-cell-line'
: 'ag-right-aligned-cell',
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 === '')
@ -723,10 +728,14 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
headerName: '专业系数', headerName: '专业系数',
field: 'majorFactor', field: 'majorFactor',
colId: 'majorFactor', colId: 'majorFactor',
headerClass: 'ag-right-aligned-header',
minWidth: 80, minWidth: 80,
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
? 'ag-right-aligned-cell editable-cell-line'
: 'ag-right-aligned-cell',
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 === '')
@ -738,10 +747,14 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
headerName: '工作环节系数(编审系数)', headerName: '工作环节系数(编审系数)',
field: 'workStageFactor', field: 'workStageFactor',
colId: 'workStageFactor', colId: 'workStageFactor',
headerClass: 'ag-right-aligned-header',
minWidth: 80, minWidth: 80,
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
? 'ag-right-aligned-cell editable-cell-line'
: 'ag-right-aligned-cell',
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 === '')
@ -753,10 +766,14 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
headerName: '工作占比(%)', headerName: '工作占比(%)',
field: 'workRatio', field: 'workRatio',
colId: 'workRatio', colId: 'workRatio',
headerClass: 'ag-right-aligned-header',
minWidth: 80, minWidth: 80,
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
? 'ag-right-aligned-cell editable-cell-line'
: 'ag-right-aligned-cell',
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 === '')
@ -930,7 +947,13 @@ const saveToIndexedDB = async () => {
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows())), detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows())),
projectCount: getTargetProjectCount() projectCount: getTargetProjectCount()
} }
zxFwPricingStore.setServicePricingMethodState(props.contractId, props.serviceId, 'landScale', payload) zxFwPricingStore.setServicePricingMethodState(
props.contractId,
props.serviceId,
'landScale',
payload,
{ force: true }
)
const synced = await syncPricingTotalToZxFw({ const synced = await syncPricingTotalToZxFw({
contractId: props.contractId, contractId: props.contractId,
serviceId: props.serviceId, serviceId: props.serviceId,
@ -1111,6 +1134,10 @@ const handleCellValueChanged = () => {
void saveToIndexedDB() void saveToIndexedDB()
} }
const handleGridReady = (event: GridReadyEvent<DetailRow>) => {
gridApi.value = event.api
}
onMounted(async () => { onMounted(async () => {
await loadFromIndexedDB() await loadFromIndexedDB()
}) })
@ -1120,6 +1147,8 @@ onActivated(() => {
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
gridApi.value?.stopEditing()
gridApi.value = null
void saveToIndexedDB() void saveToIndexedDB()
}) })
const processCellForClipboard = (params: any) => { const processCellForClipboard = (params: any) => {
@ -1219,6 +1248,7 @@ const processCellFromClipboard = (params: any) => {
<AgGridVue :style="agGridStyle" :rowData="detailRows" :pinnedTopRowData="pinnedTopRowData" <AgGridVue :style="agGridStyle" :rowData="detailRows" :pinnedTopRowData="pinnedTopRowData"
:columnDefs="columnDefs" :autoGroupColumnDef="autoGroupColumnDef" :gridOptions="gridOptions" :theme="myTheme" :columnDefs="columnDefs" :autoGroupColumnDef="autoGroupColumnDef" :gridOptions="gridOptions" :theme="myTheme"
:animateRows="true" :animateRows="true"
@grid-ready="handleGridReady"
@cell-value-changed="handleCellValueChanged" :suppressColumnVirtualisation="true" @cell-value-changed="handleCellValueChanged" :suppressColumnVirtualisation="true"
:suppressRowVirtualisation="true" :cellSelection="{ handle: { mode: 'range' } }" :enableClipboard="true" :suppressRowVirtualisation="true" :cellSelection="{ handle: { mode: 'range' } }" :enableClipboard="true"
:localeText="AG_GRID_LOCALE_CN" :tooltipShowDelay="500" :headerHeight="50" :localeText="AG_GRID_LOCALE_CN" :tooltipShowDelay="500" :headerHeight="50"

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue' import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { AgGridVue } from 'ag-grid-vue3' import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef } from 'ag-grid-community' import type { ColDef, GridApi, GridReadyEvent } from 'ag-grid-community'
import { taskList } from '@/sql' import { taskList } from '@/sql'
import { myTheme, gridOptions, agGridWrapClass, agGridStyle } from '@/lib/diyAgGridOptions' import { myTheme, gridOptions, agGridWrapClass, agGridStyle } from '@/lib/diyAgGridOptions'
import { decimalAggSum, roundTo, sumByNumber, toDecimal } from '@/lib/decimal' import { decimalAggSum, roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
@ -49,6 +49,7 @@ const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
const consultCategoryFactorMap = ref<Map<string, number | null>>(new Map()) const consultCategoryFactorMap = ref<Map<string, number | null>>(new Map())
let factorDefaultsLoaded = false let factorDefaultsLoaded = false
const paneInstanceCreatedAt = Date.now() const paneInstanceCreatedAt = Date.now()
const gridApi = ref<GridApi<DetailRow> | null>(null)
const getDefaultConsultCategoryFactor = () => const getDefaultConsultCategoryFactor = () =>
consultCategoryFactorMap.value.get(String(props.serviceId)) ?? null consultCategoryFactorMap.value.get(String(props.serviceId)) ?? null
@ -183,6 +184,7 @@ const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] =>
return buildDefaultRows().map(row => { return buildDefaultRows().map(row => {
const fromDb = dbValueMap.get(row.id) const fromDb = dbValueMap.get(row.id)
if (!fromDb) return row if (!fromDb) return row
const hasRemark = Object.prototype.hasOwnProperty.call(fromDb, 'remark')
return { return {
...row, ...row,
@ -193,7 +195,7 @@ const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] =>
consultCategoryFactor: consultCategoryFactor:
typeof fromDb.consultCategoryFactor === 'number' ? fromDb.consultCategoryFactor : null, typeof fromDb.consultCategoryFactor === 'number' ? fromDb.consultCategoryFactor : null,
serviceFee: typeof fromDb.serviceFee === 'number' ? fromDb.serviceFee : null, serviceFee: typeof fromDb.serviceFee === 'number' ? fromDb.serviceFee : null,
remark: typeof fromDb.remark === 'string' ? fromDb.remark : '' remark: typeof fromDb.remark === 'string' ? fromDb.remark : hasRemark ? '' : row.remark
} }
}) })
} }
@ -270,7 +272,9 @@ const columnDefs: ColDef<DetailRow>[] = [
minWidth: 150, minWidth: 150,
width: 220, width: 220,
pinned: 'left', pinned: 'left',
wrapText: true,
autoHeight: true, autoHeight: true,
cellStyle: { whiteSpace: 'normal', lineHeight: '1.4' },
spanRows: true, spanRows: true,
valueFormatter: params => (params.node?.rowPinned ? '' : params.value || '') valueFormatter: params => (params.node?.rowPinned ? '' : params.value || '')
@ -445,7 +449,13 @@ const saveToIndexedDB = async () => {
const payload = { const payload = {
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows())) detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows()))
} }
zxFwPricingStore.setServicePricingMethodState(props.contractId, props.serviceId, 'workload', payload) zxFwPricingStore.setServicePricingMethodState(
props.contractId,
props.serviceId,
'workload',
payload,
{ force: true }
)
const synced = await syncPricingTotalToZxFw({ const synced = await syncPricingTotalToZxFw({
contractId: props.contractId, contractId: props.contractId,
serviceId: props.serviceId, serviceId: props.serviceId,
@ -489,11 +499,17 @@ const handleCellValueChanged = () => {
void saveToIndexedDB() void saveToIndexedDB()
} }
const handleGridReady = (event: GridReadyEvent<DetailRow>) => {
gridApi.value = event.api
}
onMounted(async () => { onMounted(async () => {
await loadFromIndexedDB() await loadFromIndexedDB()
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
gridApi.value?.stopEditing()
gridApi.value = null
void saveToIndexedDB() void saveToIndexedDB()
}) })
const processCellForClipboard = (params: any) => { const processCellForClipboard = (params: any) => {
@ -542,6 +558,7 @@ const mydiyTheme = myTheme.withParams({
:columnDefs="columnDefs" :gridOptions="gridOptions" :theme="mydiyTheme" :treeData="false" :columnDefs="columnDefs" :gridOptions="gridOptions" :theme="mydiyTheme" :treeData="false"
:animateRows="true" :animateRows="true"
:enableCellSpan="true" :enableCellSpan="true"
@grid-ready="handleGridReady"
@cell-value-changed="handleCellValueChanged" :suppressColumnVirtualisation="true" @cell-value-changed="handleCellValueChanged" :suppressColumnVirtualisation="true"
:suppressRowVirtualisation="true" :suppressRowVirtualisation="true"

View File

@ -1,7 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from 'vue' import { computed, nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from 'vue'
import { AgGridVue } from 'ag-grid-vue3' import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef, ColGroupDef, GridApi, GridReadyEvent } from 'ag-grid-community' import type {
ColDef,
ColGroupDef,
FirstDataRenderedEvent,
GridApi,
GridReadyEvent,
RowDataUpdatedEvent
} from 'ag-grid-community'
import { expertList } from '@/sql' import { expertList } from '@/sql'
import { myTheme, gridOptions, agGridWrapClass, agGridStyle } from '@/lib/diyAgGridOptions' import { myTheme, gridOptions, agGridWrapClass, agGridStyle } from '@/lib/diyAgGridOptions'
import { decimalAggSum, roundTo, sumByNumber, toDecimal } from '@/lib/decimal' import { decimalAggSum, roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
@ -28,7 +35,6 @@ interface DetailRow {
interface GridState { interface GridState {
detailRows: DetailRow[] detailRows: DetailRow[]
} }
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
storageKey: string storageKey: string
@ -369,6 +375,7 @@ const columnDefs: (ColDef<DetailRow> | ColGroupDef<DetailRow>)[] = [
tooltipField: 'expertName', tooltipField: 'expertName',
wrapText: true, wrapText: true,
autoHeight: true, autoHeight: true,
cellClass: 'hourly-fee-name-cell',
cellStyle: { whiteSpace: 'normal', lineHeight: '1.2' }, cellStyle: { whiteSpace: 'normal', lineHeight: '1.2' },
valueFormatter: params => (params.node?.rowPinned ? '' : params.value || '') valueFormatter: params => (params.node?.rowPinned ? '' : params.value || '')
}, },
@ -465,13 +472,20 @@ const saveToIndexedDB = async () => {
} }
if (useServicePricingState.value && serviceMethod.value) { if (useServicePricingState.value && serviceMethod.value) {
zxFwPricingStore.setServicePricingMethodState(props.contractId!, props.serviceId!, serviceMethod.value, payload) zxFwPricingStore.setServicePricingMethodState(
props.contractId!,
props.serviceId!,
serviceMethod.value,
payload,
{ force: true }
)
} else if (useHtMethodState.value) { } else if (useHtMethodState.value) {
zxFwPricingStore.setHtFeeMethodState( zxFwPricingStore.setHtFeeMethodState(
props.htMainStorageKey!, props.htMainStorageKey!,
props.htRowId!, props.htRowId!,
props.htMethodType!, props.htMethodType!,
payload payload,
{ force: true }
) )
} else { } else {
zxFwPricingStore.setKeyState(props.storageKey, payload) zxFwPricingStore.setKeyState(props.storageKey, payload)
@ -535,16 +549,26 @@ const handleGridReady = (event: GridReadyEvent<DetailRow>) => {
} }
let autoHeightSyncTimer: ReturnType<typeof setTimeout> | null = null let autoHeightSyncTimer: ReturnType<typeof setTimeout> | null = null
const forceRefreshCellsOnLiveApi = () => {
// AG Grid
setTimeout(() => {
const liveApi = gridApi.value
if (!liveApi || liveApi.isDestroyed?.()) return
liveApi.refreshCells({ force: true })
liveApi.redrawRows()
}, 16)
}
const syncAutoRowHeights = async () => { const syncAutoRowHeights = async () => {
await nextTick() await nextTick()
const api = gridApi.value const api = gridApi.value
if (!api) return if (!api || api.isDestroyed?.()) return
api.resetRowHeights() api.resetRowHeights()
api.onRowHeightChanged() api.onRowHeightChanged()
api.refreshCells({ force: true }) api.refreshCells({ force: true })
api.redrawRows() api.redrawRows()
forceRefreshCellsOnLiveApi()
} }
const scheduleAutoRowHeights = () => { const scheduleAutoRowHeights = () => {
if (autoHeightSyncTimer) clearTimeout(autoHeightSyncTimer) if (autoHeightSyncTimer) clearTimeout(autoHeightSyncTimer)
autoHeightSyncTimer = setTimeout(() => { autoHeightSyncTimer = setTimeout(() => {
@ -561,6 +585,14 @@ const onColumnResized = () => {
scheduleAutoRowHeights() scheduleAutoRowHeights()
} }
const onFirstDataRendered = (_event: FirstDataRenderedEvent<DetailRow>) => {
scheduleAutoRowHeights()
}
const onRowDataUpdated = (_event: RowDataUpdatedEvent<DetailRow>) => {
scheduleAutoRowHeights()
}
const processCellForClipboard = (params: any) => { const processCellForClipboard = (params: any) => {
if (Array.isArray(params.value)) return JSON.stringify(params.value) if (Array.isArray(params.value)) return JSON.stringify(params.value)
return params.value return params.value
@ -648,6 +680,8 @@ onBeforeUnmount(() => {
:undoRedoCellEditing="true" :undoRedoCellEditing="true"
:undoRedoCellEditingLimit="20" :undoRedoCellEditingLimit="20"
@grid-ready="handleGridReady" @grid-ready="handleGridReady"
@first-data-rendered="onFirstDataRendered"
@row-data-updated="onRowDataUpdated"
@grid-size-changed="onGridSizeChanged" @grid-size-changed="onGridSizeChanged"
@column-resized="onColumnResized" @column-resized="onColumnResized"
/> />
@ -655,3 +689,18 @@ onBeforeUnmount(() => {
</div> </div>
</div> </div>
</template> </template>
<style scoped>
:deep(.hourly-fee-name-cell.ag-cell-auto-height) {
display: flex;
align-items: center;
}
:deep(.hourly-fee-name-cell.ag-cell-auto-height .ag-cell-wrapper),
:deep(.hourly-fee-name-cell.ag-cell-auto-height .ag-cell-value) {
display: flex;
align-items: center;
width: 100%;
white-space: normal;
}
</style>

View File

@ -228,7 +228,8 @@ const saveToIndexedDB = async () => {
props.htMainStorageKey!, props.htMainStorageKey!,
props.htRowId!, props.htRowId!,
props.htMethodType!, props.htMethodType!,
payload payload,
{ force: true }
) )
} else { } else {
zxFwPricingStore.setKeyState(props.storageKey, payload) zxFwPricingStore.setKeyState(props.storageKey, payload)
@ -430,6 +431,7 @@ watch(
) )
onBeforeUnmount(() => { onBeforeUnmount(() => {
gridApi.value?.stopEditing()
gridApi.value = null gridApi.value = null
void saveToIndexedDB() void saveToIndexedDB()
}) })

View File

@ -664,6 +664,7 @@ watch(
) )
onBeforeUnmount(() => { onBeforeUnmount(() => {
gridApi.value?.stopEditing()
saveToStore() saveToStore()
}) })
const handleDeleteConfirmOpenChange = (open: boolean) => { const handleDeleteConfirmOpenChange = (open: boolean) => {
@ -804,4 +805,19 @@ const confirmDeleteRow = () => {
min-width: 0; min-width: 0;
word-break: break-word; word-break: break-word;
} }
:deep(.work-content-main-cell.ag-cell-auto-height),
:deep(.remark-wrap-cell.ag-cell-auto-height) {
display: flex;
align-items: center;
}
:deep(.work-content-main-cell.ag-cell-auto-height .ag-cell-wrapper),
:deep(.work-content-main-cell.ag-cell-auto-height .ag-cell-value),
:deep(.remark-wrap-cell.ag-cell-auto-height .ag-cell-wrapper),
:deep(.remark-wrap-cell.ag-cell-auto-height .ag-cell-value) {
display: flex;
align-items: center;
width: 100%;
}
</style> </style>

View File

@ -5,6 +5,7 @@ import type { ColDef, FirstDataRenderedEvent, GridApi, GridReadyEvent, GridSizeC
import { myTheme, gridOptions, agGridWrapClass, agGridStyle } from '@/lib/diyAgGridOptions' import { myTheme, gridOptions, agGridWrapClass, agGridStyle } from '@/lib/diyAgGridOptions'
import { parseNumberOrNull } from '@/lib/number' import { parseNumberOrNull } from '@/lib/number'
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale' import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
import { useKvStore } from '@/pinia/kv' import { useKvStore } from '@/pinia/kv'
interface DictItem { interface DictItem {
@ -42,6 +43,7 @@ const props = defineProps<{
initBudgetValueFromStandard?: boolean initBudgetValueFromStandard?: boolean
}>() }>()
const zxFwPricingStore = useZxFwPricingStore()
const kvStore = useKvStore() const kvStore = useKvStore()
const detailRows = ref<FactorRow[]>([]) const detailRows = ref<FactorRow[]>([])
const gridApi = ref<GridApi<FactorRow> | null>(null) const gridApi = ref<GridApi<FactorRow> | null>(null)
@ -228,7 +230,7 @@ const saveToIndexedDB = async () => {
const payload: GridState = { const payload: GridState = {
detailRows: JSON.parse(JSON.stringify(detailRows.value)) detailRows: JSON.parse(JSON.stringify(detailRows.value))
} }
await kvStore.setItem(props.storageKey, payload) zxFwPricingStore.setKeyState(props.storageKey, payload)
} catch (error) { } catch (error) {
console.error('saveToIndexedDB failed:', error) console.error('saveToIndexedDB failed:', error)
} }
@ -236,9 +238,18 @@ const saveToIndexedDB = async () => {
const loadGridState = async (storageKey: string): Promise<GridState | null> => { const loadGridState = async (storageKey: string): Promise<GridState | null> => {
if (!storageKey) return null if (!storageKey) return null
const data = await kvStore.getItem<GridState>(storageKey) const piniaData = await zxFwPricingStore.loadKeyState<GridState>(storageKey)
if (!data?.detailRows || !Array.isArray(data.detailRows)) return null if (piniaData?.detailRows && Array.isArray(piniaData.detailRows)) return piniaData
return data
// kvStore pinia keyed state
const legacyData = await kvStore.getItem<GridState>(storageKey)
if (!legacyData?.detailRows || !Array.isArray(legacyData.detailRows)) return null
zxFwPricingStore.setKeyState(storageKey, legacyData, { force: true })
return legacyData
}
const handleGridReady = (event: GridReadyEvent<FactorRow>) => {
gridApi.value = event.api
} }
const loadFromIndexedDB = async () => { const loadFromIndexedDB = async () => {
@ -312,6 +323,7 @@ watch(
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (gridPersistTimer) clearTimeout(gridPersistTimer) if (gridPersistTimer) clearTimeout(gridPersistTimer)
gridApi.value?.stopEditing()
gridApi.value = null gridApi.value = null
void saveToIndexedDB() void saveToIndexedDB()
}) })
@ -348,6 +360,7 @@ onBeforeUnmount(() => {
:processCellFromClipboard="processCellFromClipboard" :processCellFromClipboard="processCellFromClipboard"
:undoRedoCellEditing="true" :undoRedoCellEditing="true"
:undoRedoCellEditingLimit="20" :undoRedoCellEditingLimit="20"
@grid-ready="handleGridReady"
/> />
</div> </div>

View File

@ -1,5 +1,6 @@
import { import {
CellStyleModule, AggregationModule,ServerSideRowModelApiModule ,UndoRedoEditModule, CellStyleModule,
ClientSideRowModelModule, ClientSideRowModelModule,
ColumnAutoSizeModule, ColumnAutoSizeModule,
LargeTextEditorModule, LargeTextEditorModule,
@ -10,11 +11,7 @@ import {
RowAutoHeightModule, RowAutoHeightModule,
TextEditorModule, TextEditorModule,
TooltipModule,ClientSideRowModelApiModule , TooltipModule,ClientSideRowModelApiModule ,
UndoRedoEditModule,RenderApiModule ,ColumnApiModule ,CellSpanModule ,RowStyleModule ,RowSelectionModule RenderApiModule ,ColumnApiModule ,CellSpanModule ,RowStyleModule ,RowSelectionModule ,
} from 'ag-grid-community'
import {
AggregationModule,ServerSideRowModelApiModule ,
CellSelectionModule, CellSelectionModule,
ClipboardModule, ClipboardModule,
LicenseManager, LicenseManager,

View File

@ -219,7 +219,14 @@ input[inputmode='numeric'] {
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
.ag-cell{display: flex;} .ag-theme-quartz .ag-cell:not(.ag-cell-auto-height) {
display: flex;
align-items: center;
}
.ag-theme-quartz .ag-cell.ag-cell-auto-height {
display: block;
}
.zxfw-action-group { .zxfw-action-group {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;