1
This commit is contained in:
1 parent
42fd6e48c4
commit
e4a2b5373f
16 files changed
+420
-330
No files matched your search
@@ -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.
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>
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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
@@ -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>
|
||||
Reference in new issue
Block a user