1182 lines
40 KiB
Vue
1182 lines
40 KiB
Vue
<script setup lang="ts">
|
||
import { computed, defineComponent, h, nextTick, onActivated, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
|
||
import type { PropType } from 'vue'
|
||
import { AgGridVue } from 'ag-grid-vue3'
|
||
import type { ColDef, GridApi, GridOptions, GridReadyEvent, ICellRendererParams } from 'ag-grid-community'
|
||
import type { FirstDataRenderedEvent } from 'ag-grid-community'
|
||
import { myTheme, gridOptions, agGridWrapClass, agGridStyle } from '@/lib/diyAgGridOptions'
|
||
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
|
||
import { addNumbers, roundTo } from '@/lib/decimal'
|
||
import { parseNumberOrNull } from '@/lib/number'
|
||
import { formatThousands, formatThousandsFlexible } from '@/lib/numberFormat'
|
||
import {
|
||
ensurePricingMethodDetailRowsForServices,
|
||
getPricingMethodTotalsForService,
|
||
getPricingMethodTotalsForServices,
|
||
type PricingMethodTotals
|
||
} from '@/lib/pricingMethodTotals'
|
||
import { Pencil, Eraser, Trash2 } from 'lucide-vue-next'
|
||
import {
|
||
AlertDialogAction,
|
||
AlertDialogCancel,
|
||
AlertDialogContent,
|
||
AlertDialogDescription,
|
||
AlertDialogOverlay,
|
||
AlertDialogPortal,
|
||
AlertDialogRoot,
|
||
AlertDialogTitle,
|
||
} from 'reka-ui'
|
||
import { Button } from '@/components/ui/button'
|
||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||
import { getServiceDictEntries, isIndustryEnabledByType, getIndustryTypeValue } from '@/sql'
|
||
import { useTabStore } from '@/pinia/tab'
|
||
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||
import { useKvStore } from '@/pinia/kv'
|
||
import ServiceCheckboxSelector from '@/components/shared/ServiceCheckboxSelector.vue'
|
||
|
||
interface ServiceItem {
|
||
id: string
|
||
code: string
|
||
name: string
|
||
type: ServiceMethodType
|
||
}
|
||
|
||
interface DetailRow {
|
||
id: string
|
||
code: string
|
||
name: string
|
||
process: number | null
|
||
investScale: number | null
|
||
landScale: number | null
|
||
workload: number | null
|
||
hourly: number | null
|
||
subtotal?: number | null
|
||
finalFee?: number | null
|
||
actions?: unknown
|
||
}
|
||
|
||
interface ZxFwViewState {
|
||
selectedIds?: string[]
|
||
selectedCodes?: string[]
|
||
detailRows: DetailRow[]
|
||
}
|
||
|
||
interface XmBaseInfoState {
|
||
projectIndustry?: string
|
||
}
|
||
|
||
interface ServiceMethodType {
|
||
scale?: boolean | null
|
||
onlyCostScale?: boolean | null
|
||
amount?: boolean | null
|
||
workDay?: boolean | null
|
||
}
|
||
|
||
const props = defineProps<{
|
||
contractId: string
|
||
contractName?: string
|
||
projectInfoKey?: string
|
||
}>()
|
||
const tabStore = useTabStore()
|
||
const zxFwPricingStore = useZxFwPricingStore()
|
||
const kvStore = useKvStore()
|
||
const PROJECT_INFO_KEY = computed(() => props.projectInfoKey || 'xm-base-info-v1')
|
||
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
|
||
const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
|
||
const PRICING_CLEAR_SKIP_TTL_MS = 5000
|
||
const PRICING_TOTALS_OPTIONS = { excludeInvestmentCostAndAreaRows: true } as const
|
||
const projectIndustry = ref('')
|
||
|
||
type ServiceListItem = {
|
||
code?: string
|
||
ref?: string
|
||
name: string
|
||
defCoe: number | null
|
||
isRoad?: boolean
|
||
isRailway?: boolean
|
||
isWaterway?: boolean
|
||
scale?: boolean | null
|
||
onlyCostScale?: boolean | null
|
||
amount?: boolean | null
|
||
workDay?: boolean | null
|
||
}
|
||
|
||
/** 仅保留明确的 boolean,其他值统一转 null。 */
|
||
const toNullableBoolean = (value: unknown): boolean | null =>
|
||
typeof value === 'boolean' ? value : null
|
||
|
||
/** 解析方法开关,未配置时返回默认值。 */
|
||
const resolveMethodEnabled = (value: boolean | null | undefined, fallback = true) =>
|
||
typeof value === 'boolean' ? value : fallback
|
||
|
||
const defaultServiceMethodType = {
|
||
scale: true,
|
||
onlyCostScale: false,
|
||
amount: true,
|
||
workDay: true
|
||
}
|
||
|
||
const serviceDict = computed<ServiceItem[]>(() => {
|
||
const industry = projectIndustry.value
|
||
if (!industry) return []
|
||
const filteredEntries = getServiceDictEntries()
|
||
.map(({ id, item }) => ({ id, item: item as ServiceListItem }))
|
||
.filter(({ item }) => {
|
||
const itemCode = item?.code || item?.ref
|
||
return Boolean(itemCode && item?.name) && item.defCoe !== null && isIndustryEnabledByType(item, getIndustryTypeValue(industry))
|
||
|
||
})
|
||
return filteredEntries.map(({ id, item }) => ({
|
||
id,
|
||
code: item.code || item.ref || '',
|
||
name: item.name,
|
||
type: {
|
||
scale: toNullableBoolean(item.scale),
|
||
onlyCostScale: toNullableBoolean(item.onlyCostScale),
|
||
amount: toNullableBoolean(item.amount),
|
||
workDay: toNullableBoolean(item.workDay)
|
||
}
|
||
}))
|
||
})
|
||
|
||
const serviceById = computed(() => new Map(serviceDict.value.map(item => [item.id, item])))
|
||
const serviceIdByCode = computed(() => new Map(serviceDict.value.map(item => [item.code, item.id])))
|
||
const serviceIdSignature = computed(() => serviceDict.value.map(item => item.id).join('|'))
|
||
const fixedBudgetRow: Pick<DetailRow, 'id' | 'code' | 'name'> = { id: 'fixed-budget-c', code: '', name: '小计' }
|
||
|
||
/** 判断是否固定汇总行(小计行)。 */
|
||
const isFixedRow = (row?: DetailRow | null) => row?.id === fixedBudgetRow.id
|
||
|
||
/**
|
||
* 统一把 store/raw row 转成页面使用的 DetailRow 结构。
|
||
* 被 detailRows 计算属性和 getCurrentContractState 复用,避免重复映射逻辑。
|
||
*/
|
||
const normalizeDetailRow = (row: Partial<DetailRow>): DetailRow => ({
|
||
id: String(row.id || ''),
|
||
code: row.code || '',
|
||
name: row.name || '',
|
||
process: String(row.id || '') === fixedBudgetRow.id ? null : (Number(row.process) === 1 ? 1 : 0),
|
||
investScale: typeof row.investScale === 'number' ? row.investScale : null,
|
||
landScale: typeof row.landScale === 'number' ? row.landScale : null,
|
||
workload: typeof row.workload === 'number' ? row.workload : null,
|
||
hourly: typeof row.hourly === 'number' ? row.hourly : null,
|
||
subtotal: typeof row.subtotal === 'number' ? row.subtotal : null,
|
||
finalFee: typeof row.finalFee === 'number' ? row.finalFee : null,
|
||
actions: row.actions
|
||
})
|
||
|
||
const selectedIds = computed<string[]>(() => zxFwPricingStore.contracts[props.contractId]?.selectedIds || [])
|
||
const detailRows = computed<DetailRow[]>(() =>
|
||
(zxFwPricingStore.contracts[props.contractId]?.detailRows || []).map(row => normalizeDetailRow(row as Partial<DetailRow>))
|
||
)
|
||
|
||
/**
|
||
* 获取当前合同状态的“可变副本”。
|
||
* 所有写回前都先读这里,避免直接修改 store 引用对象。
|
||
*/
|
||
const getCurrentContractState = (): ZxFwViewState => {
|
||
const current = zxFwPricingStore.getContractState(props.contractId)
|
||
if (current) {
|
||
return {
|
||
selectedIds: Array.isArray(current.selectedIds) ? [...current.selectedIds] : [],
|
||
selectedCodes: Array.isArray(current.selectedCodes) ? [...current.selectedCodes] : [],
|
||
detailRows: (current.detailRows || []).map(row => normalizeDetailRow(row as Partial<DetailRow>))
|
||
}
|
||
}
|
||
return {
|
||
selectedIds: [],
|
||
detailRows: []
|
||
}
|
||
}
|
||
|
||
/** 统一写回当前合同状态到 store。 */
|
||
const setCurrentContractState = async (nextState: ZxFwViewState) => {
|
||
await zxFwPricingStore.setContractState(props.contractId, nextState)
|
||
}
|
||
|
||
const gridApi = shallowRef<GridApi<DetailRow> | null>(null)
|
||
/** 记录 grid api,供编辑后局部刷新固定行使用。 */
|
||
const onGridReady = (event: GridReadyEvent<DetailRow>) => {
|
||
gridApi.value = event.api
|
||
scheduleAutoRowHeights()
|
||
}
|
||
|
||
let autoHeightSyncTimer: ReturnType<typeof setTimeout> | null = null
|
||
const syncAutoRowHeights = async () => {
|
||
await nextTick()
|
||
const api = gridApi.value
|
||
if (!api) return
|
||
api.resetRowHeights()
|
||
api.onRowHeightChanged()
|
||
}
|
||
|
||
const scheduleAutoRowHeights = () => {
|
||
if (autoHeightSyncTimer) clearTimeout(autoHeightSyncTimer)
|
||
autoHeightSyncTimer = setTimeout(() => {
|
||
autoHeightSyncTimer = null
|
||
void syncAutoRowHeights()
|
||
}, 0)
|
||
}
|
||
|
||
const onFirstDataRendered = (_event: FirstDataRenderedEvent<DetailRow>) => {
|
||
scheduleAutoRowHeights()
|
||
}
|
||
|
||
const clearConfirmOpen = ref(false)
|
||
const pendingClearServiceId = ref<string | null>(null)
|
||
const deleteConfirmOpen = ref(false)
|
||
const pendingDeleteServiceId = ref<string | null>(null)
|
||
|
||
const pendingClearServiceName = computed(() => {
|
||
if (!pendingClearServiceId.value) return ''
|
||
const row = detailRows.value.find(item => item.id === pendingClearServiceId.value)
|
||
if (row) return `${row.code}${row.name}`
|
||
const dict = serviceById.value.get(pendingClearServiceId.value)
|
||
if (dict) return `${dict.code}${dict.name}`
|
||
return pendingClearServiceId.value
|
||
})
|
||
|
||
const pendingDeleteServiceName = computed(() => {
|
||
if (!pendingDeleteServiceId.value) return ''
|
||
const row = detailRows.value.find(item => item.id === pendingDeleteServiceId.value)
|
||
if (row) return `${row.code}${row.name}`
|
||
const dict = serviceById.value.get(pendingDeleteServiceId.value)
|
||
if (dict) return `${dict.code}${dict.name}`
|
||
return pendingDeleteServiceId.value
|
||
})
|
||
|
||
/** 清空确认框开关回调。 */
|
||
const handleClearConfirmOpenChange = (open: boolean) => {
|
||
clearConfirmOpen.value = open
|
||
}
|
||
|
||
/** 删除确认框开关回调。 */
|
||
const handleDeleteConfirmOpenChange = (open: boolean) => {
|
||
deleteConfirmOpen.value = open
|
||
}
|
||
|
||
/** 触发“恢复默认”二次确认。 */
|
||
const requestClearRow = (row: DetailRow) => {
|
||
if (isFixedRow(row)) return
|
||
pendingClearServiceId.value = row.id
|
||
clearConfirmOpen.value = true
|
||
}
|
||
|
||
/** 执行“恢复默认”并关闭确认框。 */
|
||
const confirmClearRow = async () => {
|
||
const id = pendingClearServiceId.value
|
||
if (!id) return
|
||
const row = detailRows.value.find(item => item.id === id)
|
||
if (!row || isFixedRow(row)) {
|
||
clearConfirmOpen.value = false
|
||
pendingClearServiceId.value = null
|
||
return
|
||
}
|
||
await clearRowValues(row)
|
||
clearConfirmOpen.value = false
|
||
pendingClearServiceId.value = null
|
||
}
|
||
|
||
/** 触发“删除服务”二次确认。 */
|
||
const requestDeleteRow = (row: DetailRow) => {
|
||
if (isFixedRow(row)) return
|
||
pendingDeleteServiceId.value = row.id
|
||
deleteConfirmOpen.value = true
|
||
}
|
||
|
||
/** 执行删除并关闭确认框。 */
|
||
const confirmDeleteRow = async () => {
|
||
const id = pendingDeleteServiceId.value
|
||
if (!id) return
|
||
const row = detailRows.value.find(item => item.id === id)
|
||
if (!row || isFixedRow(row)) {
|
||
deleteConfirmOpen.value = false
|
||
pendingDeleteServiceId.value = null
|
||
return
|
||
}
|
||
await removeRow(row)
|
||
deleteConfirmOpen.value = false
|
||
pendingDeleteServiceId.value = null
|
||
}
|
||
|
||
|
||
/** 表格数字输入解析器(保留 3 位小数)。 */
|
||
const numericParser = (newValue: any): number | null => {
|
||
return parseNumberOrNull(newValue, { precision: 3 })
|
||
}
|
||
|
||
/** 类型守卫:有限数字。 */
|
||
const isFiniteNumberValue = (value: unknown): value is number =>
|
||
typeof value === 'number' && Number.isFinite(value)
|
||
|
||
/** 可空数字求和:全为空返回 null。 */
|
||
const sumNullableNumbers = (values: Array<number | null | undefined>): number | null => {
|
||
const validValues = values.filter(isFiniteNumberValue)
|
||
if (validValues.length === 0) return null
|
||
return addNumbers(...validValues)
|
||
}
|
||
|
||
const isSameNullableNumber = (a: number | null | undefined, b: number | null | undefined, precision = 3) => {
|
||
if (a == null && b == null) return true
|
||
if (a == null || b == null) return false
|
||
return roundTo(a, precision) === roundTo(b, precision)
|
||
}
|
||
|
||
/**
|
||
* 读取服务词典中的计价方法开关(规模/工作量/工时),并应用默认值。
|
||
* 该方法被 sanitizePricingTotalsByService 复用。
|
||
*/
|
||
const getServiceMethodTypeById = (serviceId: string) => {
|
||
const type = serviceById.value.get(serviceId)?.type
|
||
const scale = resolveMethodEnabled(type?.scale, defaultServiceMethodType.scale)
|
||
const onlyCostScale = resolveMethodEnabled(type?.onlyCostScale, defaultServiceMethodType.onlyCostScale)
|
||
const amount = resolveMethodEnabled(type?.amount, defaultServiceMethodType.amount)
|
||
const workDay = resolveMethodEnabled(type?.workDay, defaultServiceMethodType.workDay)
|
||
return { scale, onlyCostScale, amount, workDay }
|
||
}
|
||
|
||
/**
|
||
* 按服务方法开关过滤计价合计。
|
||
* 被 clearRowValues/fillPricingTotalsForServiceIds 等入口复用。
|
||
*/
|
||
const sanitizePricingTotalsByService = (serviceId: string, totals: PricingMethodTotals): PricingMethodTotals => {
|
||
const methodType = getServiceMethodTypeById(serviceId)
|
||
const isScaleEnabled = methodType.scale
|
||
const isLandScaleEnabled = isScaleEnabled && !methodType.onlyCostScale
|
||
return {
|
||
investScale: isScaleEnabled ? totals.investScale : null,
|
||
landScale: isLandScaleEnabled ? totals.landScale : null,
|
||
workload: methodType.amount ? totals.workload : null,
|
||
hourly: methodType.workDay ? totals.hourly : null
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 对 4 个计价字段做开关过滤(例如 onlyCostScale 会禁用用地规模法)。
|
||
* 主要在“选择服务、加载历史值”时复用。
|
||
*/
|
||
const sanitizePricingFieldsByService = (
|
||
serviceId: string,
|
||
values: Pick<DetailRow, 'investScale' | 'landScale' | 'workload' | 'hourly'>
|
||
) => {
|
||
const sanitized = sanitizePricingTotalsByService(serviceId, values)
|
||
return {
|
||
investScale: sanitized.investScale,
|
||
landScale: sanitized.landScale,
|
||
workload: sanitized.workload,
|
||
hourly: sanitized.hourly
|
||
}
|
||
}
|
||
|
||
type PricingMethodField = 'investScale' | 'landScale' | 'workload' | 'hourly'
|
||
|
||
/**
|
||
* 计算某列在“非固定行”上的合计。
|
||
* 被 getMethodTotal 和固定行汇总逻辑复用。
|
||
*/
|
||
const getMethodTotalFromRows = (
|
||
rows: DetailRow[],
|
||
field: PricingMethodField
|
||
) => sumNullableNumbers(
|
||
rows
|
||
.filter(row => !isFixedRow(row))
|
||
.map(row => row[field])
|
||
)
|
||
|
||
/** 当前页面行数据中某计价列总计(仅非固定行)。 */
|
||
const getMethodTotal = (field: PricingMethodField) =>
|
||
getMethodTotalFromRows(detailRows.value, field)
|
||
|
||
/**
|
||
* 生成 4 个计价法列的公共配置,减少重复定义。
|
||
* 值来源统一:固定行取列合计,普通行取自身字段。
|
||
*/
|
||
const createMethodColumn = (
|
||
headerName: string,
|
||
field: PricingMethodField,
|
||
minWidth: number
|
||
): ColDef<DetailRow> => ({
|
||
headerName,
|
||
field,
|
||
headerClass: 'ag-right-aligned-header',
|
||
minWidth,
|
||
flex: 1.5,
|
||
cellClass: 'ag-right-aligned-cell',
|
||
editable: false,
|
||
valueGetter: params => {
|
||
if (!params.data) return null
|
||
if (isFixedRow(params.data)) return getMethodTotal(field)
|
||
return params.data[field]
|
||
},
|
||
valueParser: params => numericParser(params.newValue),
|
||
valueFormatter: params => (params.value == null ? '' : formatThousandsFlexible(params.value, 3))
|
||
})
|
||
|
||
/** 获取某服务对应四个计价页的存储键。 */
|
||
const getPricingPaneStorageKeys = (serviceId: string) =>
|
||
zxFwPricingStore.getServicePricingStorageKeys(props.contractId, serviceId)
|
||
|
||
/**
|
||
* 清空某服务在 4 个计价页的缓存与持久化数据。
|
||
* 会写入短期 skip/force 标记,避免“刚删完又被旧数据回填”。
|
||
*/
|
||
const clearPricingPaneValues = async (serviceId: string) => {
|
||
const keys = getPricingPaneStorageKeys(serviceId)
|
||
const clearIssuedAt = Date.now()
|
||
const skipUntil = clearIssuedAt + PRICING_CLEAR_SKIP_TTL_MS
|
||
const skipToken = `${clearIssuedAt}:${skipUntil}`
|
||
for (const key of keys) {
|
||
sessionStorage.setItem(`${PRICING_CLEAR_SKIP_PREFIX}${key}`, skipToken)
|
||
sessionStorage.setItem(`${PRICING_FORCE_DEFAULT_PREFIX}${key}`, String(skipUntil))
|
||
}
|
||
zxFwPricingStore.removeAllServicePricingMethodStates(props.contractId, serviceId)
|
||
// Reset后会立刻有逻辑读取IndexedDB计算默认值,这里强制同步删除持久层,避免读到旧数据。
|
||
await Promise.all(keys.map(key => kvStore.removeItem(key)))
|
||
}
|
||
|
||
/**
|
||
* 恢复单行到默认计价结果(“恢复默认”按钮)。
|
||
* 执行顺序:关闭编辑页 -> 清缓存/持久层 -> 重新生成默认明细 -> 回填合计。
|
||
*/
|
||
const clearRowValues = async (row: DetailRow) => {
|
||
if (isFixedRow(row)) return
|
||
|
||
// 若该服务编辑页已打开,先关闭,避免子页面卸载时把旧数据写回。
|
||
tabStore.removeTab(`zxfw-edit-${props.contractId}-${row.id}`)
|
||
await nextTick()
|
||
await clearPricingPaneValues(row.id)
|
||
await ensurePricingMethodDetailRowsForServices({
|
||
contractId: props.contractId,
|
||
serviceIds: [row.id],
|
||
options: PRICING_TOTALS_OPTIONS
|
||
})
|
||
const totals = await getPricingMethodTotalsForService({
|
||
contractId: props.contractId,
|
||
serviceId: row.id,
|
||
options: PRICING_TOTALS_OPTIONS
|
||
})
|
||
const sanitizedTotals = sanitizePricingTotalsByService(row.id, totals)
|
||
const currentState = getCurrentContractState()
|
||
const clearedRows = currentState.detailRows.map(item => {
|
||
if (item.id !== row.id) return item
|
||
const newSubtotal = sumNullableNumbers([sanitizedTotals.investScale, sanitizedTotals.landScale, sanitizedTotals.workload, sanitizedTotals.hourly])
|
||
return {
|
||
...item,
|
||
investScale: sanitizedTotals.investScale,
|
||
landScale: sanitizedTotals.landScale,
|
||
workload: sanitizedTotals.workload,
|
||
hourly: sanitizedTotals.hourly,
|
||
subtotal: newSubtotal != null ? roundTo(newSubtotal, 2) : null,
|
||
finalFee: newSubtotal != null ? roundTo(newSubtotal, 2) : null
|
||
}
|
||
})
|
||
await setCurrentContractState({
|
||
...currentState,
|
||
detailRows: applyFixedRowTotals(clearedRows)
|
||
})
|
||
}
|
||
|
||
/** 打开服务编辑子页。 */
|
||
const openEditTab = (row: DetailRow) => {
|
||
const serviceType = serviceById.value.get(row.id)?.type
|
||
tabStore.openTab({
|
||
id: `zxfw-edit-${props.contractId}-${row.id}`,
|
||
title: `服务编辑-${row.code}${row.name}`,
|
||
componentName: 'ZxFwView',
|
||
props: {
|
||
contractId: props.contractId,
|
||
contractName: props.contractName || '',
|
||
serviceId: row.id,
|
||
fwName: row.code + row.name,
|
||
type: serviceType ? { ...serviceType } : undefined,
|
||
projectInfoKey: props.projectInfoKey
|
||
}
|
||
})
|
||
}
|
||
|
||
/** 删除单行服务(本质是更新 selectedIds)。 */
|
||
const removeRow = async (row: DetailRow) => {
|
||
if (isFixedRow(row)) return
|
||
const nextIds = selectedIds.value.filter(id => id !== row.id)
|
||
await handleServiceSelectionChange(nextIds)
|
||
}
|
||
|
||
const ActionCellRenderer = defineComponent({
|
||
name: 'ActionCellRenderer',
|
||
props: {
|
||
params: {
|
||
type: Object as PropType<ICellRendererParams<DetailRow>>,
|
||
required: true
|
||
}
|
||
},
|
||
setup(props) {
|
||
return () => {
|
||
if (isFixedRow(props.params.data)) return null
|
||
return h('div', { class: 'zxfw-action-wrap' }, [
|
||
h('div', { class: 'zxfw-action-group' }, [
|
||
h('button', { class: 'zxfw-action-btn', 'data-action': 'edit', type: 'button' }, [
|
||
h(Pencil, { size: 13, 'aria-hidden': 'true' }),
|
||
h('span', '编辑')
|
||
]),
|
||
h('button', { class: 'zxfw-action-btn', 'data-action': 'clear', type: 'button' }, [
|
||
h(Eraser, { size: 13, 'aria-hidden': 'true' }),
|
||
h('span', '恢复默认')
|
||
]),
|
||
h('button', { class: 'zxfw-action-btn zxfw-action-btn--danger', 'data-action': 'delete', type: 'button' }, [
|
||
h(Trash2, { size: 13, 'aria-hidden': 'true' }),
|
||
h('span', '删除')
|
||
])
|
||
])
|
||
])
|
||
}
|
||
}
|
||
})
|
||
|
||
const ProcessCellRenderer = defineComponent({
|
||
name: 'ProcessCellRenderer',
|
||
props: {
|
||
params: {
|
||
type: Object as PropType<ICellRendererParams<DetailRow>>,
|
||
required: true
|
||
}
|
||
},
|
||
setup(props) {
|
||
return () => {
|
||
const row = props.params.data
|
||
if (!row || isFixedRow(row)) return null
|
||
const processValue = row.process === 1 ? 1 : 0
|
||
const onSelect = (event: Event, value: 0 | 1) => {
|
||
event.stopPropagation()
|
||
void props.params.context?.onSetProcess?.(row.id, value)
|
||
}
|
||
const radioName = `process-${row.id}`
|
||
return h('div', { class: 'flex items-center justify-center gap-4 w-full text-sm' }, [
|
||
h('label', { class: 'inline-flex items-center gap-1.5 cursor-pointer' }, [
|
||
h('input', {
|
||
type: 'radio',
|
||
name: radioName,
|
||
checked: processValue === 0,
|
||
class: 'cursor-pointer h-4 w-4',
|
||
onClick: (event: Event) => event.stopPropagation(),
|
||
onChange: (event: Event) => onSelect(event, 0)
|
||
}),
|
||
h('span', '编制')
|
||
]),
|
||
h('label', { class: 'inline-flex items-center gap-1.5 cursor-pointer' }, [
|
||
h('input', {
|
||
type: 'radio',
|
||
name: radioName,
|
||
checked: processValue === 1,
|
||
class: 'cursor-pointer h-4 w-4',
|
||
onClick: (event: Event) => event.stopPropagation(),
|
||
onChange: (event: Event) => onSelect(event, 1)
|
||
}),
|
||
h('span', '审核')
|
||
])
|
||
])
|
||
}
|
||
}
|
||
})
|
||
|
||
const NameCellRenderer = defineComponent({
|
||
name: 'NameCellRenderer',
|
||
props: {
|
||
params: {
|
||
type: Object as PropType<ICellRendererParams<DetailRow>>,
|
||
required: true
|
||
}
|
||
},
|
||
setup(props) {
|
||
return () => {
|
||
const row = props.params.data
|
||
if (!row || isFixedRow(row)) return ''
|
||
return h('div', { class: 'zxfw-name-wrap' }, String(props.params.value || row.name || ''))
|
||
}
|
||
}
|
||
})
|
||
|
||
const columnDefs: ColDef<DetailRow>[] = [
|
||
{
|
||
headerName: '编码',
|
||
field: 'code',
|
||
minWidth: 50,
|
||
maxWidth: 100,
|
||
valueGetter: params => {
|
||
if (!params.data) return ''
|
||
if (isFixedRow(params.data)) return '小计'
|
||
return params.data.code
|
||
},
|
||
colSpan: params => (params.data && isFixedRow(params.data) ? 3 : 1)
|
||
},
|
||
{
|
||
headerName: '名称',
|
||
field: 'name',
|
||
minWidth: 150,
|
||
flex: 3,
|
||
cellClass: 'zxfw-name-cell',
|
||
wrapText: true,
|
||
autoHeight: true,
|
||
cellStyle: { 'line-height': 1.6 },
|
||
cellRenderer: NameCellRenderer,
|
||
valueGetter: params => {
|
||
if (!params.data) return ''
|
||
if (isFixedRow(params.data)) return ''
|
||
return params.data.name
|
||
}
|
||
},
|
||
{
|
||
headerName: '工作环节',
|
||
field: 'process',
|
||
headerClass: 'ag-center-header zxfw-process-header',
|
||
minWidth: 150,
|
||
maxWidth: 200,
|
||
flex: 1,
|
||
editable: false,
|
||
sortable: false,
|
||
filter: false,
|
||
cellStyle: {
|
||
textAlign: 'center',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center'
|
||
},
|
||
valueGetter: params => {
|
||
if (!params.data || isFixedRow(params.data)) return null
|
||
return params.data.process === 1 ? 1 : 0
|
||
},
|
||
cellRenderer: ProcessCellRenderer
|
||
},
|
||
createMethodColumn('投资规模法', 'investScale', 100),
|
||
createMethodColumn('用地规模法', 'landScale', 100),
|
||
createMethodColumn('工作量法', 'workload', 90),
|
||
createMethodColumn('工时法', 'hourly', 90),
|
||
{
|
||
headerName: '小计',
|
||
field: 'subtotal',
|
||
headerClass: 'ag-right-aligned-header',
|
||
flex: 2,
|
||
minWidth: 100,
|
||
cellClass: 'ag-right-aligned-cell',
|
||
editable: false,
|
||
valueGetter: params => {
|
||
if (!params.data) return null
|
||
return params.data.subtotal
|
||
},
|
||
valueFormatter: params => (params.value == null ? '' : formatThousands(params.value, 2))
|
||
},
|
||
{
|
||
headerName: '确认金额',
|
||
field: 'finalFee',
|
||
headerClass: 'ag-right-aligned-header',
|
||
flex: 2,
|
||
minWidth: 110,
|
||
cellClass: 'ag-right-aligned-cell',
|
||
editable: params => !isFixedRow(params.data),
|
||
valueGetter: params => {
|
||
if (!params.data) return null
|
||
return params.data.finalFee
|
||
},
|
||
// valueSetter: params => {
|
||
// const parsed = parseNumberOrNull(params.newValue, { precision: 2 })
|
||
// const val = parsed != null ? roundTo(parsed, 2) : null
|
||
// if (params.data.finalFee === val) return false
|
||
// params.data.finalFee = val
|
||
// return true
|
||
// },
|
||
valueParser: params => {
|
||
const parsed = parseNumberOrNull(params.newValue, { precision: 2 })
|
||
return parsed != null ? roundTo(parsed, 2) : null
|
||
},
|
||
valueFormatter: params => (params.value == null ? '' : formatThousands(params.value, 2))
|
||
},
|
||
{
|
||
headerName: '操作',
|
||
field: 'actions',
|
||
minWidth: 200,
|
||
flex: 1.5,
|
||
maxWidth: 220,
|
||
editable: false,
|
||
sortable: false,
|
||
filter: false,
|
||
suppressMovable: true,
|
||
cellRenderer: ActionCellRenderer
|
||
}
|
||
]
|
||
|
||
const detailGridOptions: GridOptions<DetailRow> = {
|
||
...gridOptions,
|
||
treeData: false,
|
||
getDataPath: undefined,
|
||
context: {
|
||
onSetProcess: async (rowId: string, value: 0 | 1) => {
|
||
const currentState = getCurrentContractState()
|
||
let changed = false
|
||
const nextRows = currentState.detailRows.map(row => {
|
||
if (isFixedRow(row) || String(row.id) !== String(rowId)) return row
|
||
const nextValue = value === 1 ? 1 : 0
|
||
if ((row.process === 1 ? 1 : 0) === nextValue) return row
|
||
changed = true
|
||
return {
|
||
...row,
|
||
process: nextValue
|
||
}
|
||
})
|
||
if (!changed) return
|
||
await setCurrentContractState({
|
||
...currentState,
|
||
detailRows: nextRows
|
||
})
|
||
}
|
||
},
|
||
onCellClicked: async params => {
|
||
if (params.colDef.field !== 'actions' || !params.data || isFixedRow(params.data)) return
|
||
const target = params.event?.target as HTMLElement | null
|
||
const btn = target?.closest('button[data-action]') as HTMLButtonElement | null
|
||
const action = btn?.dataset.action
|
||
if (action === 'clear') {
|
||
requestClearRow(params.data)
|
||
return
|
||
}
|
||
if (action === 'edit') {
|
||
openEditTab(params.data)
|
||
return
|
||
}
|
||
if (action === 'delete') {
|
||
requestDeleteRow(params.data)
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 只重算固定行(小计行)汇总,不覆盖普通行 finalFee。
|
||
* 主要用于用户手动编辑 finalFee 后的同步场景。
|
||
*/
|
||
const applyFixedRowSummary = (rows: DetailRow[]) => {
|
||
const nextInvestScale = getMethodTotalFromRows(rows, 'investScale')
|
||
const nextLandScale = getMethodTotalFromRows(rows, 'landScale')
|
||
const nextWorkload = getMethodTotalFromRows(rows, 'workload')
|
||
const nextHourly = getMethodTotalFromRows(rows, 'hourly')
|
||
const totalFinalFee = sumNullableNumbers(rows.filter(r => !isFixedRow(r)).map(r => r.finalFee))
|
||
return rows.map(row =>
|
||
isFixedRow(row)
|
||
? {
|
||
...row,
|
||
investScale: nextInvestScale,
|
||
landScale: nextLandScale,
|
||
workload: nextWorkload,
|
||
hourly: nextHourly,
|
||
subtotal: sumNullableNumbers([nextInvestScale, nextLandScale, nextWorkload, nextHourly]),
|
||
finalFee: totalFinalFee != null ? roundTo(totalFinalFee, 2) : null
|
||
}
|
||
: row
|
||
)
|
||
}
|
||
|
||
/**
|
||
* 计价法金额发生变化时调用:
|
||
* 1) 普通行 finalFee 强制同步 subtotal
|
||
* 2) 固定行汇总(四列+确认金额)统一重算
|
||
*/
|
||
const applyFixedRowTotals = (rows: DetailRow[]) => {
|
||
const syncedRows = rows.map(row => {
|
||
if (isFixedRow(row)) return row
|
||
const rowSubtotal = sumNullableNumbers([row.investScale, row.landScale, row.workload, row.hourly])
|
||
return {
|
||
...row,
|
||
finalFee: rowSubtotal != null ? roundTo(rowSubtotal, 2) : null
|
||
}
|
||
})
|
||
return applyFixedRowSummary(syncedRows)
|
||
}
|
||
|
||
/** 获取当前已选服务 id(排除固定小计行)。 */
|
||
const getSelectedServiceIdsWithoutFixed = () =>
|
||
detailRows.value
|
||
.filter(row => !isFixedRow(row))
|
||
.map(row => String(row.id))
|
||
|
||
/**
|
||
* 为当前选中服务确保“计价明细行”存在(必要时创建默认明细)。
|
||
* 初始化、勾选变化后都会调用。
|
||
*/
|
||
const ensurePricingDetailRowsForCurrentSelection = async () => {
|
||
const serviceIds = getSelectedServiceIdsWithoutFixed()
|
||
if (serviceIds.length === 0) return
|
||
await ensurePricingMethodDetailRowsForServices({
|
||
contractId: props.contractId,
|
||
serviceIds,
|
||
options: PRICING_TOTALS_OPTIONS
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 刷新指定服务的 4 个计价法合计,并回写到 zxFw 明细。
|
||
* 计价法变更场景统一走这里,最终会触发 applyFixedRowTotals。
|
||
*/
|
||
const fillPricingTotalsForServiceIds = async (serviceIds: string[]) => {
|
||
|
||
const currentState = getCurrentContractState()
|
||
const targetIds = Array.from(
|
||
new Set(
|
||
serviceIds.filter(id =>
|
||
currentState.detailRows.some(row => !isFixedRow(row) && String(row.id) === String(id))
|
||
)
|
||
)
|
||
)
|
||
|
||
if (targetIds.length === 0) {
|
||
await setCurrentContractState({
|
||
...currentState,
|
||
detailRows: applyFixedRowSummary(currentState.detailRows)
|
||
})
|
||
return
|
||
}
|
||
|
||
await ensurePricingMethodDetailRowsForServices({
|
||
contractId: props.contractId,
|
||
serviceIds: targetIds,
|
||
options: PRICING_TOTALS_OPTIONS
|
||
})
|
||
|
||
const totalsByServiceId = await getPricingMethodTotalsForServices({
|
||
contractId: props.contractId,
|
||
serviceIds: targetIds,
|
||
options: PRICING_TOTALS_OPTIONS
|
||
})
|
||
|
||
const targetSet = new Set(targetIds.map(id => String(id)))
|
||
const nextRows = currentState.detailRows.map(row => {
|
||
if (isFixedRow(row) || !targetSet.has(String(row.id))) return row
|
||
const totalsRaw = totalsByServiceId.get(String(row.id))
|
||
const totals = totalsRaw ? sanitizePricingTotalsByService(String(row.id), totalsRaw) : null
|
||
if (!totals) return row
|
||
const newSubtotal = sumNullableNumbers([totals.investScale, totals.landScale, totals.workload, totals.hourly])
|
||
const methodChanged = !(
|
||
isSameNullableNumber(row.investScale, totals.investScale)
|
||
&& isSameNullableNumber(row.landScale, totals.landScale)
|
||
&& isSameNullableNumber(row.workload, totals.workload)
|
||
&& isSameNullableNumber(row.hourly, totals.hourly)
|
||
)
|
||
return {
|
||
...row,
|
||
investScale: totals.investScale,
|
||
landScale: totals.landScale,
|
||
workload: totals.workload,
|
||
hourly: totals.hourly,
|
||
finalFee: methodChanged
|
||
? (newSubtotal != null ? roundTo(newSubtotal, 2) : null)
|
||
: row.finalFee
|
||
}
|
||
})
|
||
|
||
await setCurrentContractState({
|
||
...currentState,
|
||
detailRows: applyFixedRowSummary(nextRows)
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 应用服务勾选结果(新增/删除服务)。
|
||
* 会保留已有行可复用信息,并确保固定行始终存在。
|
||
*/
|
||
const applySelection = async (codes: string[]) => {
|
||
const currentState = getCurrentContractState()
|
||
const prevSelectedSet = new Set(currentState.selectedIds || [])
|
||
const uniqueIds = Array.from(new Set(codes)).filter(
|
||
id => serviceById.value.has(id) && id !== fixedBudgetRow.id
|
||
)
|
||
const existingMap = new Map(currentState.detailRows.map(row => [row.id, row]))
|
||
|
||
const baseRows: DetailRow[] = uniqueIds
|
||
.map<DetailRow | null>(id => {
|
||
const dictItem = serviceById.value.get(id)
|
||
if (!dictItem) return null
|
||
|
||
const old = existingMap.get(id)
|
||
|
||
const nextValues = sanitizePricingFieldsByService(id, {
|
||
investScale: old?.investScale ?? null,
|
||
landScale: old?.landScale ?? null,
|
||
workload: old?.workload ?? null,
|
||
hourly: old?.hourly ?? null
|
||
})
|
||
return {
|
||
id: old?.id || id,
|
||
code: dictItem.code,
|
||
name: dictItem.name,
|
||
process: old?.process === 1 ? 1 : 0,
|
||
investScale: nextValues.investScale,
|
||
landScale: nextValues.landScale,
|
||
workload: nextValues.workload,
|
||
hourly: nextValues.hourly,
|
||
subtotal: typeof old?.subtotal === 'number' ? old.subtotal : null,
|
||
|
||
finalFee: typeof old?.finalFee === 'number' ? old.finalFee : null
|
||
}
|
||
})
|
||
.filter((row): row is DetailRow => row !== null)
|
||
|
||
const orderMap = new Map(serviceDict.value.map((item, index) => [item.id, index]))
|
||
baseRows.sort((a, b) => (orderMap.get(a.id) || 0) - (orderMap.get(b.id) || 0))
|
||
|
||
const fixedOld = existingMap.get(fixedBudgetRow.id)
|
||
const fixedRow: DetailRow = {
|
||
id: fixedOld?.id || fixedBudgetRow.id,
|
||
code: fixedBudgetRow.code,
|
||
name: fixedBudgetRow.name,
|
||
process: null,
|
||
investScale: typeof fixedOld?.investScale === 'number' ? fixedOld.investScale : null,
|
||
landScale: typeof fixedOld?.landScale === 'number' ? fixedOld.landScale : null,
|
||
workload: typeof fixedOld?.workload === 'number' ? fixedOld.workload : null,
|
||
hourly: typeof fixedOld?.hourly === 'number' ? fixedOld.hourly : null,
|
||
subtotal: typeof fixedOld?.subtotal === 'number' ? fixedOld.subtotal : null,
|
||
finalFee: typeof fixedOld?.finalFee === 'number' ? fixedOld.finalFee : null,
|
||
actions: null
|
||
}
|
||
|
||
const removedIds = Array.from(prevSelectedSet).filter(id => !uniqueIds.includes(id))
|
||
for (const id of removedIds) {
|
||
tabStore.removeTab(`zxfw-edit-${props.contractId}-${id}`)
|
||
}
|
||
|
||
await setCurrentContractState({
|
||
...currentState,
|
||
selectedIds: uniqueIds,
|
||
detailRows: applyFixedRowSummary([...baseRows, fixedRow])
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 服务勾选变化入口:先更新行,再刷新新增服务的计价汇总。
|
||
*/
|
||
const handleServiceSelectionChange = async (ids: string[]) => {
|
||
const prevIds = [...selectedIds.value]
|
||
await applySelection(ids)
|
||
const nextSelectedIds = getCurrentContractState().selectedIds || []
|
||
const nextSelectedSet = new Set(nextSelectedIds)
|
||
const addedIds = nextSelectedIds.filter(id => !prevIds.includes(id) && nextSelectedSet.has(id))
|
||
await fillPricingTotalsForServiceIds(addedIds)
|
||
await ensurePricingDetailRowsForCurrentSelection()
|
||
}
|
||
|
||
/**
|
||
* 页面初始化/激活时入口:加载合同、套用选择、确保明细、刷新合计。
|
||
*/
|
||
const initializeContractState = async () => {
|
||
try {
|
||
await zxFwPricingStore.loadContract(props.contractId)
|
||
const data = zxFwPricingStore.getContractState(props.contractId)
|
||
const idsFromStorage = data?.selectedIds
|
||
|| (data?.selectedCodes || []).map(code => serviceIdByCode.value.get(code)).filter((id): id is string => Boolean(id))
|
||
await applySelection(idsFromStorage || [])
|
||
await ensurePricingDetailRowsForCurrentSelection()
|
||
// 重新获取所有已选服务的计价总额,确保从编辑页返回后 finalFee 和小计行都更新
|
||
const allServiceIds = getSelectedServiceIdsWithoutFixed()
|
||
if (allServiceIds.length > 0) {
|
||
await fillPricingTotalsForServiceIds(allServiceIds)
|
||
}
|
||
} catch (error) {
|
||
console.error('initializeContractState failed:', error)
|
||
await setCurrentContractState({
|
||
selectedIds: [],
|
||
detailRows: applyFixedRowTotals([{
|
||
id: fixedBudgetRow.id,
|
||
code: fixedBudgetRow.code,
|
||
name: fixedBudgetRow.name,
|
||
process: null,
|
||
investScale: null,
|
||
landScale: null,
|
||
workload: null,
|
||
hourly: null,
|
||
subtotal: null,
|
||
finalFee: null,
|
||
actions: null
|
||
}])
|
||
})
|
||
}
|
||
}
|
||
|
||
/** 读取项目行业,用于过滤可选服务词典。 */
|
||
const loadProjectIndustry = async () => {
|
||
try {
|
||
const data = await kvStore.getItem<XmBaseInfoState>(PROJECT_INFO_KEY.value)
|
||
projectIndustry.value =
|
||
typeof data?.projectIndustry === 'string' ? data.projectIndustry.trim() : ''
|
||
} catch (error) {
|
||
console.error('loadProjectIndustry failed:', error)
|
||
projectIndustry.value = ''
|
||
}
|
||
}
|
||
|
||
watch(serviceIdSignature, () => {
|
||
const availableIds = new Set(serviceDict.value.map(item => item.id))
|
||
const nextSelectedIds = selectedIds.value.filter(id => availableIds.has(id))
|
||
if (nextSelectedIds.length !== selectedIds.value.length) {
|
||
void applySelection(nextSelectedIds)
|
||
}
|
||
})
|
||
|
||
watch(
|
||
() => detailRows.value.map(row => `${row.id}:${row.name}`).join('|'),
|
||
() => {
|
||
scheduleAutoRowHeights()
|
||
}
|
||
)
|
||
|
||
onBeforeUnmount(() => {
|
||
if (autoHeightSyncTimer) {
|
||
clearTimeout(autoHeightSyncTimer)
|
||
autoHeightSyncTimer = null
|
||
}
|
||
})
|
||
|
||
/**
|
||
* 处理表格单元格编辑:当前只接管 finalFee 列。
|
||
* 编辑后仅重算固定行,避免覆盖用户刚输入的确认金额。
|
||
*/
|
||
const handleCellValueChanged = async (event: any) => {
|
||
if (event.colDef?.field !== 'finalFee') return
|
||
const row = event.data as DetailRow | undefined
|
||
if (!row || isFixedRow(row)) return
|
||
const newValue = event.newValue != null ? roundTo(Number(event.newValue), 2) : null
|
||
const currentState = getCurrentContractState()
|
||
|
||
const nextRows = currentState.detailRows.map(item =>
|
||
item.id === row.id ? { ...item, finalFee: newValue } : item
|
||
)
|
||
const finalRows = applyFixedRowSummary(nextRows)
|
||
await setCurrentContractState({
|
||
...currentState,
|
||
detailRows: finalRows
|
||
})
|
||
// 真实更新小计行的 rowNode.data,确保 AG Grid 显示最新值
|
||
const api = gridApi.value
|
||
if (api) {
|
||
const fixedRowData = finalRows.find(r => isFixedRow(r))
|
||
const fixedNode = api.getRowNode(fixedBudgetRow.id)
|
||
if (fixedNode && fixedRowData) {
|
||
fixedNode.setData(fixedRowData)
|
||
}
|
||
}
|
||
}
|
||
|
||
onMounted(async () => {
|
||
await loadProjectIndustry()
|
||
await initializeContractState()
|
||
})
|
||
|
||
onActivated(async () => {
|
||
await loadProjectIndustry()
|
||
await initializeContractState()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<TooltipProvider>
|
||
<div class="h-full min-h-0 flex flex-col gap-2">
|
||
<ServiceCheckboxSelector :services="serviceDict" :model-value="selectedIds"
|
||
@update:model-value="handleServiceSelectionChange" />
|
||
|
||
<div class="rounded-lg border bg-card xmMx flex min-h-0 flex-1 flex-col overflow-hidden">
|
||
<div class="flex items-center justify-between border-b px-3 py-2">
|
||
<h3 class="text-xs font-semibold text-foreground leading-none">
|
||
咨询服务明细
|
||
</h3>
|
||
<div class="text-[11px] text-muted-foreground leading-none">按服务词典生成</div>
|
||
</div>
|
||
|
||
<div :class="agGridWrapClass">
|
||
<AgGridVue :style="agGridStyle" :rowData="detailRows" :columnDefs="columnDefs"
|
||
:gridOptions="detailGridOptions" :theme="myTheme" @cell-value-changed="handleCellValueChanged"
|
||
:animateRows="true"
|
||
@grid-ready="onGridReady"
|
||
@first-data-rendered="onFirstDataRendered"
|
||
:enableClipboard="true" :localeText="AG_GRID_LOCALE_CN" :tooltipShowDelay="500" :headerHeight="30"
|
||
:undoRedoCellEditing="true" :undoRedoCellEditingLimit="20" />
|
||
</div>
|
||
</div>
|
||
|
||
<AlertDialogRoot :open="clearConfirmOpen" @update:open="handleClearConfirmOpenChange">
|
||
<AlertDialogPortal>
|
||
<AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" />
|
||
<AlertDialogContent
|
||
class="fixed left-1/2 top-1/2 z-[70] w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl">
|
||
<AlertDialogTitle class="text-base font-semibold">确认恢复默认数据</AlertDialogTitle>
|
||
<AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
|
||
会使用合同卡片里面最新填写的规模信息以及系数,自动计算默认数据,覆盖“{{ pendingClearServiceName }}”当前数据,是否继续?
|
||
</AlertDialogDescription>
|
||
<div class="mt-4 flex items-center justify-end gap-2">
|
||
<AlertDialogCancel as-child>
|
||
<Button variant="outline">取消</Button>
|
||
</AlertDialogCancel>
|
||
<AlertDialogAction as-child>
|
||
<Button variant="destructive" @click="confirmClearRow">确认恢复</Button>
|
||
</AlertDialogAction>
|
||
</div>
|
||
</AlertDialogContent>
|
||
</AlertDialogPortal>
|
||
</AlertDialogRoot>
|
||
|
||
<AlertDialogRoot :open="deleteConfirmOpen" @update:open="handleDeleteConfirmOpenChange">
|
||
<AlertDialogPortal>
|
||
<AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" />
|
||
<AlertDialogContent
|
||
class="fixed left-1/2 top-1/2 z-[70] w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl">
|
||
<AlertDialogTitle class="text-base font-semibold">确认删除服务</AlertDialogTitle>
|
||
<AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
|
||
将逻辑删除“{{ pendingDeleteServiceName }}”,已填写的数据不会清空,重新勾选后会恢复,是否继续?
|
||
</AlertDialogDescription>
|
||
<div class="mt-4 flex items-center justify-end gap-2">
|
||
<AlertDialogCancel as-child>
|
||
<Button variant="outline">取消</Button>
|
||
</AlertDialogCancel>
|
||
<AlertDialogAction as-child>
|
||
<Button variant="destructive" @click="confirmDeleteRow">确认删除</Button>
|
||
</AlertDialogAction>
|
||
</div>
|
||
</AlertDialogContent>
|
||
</AlertDialogPortal>
|
||
</AlertDialogRoot>
|
||
|
||
</div>
|
||
</TooltipProvider>
|
||
</template>
|
||
|
||
<style scoped>
|
||
:deep(.zxfw-process-header .ag-header-cell-label) {
|
||
justify-content: center;
|
||
}
|
||
|
||
:deep(.zxfw-process-header .ag-header-cell-text) {
|
||
text-align: center;
|
||
}
|
||
|
||
:deep(.ag-cell:not(.ag-cell-auto-height)) {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
:deep(.zxfw-name-cell.ag-cell-auto-height) {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
:deep(.zxfw-name-cell.ag-cell-auto-height .ag-cell-wrapper),
|
||
:deep(.zxfw-name-cell.ag-cell-auto-height .ag-cell-value) {
|
||
display: flex;
|
||
align-items: center;
|
||
width: 100%;
|
||
white-space: normal;
|
||
}
|
||
|
||
:deep(.zxfw-name-wrap) {
|
||
display: flex;
|
||
align-items: center;
|
||
width: 100%;
|
||
white-space: normal;
|
||
word-break: break-word;
|
||
overflow-wrap: anywhere;
|
||
line-height: 1.6;
|
||
}
|
||
</style>
|