This commit is contained in:
zwgczx committed 2026-03-05 10:48:57 +08:00
1 parent 42fd6e48c4
commit e4a2b5373f
16 files changed
+420 -330

No files matched your search

+49
View File
@@ -0,0 +1,49 @@
<script setup lang="ts">
import { AgGridVue } from 'ag-grid-vue3'
import type { CellValueChangedEvent, ColDef } from 'ag-grid-community'
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
const props = defineProps<{
rowData: unknown[]
pinnedTopRowData?: unknown[]
columnDefs: ColDef[]
autoGroupColumnDef: ColDef
processCellForClipboard?: (params: any) => unknown
processCellFromClipboard?: (params: any) => unknown
headerHeight?: number
}>()
const emit = defineEmits<{
(event: 'cell-value-changed', payload: CellValueChangedEvent): void
}>()
const onCellValueChanged = (event: CellValueChangedEvent) => {
emit('cell-value-changed', event)
}
</script>
<template>
<AgGridVue
:style="{ height: '100%' }"
:rowData="props.rowData"
:pinnedTopRowData="props.pinnedTopRowData"
:columnDefs="props.columnDefs"
:autoGroupColumnDef="props.autoGroupColumnDef"
:gridOptions="gridOptions"
:theme="myTheme"
@cell-value-changed="onCellValueChanged"
:suppressColumnVirtualisation="true"
:suppressRowVirtualisation="true"
:cellSelection="{ handle: { mode: 'range' } }"
:enableClipboard="true"
:localeText="AG_GRID_LOCALE_CN"
:tooltipShowDelay="500"
:headerHeight="props.headerHeight ?? 50"
:suppressHorizontalScroll="true"
:processCellForClipboard="props.processCellForClipboard"
:processCellFromClipboard="props.processCellFromClipboard"
:undoRedoCellEditing="true"
:undoRedoCellEditingLimit="20"
/>
</template>
File renamed without changes.
@@ -31,31 +31,32 @@ const clearAll = () => {
</script>
<template>
<div class="rounded-lg border bg-card p-4 shadow-sm shrink-0">
<div class="mb-2 flex items-center justify-between gap-3">
<label class="block text-sm font-medium text-foreground">选择服务</label>
<div class="rounded-lg border bg-card p-2.5 shadow-sm">
<div class="mb-1 flex items-center justify-between gap-2">
<label class="block text-[11px] font-medium text-foreground leading-none">选择服务</label>
<button
type="button"
class="h-8 rounded-md border px-3 text-xs text-muted-foreground transition hover:bg-accent"
class="h-6 rounded-md border px-2 text-[11px] text-muted-foreground transition hover:bg-accent"
@click="clearAll"
>
清空
</button>
</div>
<div class="max-h-52 overflow-auto rounded-md border p-2">
<div class="flex flex-wrap gap-2">
<div class="rounded-md border p-1.5">
<div class="flex flex-wrap items-start gap-1">
<label
v-for="item in props.services"
:key="item.id"
class="inline-flex cursor-pointer items-center gap-2 rounded-md border px-2.5 py-1.5 text-sm hover:bg-muted/60"
class="inline-flex w-fit max-w-full cursor-pointer items-start gap-1.5 rounded-md border px-2 py-1 text-[11px] leading-4 hover:bg-muted/60"
>
<input
type="checkbox"
class="mt-0.5"
:checked="selectedSet.has(item.id)"
@change="toggleService(item.id, ($event.target as HTMLInputElement).checked)"
/>
<span class="text-muted-foreground">{{ item.code }}</span>
<span class="text-foreground">{{ item.name }}</span>
<span class="text-muted-foreground shrink-0">{{ item.code }}</span>
<span class="text-foreground break-words">{{ item.name }}</span>
</label>
</div>
<div v-if="props.services.length === 0" class="px-2 py-4 text-center text-xs text-muted-foreground">
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { serviceList } from '@/sql'
import XmFactorGrid from '@/components/views/XmFactorGrid.vue'
import XmFactorGrid from '@/components/common/XmFactorGrid.vue'
</script>
<template>
+4 -6
View File
@@ -1,9 +1,9 @@
<script setup lang="ts">
import { computed, onActivated, onMounted, ref } from 'vue'
import localforage from 'localforage'
import { majorList } from '@/sql'
import XmFactorGrid from '@/components/views/XmFactorGrid.vue'
import MethodUnavailableNotice from '@/components/views/pricingView/MethodUnavailableNotice.vue'
import { isMajorCodeInIndustryScope, majorList } from '@/sql'
import XmFactorGrid from '@/components/common/XmFactorGrid.vue'
import MethodUnavailableNotice from '@/components/common/MethodUnavailableNotice.vue'
interface XmBaseInfoState {
projectIndustry?: string
@@ -37,9 +37,7 @@ const loadProjectIndustry = async () => {
const filteredMajorDict = computed<Record<string, MajorItem>>(() => {
const industry = projectIndustry.value
if (!industry) return {}
const entries = Object.entries(majorList as Record<string, MajorItem>).filter(([, item]) =>
item.code === industry || item.code.startsWith(`${industry}-`)
)
const entries = Object.entries(majorList as Record<string, MajorItem>).filter(([, item]) => isMajorCodeInIndustryScope(item.code, industry))
return Object.fromEntries(entries)
})
+13 -31
View File
@@ -1,14 +1,11 @@
<script setup lang="ts">
import { computed, onActivated, onBeforeUnmount, onMounted, ref } from 'vue'
import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef, FirstDataRenderedEvent, GridApi, GridReadyEvent, GridSizeChangedEvent } from 'ag-grid-community'
import type { ColDef } from 'ag-grid-community'
import localforage from 'localforage'
import { majorList } from '@/sql'
import { myTheme ,gridOptions} from '@/lib/diyAgGridOptions'
import { isMajorCodeInIndustryScope, majorList } from '@/sql'
import { decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
import { formatThousands } from '@/lib/numberFormat'
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale';
import CommonAgGrid from '@/components/common/CommonAgGrid.vue'
// 精简的边框配置(细线条+浅灰色,弱化分割线视觉)
interface DictLeaf {
@@ -114,7 +111,7 @@ const buildDefaultRows = (): DetailRow[] => {
if (!activeIndustryCode.value) return []
const rows: DetailRow[] = []
for (const group of detailDict) {
if (activeIndustryCode.value && group.code !== activeIndustryCode.value) continue
if (activeIndustryCode.value && !isMajorCodeInIndustryScope(group.code, activeIndustryCode.value)) continue
for (const child of group.children) {
rows.push({
id: child.id,
@@ -348,30 +345,15 @@ const processCellFromClipboard = (params:any) => {
</div>
<div class="ag-theme-quartz h-full min-h-0 min-w-0 w-full flex-1 overflow-hidden">
<AgGridVue
:style="{ height: '100%' }"
:rowData="detailRows"
:pinnedTopRowData="pinnedTopRowData"
:columnDefs="columnDefs"
:autoGroupColumnDef="autoGroupColumnDef"
:gridOptions="gridOptions"
:theme="myTheme"
@cell-value-changed="handleCellValueChanged"
:suppressColumnVirtualisation="true"
:suppressRowVirtualisation="true"
:cellSelection="{ handle: { mode: 'range' } }"
:enableClipboard="true"
:localeText="AG_GRID_LOCALE_CN"
:tooltipShowDelay="500"
:headerHeight="50"
:suppressHorizontalScroll="true"
:processCellForClipboard="processCellForClipboard"
:processCellFromClipboard="processCellFromClipboard"
:undoRedoCellEditing="true"
:undoRedoCellEditingLimit="20"
/>
<CommonAgGrid
:rowData="detailRows"
:pinnedTopRowData="pinnedTopRowData"
:columnDefs="columnDefs"
:autoGroupColumnDef="autoGroupColumnDef"
:processCellForClipboard="processCellForClipboard"
:processCellFromClipboard="processCellFromClipboard"
@cell-value-changed="handleCellValueChanged"
/>
</div>
</div>
</div>
+3 -3
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'
import localforage from 'localforage'
import { majorList } from '@/sql'
import { isMajorIndustrySelectable, majorList } from '@/sql'
import { CircleHelp } from 'lucide-vue-next'
import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '@/components/ui/tooltip'
@@ -14,7 +14,7 @@ interface XmInfoState {
preparedDate?: string
}
type MajorLite = { code: string; name: string }
type MajorLite = { code: string; name: string; hideInIndustrySelector?: boolean }
type MajorParentNode = { id: string; code: string; name: string }
const DB_KEY = 'xm-base-info-v1'
@@ -41,7 +41,7 @@ const majorEntries = Object.entries(majorList as Record<string, MajorLite>)
const getMajorParentNodes = (entries: Array<[string, MajorLite]>): MajorParentNode[] =>
entries
.filter(([, item]) => !item.code.includes('-'))
.filter(([, item]) => isMajorIndustrySelectable(item))
.map(([id, item]) => ({
id,
code: item.code,
@@ -3,7 +3,7 @@ import { computed, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'v
import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef, ColGroupDef } from 'ag-grid-community'
import localforage from 'localforage'
import { majorList } from '@/sql'
import { isMajorCodeInIndustryScope, majorList } from '@/sql'
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
import { decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
import { formatThousands } from '@/lib/numberFormat'
@@ -200,7 +200,7 @@ const buildDefaultRows = (): DetailRow[] => {
if (!activeIndustryCode.value) return []
const rows: DetailRow[] = []
for (const group of detailDict) {
if (activeIndustryCode.value && group.code !== activeIndustryCode.value) continue
if (activeIndustryCode.value && !isMajorCodeInIndustryScope(group.code, activeIndustryCode.value)) continue
for (const child of group.children) {
rows.push({
id: child.id,
@@ -3,7 +3,7 @@ import { computed, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'v
import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef, ColGroupDef } from 'ag-grid-community'
import localforage from 'localforage'
import { majorList } from '@/sql'
import { isMajorCodeInIndustryScope, majorList } from '@/sql'
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
import { decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
import { formatThousands } from '@/lib/numberFormat'
@@ -201,7 +201,7 @@ const buildDefaultRows = (): DetailRow[] => {
if (!activeIndustryCode.value) return []
const rows: DetailRow[] = []
for (const group of detailDict) {
if (activeIndustryCode.value && group.code !== activeIndustryCode.value) continue
if (activeIndustryCode.value && !isMajorCodeInIndustryScope(group.code, activeIndustryCode.value)) continue
for (const child of group.children) {
rows.push({
id: child.id,
@@ -11,7 +11,7 @@ import { parseNumberOrNull } from '@/lib/number'
import { syncPricingTotalToZxFw, ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
import { loadConsultCategoryFactorMap } from '@/lib/xmFactorDefaults'
import MethodUnavailableNotice from '@/components/views/pricingView/MethodUnavailableNotice.vue'
import MethodUnavailableNotice from '@/components/common/MethodUnavailableNotice.vue'
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale';
+7 -24
View File
@@ -1,14 +1,12 @@
<script setup lang="ts">
import { computed, onActivated, onBeforeUnmount, onMounted, ref } from 'vue'
import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef, GridApi } from 'ag-grid-community'
import type { ColDef } from 'ag-grid-community'
import localforage from 'localforage'
import { majorList } from '@/sql'
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
import { isMajorCodeInIndustryScope, majorList } from '@/sql'
import { decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
import { formatThousands } from '@/lib/numberFormat'
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
import MethodUnavailableNotice from '@/components/views/pricingView/MethodUnavailableNotice.vue'
import CommonAgGrid from '@/components/common/CommonAgGrid.vue'
import MethodUnavailableNotice from '@/components/common/MethodUnavailableNotice.vue'
interface DictLeaf {
id: string
@@ -171,9 +169,7 @@ const rebuildDictByIndustry = (industryCode: string) => {
idLabelMap.value = new Map()
return
}
const filteredEntries = majorEntries.filter(([, item]) =>
item.code === industryCode || item.code.startsWith(`${industryCode}-`)
)
const filteredEntries = majorEntries.filter(([, item]) => isMajorCodeInIndustryScope(item.code, industryCode))
const nextDict = buildDetailDict(filteredEntries)
const nextLabelMap = new Map<string, string>()
for (const group of nextDict) {
@@ -423,27 +419,14 @@ const processCellFromClipboard = (params: any) => {
</div>
<div ref="agGridRef" class="ag-theme-quartz w-full flex-1 min-h-0 h-full">
<AgGridVue
:style="{ height: '100%' }"
<CommonAgGrid
:rowData="detailRows"
:pinnedTopRowData="pinnedTopRowData"
:columnDefs="columnDefs"
:autoGroupColumnDef="autoGroupColumnDef"
:gridOptions="gridOptions"
:theme="myTheme"
@cell-value-changed="handleCellValueChanged"
:suppressColumnVirtualisation="true"
:suppressRowVirtualisation="true"
:cellSelection="{ handle: { mode: 'range' } }"
:enableClipboard="true"
:localeText="AG_GRID_LOCALE_CN"
:tooltipShowDelay="500"
:headerHeight="50"
:suppressHorizontalScroll="true"
:processCellForClipboard="processCellForClipboard"
:processCellFromClipboard="processCellFromClipboard"
:undoRedoCellEditing="true"
:undoRedoCellEditingLimit="20"
@cell-value-changed="handleCellValueChanged"
/>
</div>
</div>
+91 -114
View File
@@ -11,7 +11,7 @@ import { parseNumberOrNull } from '@/lib/number'
import { formatThousands, formatThousandsFlexible } from '@/lib/numberFormat'
import { getPricingMethodTotalsForServices } from '@/lib/pricingMethodTotals'
import { ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
import { Search } from 'lucide-vue-next'
import { Pencil, Eraser, Trash2 } from 'lucide-vue-next'
import {
AlertDialogAction,
AlertDialogCancel,
@@ -31,7 +31,7 @@ import {
DialogTrigger
} from 'reka-ui'
import { Button } from '@/components/ui/button'
import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '@/components/ui/tooltip'
import { TooltipProvider } from '@/components/ui/tooltip'
import { serviceList } from '@/sql'
import { useTabStore } from '@/pinia/tab'
import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
@@ -93,74 +93,6 @@ const isFixedRow = (row?: DetailRow | null) => row?.id === fixedBudgetRow.id
const selectedIds = ref<string[]>([])
const detailRows = ref<DetailRow[]>([])
const rootRef = ref<HTMLElement | null>(null)
const gridSectionRef = ref<HTMLElement | null>(null)
const agGridRef = ref<HTMLElement | null>(null)
const agGridHeight = ref(580)
let snapScrollHost: HTMLElement | null = null
let snapTimer: ReturnType<typeof setTimeout> | null = null
let snapLockTimer: ReturnType<typeof setTimeout> | null = null
let isSnapping = false
let hostResizeObserver: ResizeObserver | null = null
const updateGridCardHeight = () => {
if (!snapScrollHost || !rootRef.value) return
const contentWrap = rootRef.value.parentElement
const style = contentWrap ? window.getComputedStyle(contentWrap) : null
const paddingTop = style ? Number.parseFloat(style.paddingTop || '0') || 0 : 0
const paddingBottom = style ? Number.parseFloat(style.paddingBottom || '0') || 0 : 0
const nextHeight = Math.max(360, Math.floor(snapScrollHost.clientHeight - paddingTop - paddingBottom))
agGridHeight.value = nextHeight-40
}
const bindSnapScrollHost = () => {
snapScrollHost = rootRef.value?.closest('[data-slot="scroll-area-viewport"]') as HTMLElement | null
if (!snapScrollHost) return
snapScrollHost.addEventListener('scroll', handleSnapHostScroll, { passive: true })
hostResizeObserver?.disconnect()
hostResizeObserver = new ResizeObserver(() => {
updateGridCardHeight()
})
hostResizeObserver.observe(snapScrollHost)
updateGridCardHeight()
}
const unbindSnapScrollHost = () => {
if (snapScrollHost) {
snapScrollHost.removeEventListener('scroll', handleSnapHostScroll)
}
hostResizeObserver?.disconnect()
hostResizeObserver = null
snapScrollHost = null
}
const trySnapToGrid = () => {
if (isSnapping || !snapScrollHost || !agGridRef.value) return
const hostRect = snapScrollHost.getBoundingClientRect()
const gridRect = agGridRef.value.getBoundingClientRect()
const offsetTop = gridRect.top - hostRect.top
const inVisibleBand = gridRect.bottom > hostRect.top + 40 && gridRect.top < hostRect.bottom - 40
const inSnapRange = offsetTop > -120 && offsetTop < 180
if (!inVisibleBand || !inSnapRange) return
isSnapping = true
agGridRef.value.scrollIntoView({ behavior: 'smooth', block: 'start' })
if (snapLockTimer) clearTimeout(snapLockTimer)
snapLockTimer = setTimeout(() => {
isSnapping = false
}, 420)
}
function handleSnapHostScroll() {
if (isSnapping) return
if (snapTimer) clearTimeout(snapTimer)
snapTimer = setTimeout(() => {
trySnapToGrid()
}, 90)
}
const pickerOpen = ref(false)
const pickerTempIds = ref<string[]>([])
@@ -168,6 +100,8 @@ const pickerSearch = ref('')
const pickerListScrollRef = ref<HTMLElement | null>(null)
const clearConfirmOpen = ref(false)
const pendingClearServiceId = ref<string | null>(null)
const deleteConfirmOpen = ref(false)
const pendingDeleteServiceId = ref<string | null>(null)
const dragSelecting = ref(false)
const dragMoved = ref(false)
let dragSelectChecked = false
@@ -249,10 +183,23 @@ const pendingClearServiceName = computed(() => {
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.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
@@ -273,6 +220,26 @@ const confirmClearRow = async () => {
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
}
const filteredServiceDict = computed(() => {
const keyword = pickerSearch.value.trim()
if (!keyword) return serviceDict
@@ -393,6 +360,12 @@ const openEditTab = (row: DetailRow) => {
})
}
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: {
@@ -405,24 +378,20 @@ const ActionCellRenderer = defineComponent({
return () => {
if (isFixedRow(props.params.data)) return null
return h('div', { class: 'zxfw-action-wrap' }, [
h(TooltipRoot, null, {
default: () => [
h(TooltipTrigger, { asChild: true }, {
default: () =>
h('button', { class: 'zxfw-action-btn', 'data-action': 'edit' }, '✏️')
}),
h(TooltipContent, { side: 'top' }, { default: () => '编辑' })
]
}),
h(TooltipRoot, null, {
default: () => [
h(TooltipTrigger, { asChild: true }, {
default: () =>
h('button', { class: 'zxfw-action-btn', 'data-action': 'clear' }, '🧹')
}),
h(TooltipContent, { side: 'top' }, { default: () => '清空' })
]
})
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', '删除')
])
])
])
}
}
@@ -525,9 +494,9 @@ const columnDefs: ColDef<DetailRow>[] = [
{
headerName: '操作',
field: 'actions',
minWidth: 50,
flex: 1,
maxWidth: 120,
minWidth: 220,
flex: 2,
maxWidth: 260,
editable: false,
sortable: false,
filter: false,
@@ -551,6 +520,10 @@ const detailGridOptions: GridOptions<DetailRow> = {
}
if (action === 'edit') {
openEditTab(params.data)
return
}
if (action === 'delete') {
requestDeleteRow(params.data)
}
}
}
@@ -833,32 +806,20 @@ const handleCellValueChanged = () => {
}, 1000)
}
const scrollToGridSection = () => {
const target = gridSectionRef.value || agGridRef.value
target?.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
onMounted(async () => {
await loadFromIndexedDB()
bindSnapScrollHost()
requestAnimationFrame(() => {
updateGridCardHeight()
})
})
onBeforeUnmount(() => {
unbindSnapScrollHost()
stopDragSelect()
if (gridPersistTimer) clearTimeout(gridPersistTimer)
if (snapTimer) clearTimeout(snapTimer)
if (snapLockTimer) clearTimeout(snapLockTimer)
void saveToIndexedDB()
})
</script>
<template>
<TooltipProvider>
<div ref="rootRef" class="space-y-6">
<div class="h-full min-h-0 flex flex-col gap-2">
<!-- 原“浏览框选择服务”实现已抽离并停用,改为直接复选框勾选 -->
<!-- <DialogRoot v-model:open="pickerOpen" @update:open="handlePickerOpenChange" /> -->
<ServiceCheckboxSelector
@@ -868,23 +829,19 @@ onBeforeUnmount(() => {
/>
<div
ref="gridSectionRef"
class="rounded-lg border bg-card xmMx scroll-mt-3" :style="{ height: `${agGridHeight}px` }"
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-4 py-3">
<h3
class="text-sm font-semibold text-foreground cursor-pointer select-none transition-colors hover:text-primary"
@click="scrollToGridSection"
>
<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-xs text-muted-foreground">按服务词典生成</div>
<div class="text-[11px] text-muted-foreground leading-none">按服务词典生成</div>
</div>
<div ref="agGridRef" class="ag-theme-quartz w-full h-full" >
<div class="ag-theme-quartz w-full flex-1 min-h-0" >
<AgGridVue :style="{ height: '100%' }" :rowData="detailRows" :columnDefs="columnDefs" :gridOptions="detailGridOptions"
: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="30" :undoRedoCellEditing="true"
:undoRedoCellEditingLimit="20" />
</div>
</div>
@@ -909,6 +866,26 @@ onBeforeUnmount(() => {
</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>