321 lines
8.7 KiB
Vue
321 lines
8.7 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||
import { AgGridVue } from 'ag-grid-vue3'
|
||
import type {
|
||
CellValueChangedEvent,
|
||
ColDef,
|
||
GridApi,
|
||
GridReadyEvent,
|
||
ICellRendererParams,
|
||
ValueFormatterParams
|
||
} from 'ag-grid-community'
|
||
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
|
||
import { Button } from '@/components/ui/button'
|
||
import { myTheme } from '@/lib/diyAgGridOptions'
|
||
import { workList } from '@/sql'
|
||
import type { WorkType } from '@/sql'
|
||
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||
|
||
interface WorkContentRow {
|
||
id: string
|
||
content: string
|
||
type: WorkType
|
||
remark: string
|
||
checked: boolean
|
||
custom: 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
|
||
dictMode?: 'service' | 'additional' | 'none'
|
||
}>(), {
|
||
title: '工作内容',
|
||
dictMode: 'none'
|
||
})
|
||
|
||
const emit = defineEmits<{
|
||
checkedChange: [value: string[]]
|
||
}>()
|
||
|
||
const zxFwPricingStore = useZxFwPricingStore()
|
||
const gridApi = ref<GridApi<WorkContentRow> | null>(null)
|
||
const rowData = ref<WorkContentRow[]>([])
|
||
|
||
const TYPE_LABEL_MAP: Record<number, WorkType> = {
|
||
0: '基本工作',
|
||
1: '可选工作',
|
||
2: '日常顾问',
|
||
3: '专项顾问',
|
||
4: '附加工作'
|
||
}
|
||
|
||
const buildDefaultRowsFromDict = (): WorkContentRow[] => {
|
||
const rows: WorkContentRow[] = []
|
||
const entries = Object.values(workList) as Array<{ text: string; serviceid: number; order: number; type: number }>
|
||
|
||
let filtered: typeof entries
|
||
if (props.dictMode === 'service') {
|
||
const sid = Number(props.serviceId)
|
||
filtered = entries.filter(e => e.serviceid === sid)
|
||
} else if (props.dictMode === 'additional') {
|
||
filtered = entries.filter(e => e.serviceid === -1)
|
||
} else {
|
||
return []
|
||
}
|
||
|
||
filtered.sort((a, b) => a.order - b.order)
|
||
for (const entry of filtered) {
|
||
const content = String(entry.text || '').trim()
|
||
if (!content) continue
|
||
const typeLabel = TYPE_LABEL_MAP[entry.type] ?? '基本工作'
|
||
rows.push({
|
||
id: `dict-${entry.order}`,
|
||
content,
|
||
type: typeLabel,
|
||
remark: '',
|
||
checked: false,
|
||
custom: false,
|
||
path: [typeLabel, content]
|
||
})
|
||
}
|
||
return rows
|
||
}
|
||
|
||
const checkedIds = computed(() =>
|
||
rowData.value.filter(item => item.checked).map(item => item.id)
|
||
)
|
||
|
||
// 导出用:自定义内容全部包含,默认词典内容只含勾选的
|
||
const selectedTexts = computed(() =>
|
||
rowData.value
|
||
.filter(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: rowData.value.map(item => ({ ...item }))
|
||
}
|
||
zxFwPricingStore.setKeyState(props.storageKey, payload)
|
||
emitCheckedChange()
|
||
}
|
||
|
||
const loadFromStore = async () => {
|
||
const state = await zxFwPricingStore.loadKeyState<WorkContentState>(props.storageKey)
|
||
if (Array.isArray(state?.detailRows) && state.detailRows.length > 0) {
|
||
rowData.value = state.detailRows.map(item => ({
|
||
...item,
|
||
type: item.custom ? '自定义' : (item.type || '基本工作'),
|
||
path: Array.isArray(item.path) && item.path.length ? item.path : ['自定义', item.content || '未命名']
|
||
})) as WorkContentRow[]
|
||
} else {
|
||
rowData.value = buildDefaultRowsFromDict()
|
||
saveToStore()
|
||
}
|
||
emitCheckedChange()
|
||
}
|
||
|
||
const handleCheckedToggle = (id: string, checked: boolean) => {
|
||
const target = rowData.value.find(item => item.id === id)
|
||
if (!target) return
|
||
target.checked = checked
|
||
gridApi.value?.refreshCells({ force: true })
|
||
saveToStore()
|
||
}
|
||
|
||
const contentCellRenderer = (params: ICellRendererParams<WorkContentRow>) => {
|
||
const data = params.data
|
||
if (!data) return ''
|
||
const wrapper = document.createElement('div')
|
||
wrapper.className = 'work-content-cell'
|
||
// 自定义行不显示 checkbox,直接显示文本;空时显示 placeholder
|
||
if (data.custom) {
|
||
const label = document.createElement('span')
|
||
if (!data.content) {
|
||
label.className = 'work-content-placeholder'
|
||
label.textContent = '点击输入工作内容'
|
||
} 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: '序号',
|
||
minWidth: 60,
|
||
width: 70,
|
||
pinned: 'left',
|
||
suppressMovable: true,
|
||
editable: false,
|
||
valueGetter: params => (params.node?.rowIndex ?? 0) + 1
|
||
},
|
||
{
|
||
headerName: '工作内容',
|
||
field: 'content',
|
||
minWidth: 320,
|
||
flex: 2,
|
||
editable: params => Boolean(params.data?.custom),
|
||
cellClass: params => (params.data?.custom ? 'editable-cell-line' : ''),
|
||
valueParser: params => String(params.newValue || '').trim(),
|
||
cellRenderer: contentCellRenderer
|
||
},
|
||
{
|
||
headerName: '工作类型',
|
||
field: 'type',
|
||
minWidth: 100,
|
||
width: 120,
|
||
editable: false,
|
||
valueFormatter: (params: ValueFormatterParams<WorkContentRow>) => String(params.value || '')
|
||
},
|
||
{
|
||
headerName: '备注',
|
||
field: 'remark',
|
||
minWidth: 180,
|
||
flex: 1.2,
|
||
editable: true,
|
||
cellEditor: 'agLargeTextCellEditor',
|
||
wrapText: true,
|
||
autoHeight: true,
|
||
cellStyle: { whiteSpace: 'normal', lineHeight: '1.4' },
|
||
cellClass: 'editable-cell-line remark-wrap-cell',
|
||
cellClassRules: {
|
||
'editable-cell-empty': params => params.value == null || params.value === ''
|
||
},
|
||
valueFormatter: params => params.value || '点击输入'
|
||
}
|
||
]
|
||
|
||
const addCustomRow = () => {
|
||
const ts = Date.now()
|
||
rowData.value.push({
|
||
id: `custom-${ts}`,
|
||
content: '',
|
||
type: '自定义' as WorkType,
|
||
remark: '',
|
||
checked: false,
|
||
custom: true,
|
||
path: ['自定义', `自定义-${ts}`]
|
||
})
|
||
saveToStore()
|
||
}
|
||
|
||
const onGridReady = (event: GridReadyEvent<WorkContentRow>) => {
|
||
gridApi.value = event.api
|
||
}
|
||
|
||
const onCellValueChanged = (event: CellValueChangedEvent<WorkContentRow>) => {
|
||
const row = event.data
|
||
if (!row) return
|
||
if (event.colDef.field === 'content' && row.custom) {
|
||
row.path = ['自定义', row.content || `自定义-${row.id}`]
|
||
}
|
||
if (event.colDef.field === 'type' && row.custom) {
|
||
row.type = '自定义'
|
||
}
|
||
saveToStore()
|
||
}
|
||
|
||
onMounted(() => {
|
||
void loadFromStore()
|
||
})
|
||
|
||
onBeforeUnmount(() => {
|
||
saveToStore()
|
||
})
|
||
</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 }}</h3>
|
||
<Button type="button" size="sm" variant="outline" class="cursor-pointer" @click="addCustomRow">
|
||
添加自定义内容
|
||
</Button>
|
||
</div>
|
||
<div class="ag-theme-quartz h-[calc(100%-56px)] min-h-0 w-full">
|
||
<AgGridVue
|
||
:style="{ height: '100%' }"
|
||
:rowData="rowData"
|
||
:columnDefs="columnDefs"
|
||
:theme="myTheme"
|
||
:getRowId="(params: { data: WorkContentRow }) => params.data.id"
|
||
:animateRows="true"
|
||
:localeText="AG_GRID_LOCALE_CN"
|
||
:tooltipShowDelay="500"
|
||
:singleClickEdit="true"
|
||
:stopEditingWhenCellsLoseFocus="true"
|
||
:defaultColDef="{ resizable: true, sortable: false, filter: false }"
|
||
:suppressColumnVirtualisation="false"
|
||
:suppressRowVirtualisation="false"
|
||
@grid-ready="onGridReady"
|
||
@cell-value-changed="onCellValueChanged"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
:deep(.work-content-placeholder) {
|
||
color: #94a3b8;
|
||
font-style: italic;
|
||
min-width: 0;
|
||
flex: 1;
|
||
}
|
||
|
||
:deep(.work-content-cell) {
|
||
display: flex;
|
||
width: 100%;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
}
|
||
|
||
:deep(.work-content-text) {
|
||
min-width: 0;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
:deep(.work-content-check) {
|
||
width: 14px;
|
||
height: 14px;
|
||
cursor: pointer;
|
||
}
|
||
</style>
|