JGJS2026/src/features/shared/components/WorkContentGrid.vue
2026-03-25 17:18:35 +08:00

857 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { computed, defineComponent, h, nextTick, onBeforeUnmount, onMounted, PropType, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { AgGridVue } from 'ag-grid-vue3'
import type {
CellValueChangedEvent,
ColDef,
FirstDataRenderedEvent,
GridApi,
GridReadyEvent,
ICellRendererParams,
IGroupCellRendererParams,
IRowNode,
ValueFormatterParams
} from 'ag-grid-community'
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
import {
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogOverlay,
AlertDialogPortal,
AlertDialogRoot,
AlertDialogTitle
} from 'reka-ui'
import { Button } from '@/components/ui/button'
import { agGridDefaultColDef, myTheme, agGridStyle } from '@/lib/diyAgGridOptions'
import { withReadonlyAutoHeight } from '@/lib/agGridReadonlyAutoHeight'
import { getServiceDictItemById, getWorkListEntries, wholeProcessTasks } from '@/sql'
import { WorkType } from '@/sql'
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
import { useKvStore } from '@/pinia/kv'
import { Trash2 } from 'lucide-vue-next'
interface WorkContentRow {
id: string
content: string
type: WorkType
dictOrder?: number
serviceGroup?: string
serviceid?: number | null
remark: string
checked: boolean
custom: boolean
isAddTrigger?: boolean
path: string[]
}
interface WorkContentState {
detailRows: WorkContentRow[]
}
// dictMode: 'service' 按serviceId筛选workList'additional' 取serviceid=-1的数据'none' 不加载词典
const props = withDefaults(defineProps<{
title?: string
storageKey: string
serviceId?: number | string
contractId?: string
projectInfoKey?: string
dictMode?: 'service' | 'additional' | 'none'
}>(), {
title: '',
projectInfoKey: 'xm-base-info-v1',
dictMode: 'none'
})
const emit = defineEmits<{
checkedChange: [value: string[]]
}>()
const { t, locale } = useI18n()
const zxFwPricingStore = useZxFwPricingStore()
const kvStore = useKvStore()
const gridApi = ref<GridApi<WorkContentRow> | null>(null)
const rowData = ref<WorkContentRow[]>([])
const isWholeProcessGroupedMode = ref(false)
const groupedServiceGroups = ref<string[]>([])
const defaultColDef = {
...(agGridDefaultColDef as ColDef<WorkContentRow>),
resizable: true,
sortable: false,
filter: false
}
const syncGroupedRowsRender = async () => {
await nextTick()
const api = gridApi.value
if (!api || api.isDestroyed?.()) return
if (isWholeProcessGroupedMode.value) {
api.expandAll()
}
api.refreshClientSideRowModel('group')
api.refreshCells({ force: true })
api.redrawRows()
setTimeout(() => {
const liveApi = gridApi.value
if (!liveApi || liveApi.isDestroyed?.()) return
if (isWholeProcessGroupedMode.value) {
liveApi.expandAll()
}
liveApi.refreshCells({ force: true })
liveApi.redrawRows()
}, 16)
}
const toServiceId = (value: unknown): number | null => {
const parsed = Number(value)
if (!Number.isSafeInteger(parsed)) return null
return parsed
}
const getStableDictRowKeyFromId = (idRaw: unknown) => {
const id = String(idRaw || '').trim()
if (!id.startsWith('dict-')) return ''
const matched = /^dict-(-?\d+)-(\d+)(?:-|$)/.exec(id)
if (!matched) return ''
return `sid:${matched[1]}|order:${matched[2]}`
}
const getDictRowMergeKey = (row: Pick<WorkContentRow, 'id' | 'dictOrder' | 'content' | 'serviceid' | 'serviceGroup'>) => {
const fromId = getStableDictRowKeyFromId(row.id)
if (fromId) return fromId
const content = String(row.content || '').trim()
const serviceid = toServiceId(row.serviceid)
const dictOrder = Number(row.dictOrder)
if (serviceid != null && Number.isFinite(dictOrder)) return `sid:${serviceid}|order:${dictOrder}`
if (serviceid != null) return `sid:${serviceid}|content:${content}`
const groupName = String(row.serviceGroup || '').trim()
if (groupName) return `group:${groupName}|content:${content}`
return `content:${content}`
}
const loadProjectIndustryId = async () => {
try {
const baseInfo = await kvStore.getItem<{ projectIndustry?: unknown }>(props.projectInfoKey)
const raw = typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
if (raw.toUpperCase() === 'E2') return 0
if (raw.toUpperCase() === 'E3') return 1
if (raw.toUpperCase() === 'E4') return 2
const n = Number(raw)
return Number.isFinite(n) ? n : null
} catch (_error) {
return null
}
}
const buildDefaultRowsFromDict = async (): Promise<WorkContentRow[]> => {
const rows: WorkContentRow[] = []
const entries = getWorkListEntries(locale.value) as Array<{ text: string; serviceid: number; order: number; type: number }>
let filtered: typeof entries = []
let groupedServiceIds: number[] = []
let groupedBy: 'fid' | 'sid' | null = null
let matchedWholeProcessGroup: { fid: number; industry: number; sid: number[] } | null = null
isWholeProcessGroupedMode.value = false
groupedServiceGroups.value = []
if (props.dictMode === 'service') {
const sid = Number(props.serviceId)
const industryId = await loadProjectIndustryId()
const wholeProcessGroupByFid = wholeProcessTasks.find(
item => Number(item.fid) === sid && Number(item.industry) === industryId
)
const wholeProcessGroup = wholeProcessGroupByFid
groupedBy = wholeProcessGroupByFid ? 'fid' : null
if (wholeProcessGroup) {
groupedServiceIds = Array.isArray(wholeProcessGroup.sid)
? wholeProcessGroup.sid.map(id => Number(id)).filter(Number.isFinite)
: []
matchedWholeProcessGroup = {
fid: Number(wholeProcessGroup.fid),
industry: Number(wholeProcessGroup.industry),
sid: [...groupedServiceIds]
}
const groupedSet = new Set(groupedServiceIds)
filtered = entries.filter(e => groupedSet.has(Number(e.serviceid)))
isWholeProcessGroupedMode.value = groupedServiceIds.length > 0
groupedServiceGroups.value = groupedServiceIds.map(sid => {
const serviceItem = getServiceDictItemById(sid) as { code?: string; name?: string } | undefined
return serviceItem
? `${String(serviceItem.code || '').trim()} ${String(serviceItem.name || '').trim()}`.trim()
: String(sid)
})
} else {
filtered = entries.filter(e => e.serviceid === sid)
}
} else if (props.dictMode === 'additional') {
filtered = entries.filter(e => e.serviceid === -1 && props.storageKey.split('-').at(-1) =='2')
} else {
return []
}
if (isWholeProcessGroupedMode.value) {
const sidIndex = new Map(groupedServiceIds.map((sid, index) => [sid, index]))
filtered.sort((a, b) => {
const indexA = sidIndex.get(Number(a.serviceid)) ?? Number.MAX_SAFE_INTEGER
const indexB = sidIndex.get(Number(b.serviceid)) ?? Number.MAX_SAFE_INTEGER
if (indexA !== indexB) return indexA - indexB
return a.order - b.order
})
} else {
filtered.sort((a, b) => a.order - b.order)
}
for (const entry of filtered) {
const content = String(entry.text || '').trim()
if (!content) continue
const typeLabel = ((): WorkType => {
if (entry.type === 1) return t('workContent.type.optional') as WorkType
if (entry.type === 2) return t('workContent.type.daily') as WorkType
if (entry.type === 3) return t('workContent.type.special') as WorkType
if (entry.type === 4) return t('workContent.type.additional') as WorkType
return t('workContent.type.basic') as WorkType
})()
const serviceItem = getServiceDictItemById(entry.serviceid) as { code?: string; name?: string } | undefined
const serviceGroup = serviceItem
? `${String(serviceItem.code || '').trim()} ${String(serviceItem.name || '').trim()}`.trim()
: ''
rows.push({
id: `dict-${entry.serviceid}-${entry.order}`,
content,
type: typeLabel,
dictOrder: entry.order,
serviceGroup,
serviceid: toServiceId(entry.serviceid),
remark: '',
checked: true,
custom: false,
path: isWholeProcessGroupedMode.value && serviceGroup
? [serviceGroup, content]
: [typeLabel, content]
})
}
return rows
}
const deleteConfirmOpen = ref(false)
const pendingDeleteRowId = ref<string | null>(null)
const pendingDeleteRowName = ref('')
const requestDeleteRow = (id: string, name?: string) => {
pendingDeleteRowId.value = id
pendingDeleteRowName.value = String(name || '').trim() || t('workContent.currentRow')
deleteConfirmOpen.value = true
}
const checkedIds = computed(() =>
rowData.value.filter(item => !isAddTriggerRow(item) && item.checked).map(item => item.id)
)
// 导出用:自定义内容全部包含,默认词典内容只含勾选的
const selectedTexts = computed(() =>
rowData.value
.filter(item => !isAddTriggerRow(item) && (item.custom || item.checked))
.map(item => item.content)
.filter(Boolean)
)
defineExpose({ selectedTexts })
const emitCheckedChange = () => {
emit('checkedChange', [...checkedIds.value])
}
const saveToStore = () => {
const payload: WorkContentState = {
detailRows: getPersistableRows(rowData.value).map(item => ({ ...item }))
}
zxFwPricingStore.setKeyState(props.storageKey, payload)
emitCheckedChange()
}
const loadFromStore = async () => {
const defaultRows =
props.dictMode === 'none'
? []
: await buildDefaultRowsFromDict()
const state = await zxFwPricingStore.loadKeyState<WorkContentState>(props.storageKey)
if (Array.isArray(state?.detailRows) && state.detailRows.length > 0) {
const persistedRows = state.detailRows.map(item => ({
...item,
type: item.custom ? t('workContent.type.custom') : (item.type || t('workContent.type.basic')),
checked: item.custom ? false : item.checked !== false,
serviceid: toServiceId(item.serviceid),
path: Array.isArray(item.path) && item.path.length ? item.path : [t('workContent.type.custom'), item.content || t('workContent.unnamed')]
})) as WorkContentRow[]
const defaultGroupServiceIdMap = new Map<string, number>()
for (const row of defaultRows) {
const groupName = String(row.serviceGroup || '').trim()
const serviceid = toServiceId(row.serviceid)
if (!groupName || serviceid == null) continue
defaultGroupServiceIdMap.set(groupName, serviceid)
}
for (const row of persistedRows) {
if (row.serviceid != null) continue
const groupName = String(row.serviceGroup || '').trim()
if (!groupName) continue
const fallbackServiceId = defaultGroupServiceIdMap.get(groupName)
if (fallbackServiceId != null) {
row.serviceid = fallbackServiceId
}
}
// 按最新词典规则重建默认行,再合并历史勾选/备注,保证分组规则变更后立即生效。
if (defaultRows.length > 0) {
const persistedCustomRows = persistedRows.filter(item => item.custom)
const persistedDictRows = persistedRows.filter(item => !item.custom)
const persistedByKey = new Map(
persistedDictRows.map(item => [getDictRowMergeKey(item), item])
)
const mergedDictRows = defaultRows.map(item => {
const key = getDictRowMergeKey(item)
const old = persistedByKey.get(key)
if (!old) return item
return {
...item,
checked: old.checked !== false,
remark: String(old.remark || '')
}
})
rowData.value = withAddTriggerRows([...mergedDictRows, ...persistedCustomRows])
saveToStore()
} else {
rowData.value = withAddTriggerRows(persistedRows)
isWholeProcessGroupedMode.value = rowData.value.some(
item => !item.custom && Boolean(String(item.serviceGroup || '').trim())
)
}
} else {
rowData.value = withAddTriggerRows(defaultRows)
saveToStore()
}
emitCheckedChange()
await syncGroupedRowsRender()
}
const handleCheckedToggle = (id: string, checked: boolean) => {
const target = rowData.value.find(item => item.id === id)
if (!target || isAddTriggerRow(target)) return
target.checked = checked
gridApi.value?.refreshCells({ force: true })
saveToStore()
}
const getGroupCheckableRows = (node?: IRowNode<WorkContentRow> | null) => {
if (!node) return []
return (node.allLeafChildren || [])
.map(item => item.data)
.filter((item): item is WorkContentRow => Boolean(item && !isAddTriggerRow(item) && !item.custom))
}
const handleGroupCheckedToggle = (node: IRowNode<WorkContentRow>, checked: boolean) => {
const groupRows = getGroupCheckableRows(node)
if (groupRows.length === 0) return
const targetIds = new Set(groupRows.map(item => item.id))
let changed = false
for (const row of rowData.value) {
if (!targetIds.has(row.id)) continue
if (row.checked === checked) continue
row.checked = checked
changed = true
}
if (!changed) return
gridApi.value?.refreshCells({ force: true })
gridApi.value?.redrawRows()
saveToStore()
}
const groupRowRendererParams = computed<IGroupCellRendererParams<WorkContentRow> | undefined>(() => {
if (!isWholeProcessGroupedMode.value) return undefined
return {
suppressCount: true,
innerRenderer: (params: ICellRendererParams<WorkContentRow>) => {
const wrapper = document.createElement('div')
wrapper.className = 'work-content-group-row'
const checkbox = document.createElement('input')
checkbox.type = 'checkbox'
checkbox.className = 'work-content-group-check'
const rows = getGroupCheckableRows(params.node)
const checkedCount = rows.filter(item => item.checked).length
checkbox.checked = rows.length > 0 && checkedCount === rows.length
checkbox.indeterminate = checkedCount > 0 && checkedCount < rows.length
checkbox.disabled = rows.length === 0
checkbox.addEventListener('mousedown', event => event.stopPropagation())
checkbox.addEventListener('click', event => event.stopPropagation())
checkbox.addEventListener('change', event => {
event.stopPropagation()
handleGroupCheckedToggle(params.node, checkbox.checked)
})
const label = document.createElement('span')
label.className = 'work-content-group-label'
label.textContent = String(params.valueFormatted || params.value || params.node.key || '')
wrapper.append(checkbox, label)
return wrapper
}
}
})
const contentCellRenderer = (params: ICellRendererParams<WorkContentRow>) => {
const data = params.data
if (!data) return ''
const wrapper = document.createElement('div')
wrapper.style.display = 'flex'
wrapper.style.alignItems = 'center'
wrapper.style.justifyContent = 'space-between'
wrapper.style.gap = '6px'
wrapper.style.width = '100%'
wrapper.className = 'work-content-cell'
if (isAddTriggerRow(data)) {
const label = document.createElement('span')
label.className = 'work-content-placeholder'
label.textContent = String(data.content || t('workContent.addCustom'))
wrapper.appendChild(label)
return wrapper
}
// 自定义行不显示 checkbox直接显示文本空时显示 placeholder
if (data.custom) {
const label = document.createElement('span')
if (!data.content) {
label.className = 'work-content-placeholder'
label.textContent = t('workContent.clickToInputContent')
} else {
label.className = 'work-content-text'
label.textContent = data.content
}
wrapper.appendChild(label)
return wrapper
}
const checkbox = document.createElement('input')
checkbox.type = 'checkbox'
checkbox.className = 'work-content-check'
checkbox.checked = Boolean(data.checked)
checkbox.addEventListener('change', () => {
handleCheckedToggle(data.id, checkbox.checked)
})
const label = document.createElement('span')
label.className = 'work-content-text'
label.textContent = String(data.content || '')
wrapper.appendChild(checkbox)
wrapper.appendChild(label)
return wrapper
}
const columnDefs: ColDef<WorkContentRow>[] = [
{
headerName: t('workContent.columns.no'),
minWidth: 60,
width: 70,
suppressMovable: true,
editable: false,
colSpan: params => (isAddTriggerRow(params.data) ? 5 : 1),
valueGetter: params => {
if (!params.node || params.node.group || isAddTriggerRow(params.data)) return ''
if (!isWholeProcessGroupedMode.value) return (params.node.rowIndex ?? 0) + 1
const siblings = params.node.parent?.childrenAfterSort || []
const visibleLeafSiblings = siblings.filter(node => !node.group && !isAddTriggerRow(node.data as WorkContentRow))
const index = visibleLeafSiblings.findIndex(node => node.id === params.node?.id)
return index >= 0 ? index + 1 : ''
},
cellRenderer: (params: ICellRendererParams<WorkContentRow>) => {
const row = params.data
if (!isAddTriggerRow(row)) return params.value
const button = document.createElement('button')
button.type = 'button'
button.className =
'inline-flex h-full w-full cursor-pointer items-center justify-center rounded-none border-0 bg-transparent px-3 py-3 text-sm font-medium text-blue-700 hover:bg-transparent focus:outline-none'
button.textContent = ` ${t('workContent.addCustom')}`
button.addEventListener('click', event => {
event.preventDefault()
event.stopPropagation()
addCustomRow(String(row?.serviceGroup || '').trim())
})
return button
}
},
{
headerName: t('workContent.columns.content'),
field: 'content',
minWidth: 320,
flex: 2,
cellClass: 'work-content-main-cell',
editable: params => Boolean(params.data?.custom && !isAddTriggerRow(params.data)),
valueParser: params => String(params.newValue || '').trim(),
wrapText: true,
autoHeight: true,
cellStyle: { whiteSpace: 'normal', lineHeight: '1.5' },
cellRenderer: contentCellRenderer
},
{
headerName: t('workContent.columns.type'),
field: 'type',
minWidth: 100,
width: 120,
editable: false,
valueFormatter: (params: ValueFormatterParams<WorkContentRow>) =>
isAddTriggerRow(params.data) ? '' : String(params.value || '')
},
{
headerName: t('workContent.columns.remark'),
field: 'remark',
minWidth: 180,
flex: 1.2,
editable: params => !isAddTriggerRow(params.data),
cellEditor: 'agLargeTextCellEditor',
wrapText: true,
autoHeight: true,
cellStyle: { whiteSpace: 'normal', lineHeight: '1.4' },
cellClass: 'remark-wrap-cell',
cellClassRules: {
'editable-cell-empty': params => params.value == null || params.value === ''
},
valueFormatter: params => (isAddTriggerRow(params.data) ? '' : (params.value || t('workContent.clickToInput')))
},
{
headerName: t('workContent.columns.actions'),
colId: 'actions',
minWidth: 92,
maxWidth: 110,
flex: 0.8,
editable: false,
sortable: false,
filter: false,
suppressMovable: true,
cellRenderer: defineComponent({
name: 'HtFeeGridActionCellRenderer',
props: {
params: {
type: Object as PropType<ICellRendererParams<WorkContentRow>>,
required: true
}
},
setup(rendererProps) {
return () => {
const row = rendererProps.params.data
if (!row?.custom) return null
const onDelete = (event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
requestDeleteRow(row.id, row.content)
}
return h(
'button',
{
type: 'button',
class:
'inline-flex cursor-pointer items-center gap-1 rounded border border-red-200 px-2 py-1 text-xs text-red-600 hover:bg-red-50',
onClick: onDelete
},
[h(Trash2, { size: 12, 'aria-hidden': 'true' }), h('span', t('common.delete'))]
)
}
}
})
}
]
const gridColumnDefs = computed(() => withReadonlyAutoHeight(columnDefs))
const isAddTriggerRow = (row?: WorkContentRow | null) => Boolean(row?.isAddTrigger)
const getPersistableRows = (rows: WorkContentRow[]) => rows.filter(item => !isAddTriggerRow(item))
const createAddTriggerRow = (groupName?: string): WorkContentRow => {
const suffix = groupName ? String(groupName).trim() : 'root'
return {
id: `add-trigger-${suffix}`,
content: t('workContent.addCustom'),
type: t('workContent.type.custom') as WorkType,
serviceGroup: groupName || '',
serviceid: null,
remark: '',
checked: false,
custom: false,
isAddTrigger: true,
path: groupName ? [groupName, '__add__'] : ['__add__']
}
}
const withAddTriggerRows = (rows: WorkContentRow[]) => {
const pureRows = getPersistableRows(rows)
if (isWholeProcessGroupedMode.value) {
const groupedMap = new Map<string, WorkContentRow[]>()
for (const row of pureRows) {
const groupName = String(row.serviceGroup || '').trim() || t('workContent.ungrouped')
if (!groupedMap.has(groupName)) groupedMap.set(groupName, [])
groupedMap.get(groupName)?.push(row)
}
const groupOrder = groupedServiceGroups.value.length
? [...groupedServiceGroups.value]
: [...groupedMap.keys()]
const result: WorkContentRow[] = []
const used = new Set<string>()
for (const groupName of groupOrder) {
used.add(groupName)
result.push(...(groupedMap.get(groupName) || []))
result.push(createAddTriggerRow(groupName))
}
for (const [groupName, groupRows] of groupedMap.entries()) {
if (used.has(groupName)) continue
result.push(...groupRows)
result.push(createAddTriggerRow(groupName))
}
return result
}
return [...pureRows, createAddTriggerRow()]
}
const getDataPath = (data: WorkContentRow) => {
const path = Array.isArray(data?.path)
? data.path.map(segment => String(segment || '').trim()).filter(Boolean)
: []
if (path.length > 0) return path
const fallback = String(data?.id || '').trim()
return [fallback || '__row__']
}
const addCustomRow = (groupName?: string) => {
const ts = Date.now()
const finalGroupName = isWholeProcessGroupedMode.value
? String(groupName || groupedServiceGroups.value[0] || '').trim()
: ''
const finalServiceId = isWholeProcessGroupedMode.value
? (() => {
const pureRows = getPersistableRows(rowData.value)
const hit = pureRows.find(item => String(item.serviceGroup || '').trim() === finalGroupName && item.serviceid != null)
return hit?.serviceid ?? null
})()
: null
const nextRow: WorkContentRow = {
id: `custom-${ts}`,
content: '',
type: t('workContent.type.custom') as WorkType,
serviceGroup: finalGroupName,
serviceid: finalServiceId,
remark: '',
checked: false,
custom: true,
path: isWholeProcessGroupedMode.value && finalGroupName
? [finalGroupName, `${t('workContent.type.custom')}-${ts}`]
: [t('workContent.type.custom'), `${t('workContent.type.custom')}-${ts}`]
}
const pureRows = getPersistableRows(rowData.value)
pureRows.push(nextRow)
rowData.value = withAddTriggerRows(pureRows)
saveToStore()
setTimeout(() => {
const rowIndex = rowData.value.findIndex(item => item.id === nextRow.id)
if (rowIndex >= 0) gridApi.value?.startEditingCell({ rowIndex, colKey: 'content' })
}, 0)
}
const onGridReady = (event: GridReadyEvent<WorkContentRow>) => {
gridApi.value = event.api
void syncGroupedRowsRender()
}
const onFirstDataRendered = (_event: FirstDataRenderedEvent<WorkContentRow>) => {
void syncGroupedRowsRender()
}
const onCellValueChanged = (event: CellValueChangedEvent<WorkContentRow>) => {
const row = event.data
if (!row || isAddTriggerRow(row)) return
if (event.colDef.field === 'content' && row.custom) {
const groupName = String(row.serviceGroup || '').trim()
row.path = isWholeProcessGroupedMode.value && groupName
? [groupName, row.content || `${t('workContent.type.custom')}-${row.id}`]
: [t('workContent.type.custom'), row.content || `${t('workContent.type.custom')}-${row.id}`]
}
if (event.colDef.field === 'type' && row.custom) {
row.type = t('workContent.type.custom') as WorkType
}
saveToStore()
}
onMounted(() => {
void loadFromStore()
})
watch(isWholeProcessGroupedMode, () => {
void syncGroupedRowsRender()
})
watch(
() => rowData.value.length,
() => {
void syncGroupedRowsRender()
}
)
watch(locale, () => {
void loadFromStore()
})
onBeforeUnmount(() => {
gridApi.value?.stopEditing()
saveToStore()
})
const handleDeleteConfirmOpenChange = (open: boolean) => {
deleteConfirmOpen.value = open
}
const deleteRow = (id: string) => {
rowData.value = withAddTriggerRows(rowData.value.filter(item => item.id !== id))
saveToStore()
}
const confirmDeleteRow = () => {
const id = pendingDeleteRowId.value
if (!id) return
deleteRow(id)
deleteConfirmOpen.value = false
pendingDeleteRowId.value = null
pendingDeleteRowName.value = ''
}
</script>
<template>
<div class="h-full min-h-0 xmMx">
<div class="h-full min-h-0 rounded-2xl border border-border/60 bg-card/90 shadow-sm backdrop-blur-sm">
<div class="flex items-center justify-between border-b border-border/60 px-4 py-3">
<h3 class="text-sm font-semibold text-foreground">{{ props.title || t('workContent.title') }}</h3>
</div>
<div class="ag-theme-quartz h-[calc(100%-56px)] min-h-0 w-full">
<AgGridVue
:style="agGridStyle"
:rowData="rowData"
:columnDefs="gridColumnDefs"
:theme="myTheme"
:getRowId="(params: { data: WorkContentRow }) => params.data.id"
:treeData="isWholeProcessGroupedMode"
:getDataPath="getDataPath"
:groupDefaultExpanded="isWholeProcessGroupedMode ? -1 : 0"
:groupDisplayType="isWholeProcessGroupedMode ? 'groupRows' : undefined"
:groupRowRendererParams="groupRowRendererParams"
:animateRows="true"
:localeText="AG_GRID_LOCALE_CN"
:tooltipShowDelay="500"
:singleClickEdit="true"
:stopEditingWhenCellsLoseFocus="true"
:enterNavigatesVertically="true"
:enterNavigatesVerticallyAfterEdit="true"
:defaultColDef="defaultColDef"
:suppressColumnVirtualisation="false"
:suppressRowVirtualisation="false"
@grid-ready="onGridReady"
@first-data-rendered="onFirstDataRendered"
@cell-value-changed="onCellValueChanged"
/>
</div>
</div>
<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">{{ t('workContent.dialog.deleteTitle') }}</AlertDialogTitle>
<AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
{{ t('workContent.dialog.deleteDesc', { name: pendingDeleteRowName }) }}
</AlertDialogDescription>
<div class="mt-4 flex items-center justify-end gap-2">
<AlertDialogCancel as-child>
<Button variant="outline">{{ t('common.cancel') }}</Button>
</AlertDialogCancel>
<AlertDialogAction as-child>
<Button variant="destructive" @click="confirmDeleteRow">{{ t('common.delete') }}</Button>
</AlertDialogAction>
</div>
</AlertDialogContent>
</AlertDialogPortal>
</AlertDialogRoot>
</div>
</template>
<style scoped>
:deep(.ag-cell) {
display: flex;
align-items: center;
}
:deep(.ag-cell .ag-cell-wrapper) {
width: 100%;
}
:deep(.ag-cell .ag-cell-value) {
display: flex;
align-items: center;
width: 100%;
}
:deep(.work-content-placeholder) {
color: var(--muted-foreground);
font-style: italic;
min-width: 0;
flex: 1;
}
:deep(.work-content-cell) {
display: flex;
width: 100%;
align-items: center;
gap: 8px;
}
:deep(.work-content-text) {
min-width: 0;
flex: 1;
white-space: normal;
word-break: break-word;
line-height: 1.5;
}
:deep(.work-content-check) {
width: 14px;
height: 14px;
cursor: pointer;
}
:deep(.work-content-group-row) {
display: flex;
align-items: center;
gap: 8px;
}
:deep(.work-content-group-check) {
width: 14px;
height: 14px;
cursor: pointer;
}
:deep(.work-content-group-check:disabled) {
cursor: not-allowed;
opacity: 0.5;
}
:deep(.work-content-group-label) {
min-width: 0;
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>