import { ref, reactive, onMounted, isRef, unref, type Ref } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import { formApi, type TableName, type QueryParams } from '@/api/form' import { useProgressStore } from '@/stores/progress' import type { ColumnSchema, PagedData } from '@/types/api' /** CRUD 表格 composable 选项 */ export interface UseCrudTableOptions { /** 表名(对应后端实体) */ table: TableName /** 查询参数(支持传 Ref:父组件筛选条件变化时 fetchData 读到最新值) */ query?: QueryParams | Ref /** 默认每页条数(默认 20;树形卡片等需全量数据的场景可调大) */ defaultPageSize?: number /** 主键字段名(不传则从 columns 推导 isPrimary/primaryKey,默认 Id) */ pkField?: string /** 是否显示复选列(行选择) */ selectable?: boolean /** 新增前钩子:返回 false 取消 */ beforeCreate?: (row: Partial) => boolean | Promise /** 编辑前钩子:返回 false 取消 */ beforeEdit?: (row: T) => boolean | Promise /** 删除前确认 */ beforeDelete?: (row: T) => boolean | Promise /** 批量删除前确认 */ beforeBatchDelete?: (rows: T[]) => boolean | Promise /** 行点击回调 */ onRowClick?: (row: T) => void /** 查看按钮回调(详情页跳转) */ onView?: (row: T) => void } /** * CRUD 表格通用逻辑 * * 对齐原生 TableEngine 的公共能力: * - 列表/分页/排序/搜索(LoadData/GoToPage/HandleSort) * - 新增/编辑弹窗(ShowAddDialog/ShowEditDialog/HandleFormSubmit) * - 删除/批量删除(HandleDelete/HandleBatchDelete) * - 行选择(GetSelectedRows/ClearSelection/ToggleSelectAll) * - 外部数据注入(SetData/SetLoading) * - 行点击/查看(onRowClick/onView) */ export function useCrudTable = Record>( options: UseCrudTableOptions, ) { const { table } = options // 查询参数:支持 Ref(父组件筛选联动)或普通对象;fetchData 时统一取最新值 const query = isRef(options.query) ? options.query : ref(options.query ?? {}) // === 状态 === const loading = ref(false) const submitting = ref(false) const columns = ref([]) as Ref const dataList = ref([]) as Ref const total = ref(0) const currentPage = ref(1) const pageSize = ref(options.defaultPageSize ?? 20) const sortField = ref('') const sortOrder = ref<'asc' | 'desc'>('asc') const searchKeyword = ref('') // === 行选择 === const selectedRows = ref([]) as Ref // === 弹窗 === const dialogVisible = ref(false) const dialogTitle = ref('新增') // 使用 Record 兼容 v-model 在模板上的类型推断 // eslint-disable-next-line @typescript-eslint/no-explicit-any const editingRow = reactive>({}) const isEdit = ref(false) // === 主键字段推导 === function getPkField(): string { if (options.pkField) return options.pkField const pkCol = columns.value.find((c) => c.isPrimary || c.primaryKey) return pkCol?.field || 'Id' } // === 加载列定义 === async function fetchColumns(): Promise { try { columns.value = await formApi.getColumns(table) } catch { // 错误已在拦截器统一处理 } } // === 加载数据 === async function fetchData(): Promise { loading.value = true try { const params: QueryParams = { page: currentPage.value, pageSize: pageSize.value, sortField: sortField.value || undefined, sortOrder: sortOrder.value, keyword: searchKeyword.value || undefined, ...(unref(query) ?? {}), } const result = await formApi.list(table, params) // 兼容两种返回结构:PagedData 或裸数组 if (Array.isArray(result)) { dataList.value = result total.value = result.length } else { const paged = result as unknown as PagedData dataList.value = paged.list || [] total.value = paged.total ?? 0 } } finally { loading.value = false } } // === 新增 === function handleCreate(): void { isEdit.value = false dialogTitle.value = '新增' // 用列定义初始化默认值 const row: Record = {} for (const col of columns.value) { if (col.defaultValue !== undefined) row[col.field] = col.defaultValue } Object.keys(editingRow).forEach((k) => delete editingRow[k]) Object.assign(editingRow, row) dialogVisible.value = true } // === 编辑 === async function handleEdit(row: T): Promise { if (options.beforeEdit && !(await options.beforeEdit(row))) return isEdit.value = true dialogTitle.value = '编辑' Object.keys(editingRow).forEach((k) => delete editingRow[k]) Object.assign(editingRow, row) dialogVisible.value = true } // === 查看(跳详情) === function handleView(row: T): void { if (options.onView) { options.onView(row) } } // === 行点击 === function handleRowClick(row: T): void { if (options.onRowClick) options.onRowClick(row) } // === 提交(新增/编辑统一入口) === async function handleSubmit(): Promise { if (options.beforeCreate && !isEdit.value) { if (!(await options.beforeCreate(editingRow as Partial))) return } submitting.value = true try { if (isEdit.value) { const pk = getPkField() const id = editingRow[pk] await formApi.update(table, id as string | number, editingRow as Partial) ElMessage.success('更新成功') } else { await formApi.create(table, editingRow as Partial) ElMessage.success('新增成功') } dialogVisible.value = false await fetchData() } finally { submitting.value = false } } // === 删除 === async function handleDelete(row: T): Promise { if (options.beforeDelete && !(await options.beforeDelete(row))) return try { await ElMessageBox.confirm('确认删除该记录?删除后不可恢复', '提示', { type: 'warning', confirmButtonText: '确认删除', cancelButtonText: '取消', }) } catch { return // 用户取消 } const pk = getPkField() const id = (row as Record)[pk] await formApi.remove(table, id as string | number) ElMessage.success('删除成功') await fetchData() } // === 批量删除 === async function handleBatchDelete(): Promise { if (selectedRows.value.length === 0) { ElMessage.warning('请先选择要删除的记录') return } if (options.beforeBatchDelete && !(await options.beforeBatchDelete(selectedRows.value))) return try { await ElMessageBox.confirm( `确认删除选中的 ${selectedRows.value.length} 条记录?删除后不可恢复`, '提示', { type: 'warning', confirmButtonText: '确认删除', cancelButtonText: '取消' }, ) } catch { return } const pk = getPkField() // 串行删除(后端无批量接口时);失败中断 for (const row of selectedRows.value) { const id = (row as Record)[pk] await formApi.remove(table, id as string | number) } ElMessage.success('批量删除成功') selectedRows.value = [] await fetchData() } // === 行选择 === function handleSelectionChange(rows: T[]): void { selectedRows.value = rows } /** 获取选中行(对齐原生 GetSelectedRows) */ function getSelectedRows(): T[] { return selectedRows.value } /** 清空选择(对齐原生 ClearSelection) */ function clearSelection(): void { selectedRows.value = [] } // === 搜索 === function handleSearch(): void { currentPage.value = 1 fetchData() } // === 重置搜索 === function handleReset(): void { searchKeyword.value = '' currentPage.value = 1 fetchData() } // === 分页变化 === function handlePageChange(page: number): void { currentPage.value = page fetchData() } function handleSizeChange(size: number): void { pageSize.value = size currentPage.value = 1 fetchData() } // === 外部数据注入(对齐原生 SetData) === function setData(data: T[], totalCount?: number): void { dataList.value = data total.value = totalCount ?? data.length } // === 初始化 === // v-show 多实例方案:组件首次挂载时加载数据并隐藏路由 loading // 之后 tab 切换仅 v-show(display),不重新挂载、不重新请求、不触发本钩子 onMounted(async () => { // 并行加载列定义与数据,减少页面首屏等待(串行需两次网络往返) try { await Promise.all([fetchColumns(), fetchData()]) } finally { // 数据加载完成(或失败),隐藏路由切换 loading useProgressStore().done() } }) return { // 状态 loading, submitting, columns, dataList, total, currentPage, pageSize, sortField, sortOrder, searchKeyword, selectedRows, dialogVisible, dialogTitle, editingRow, isEdit, // 主键 getPkField, // 动作 fetchColumns, fetchData, handleCreate, handleEdit, handleView, handleRowClick, handleSubmit, handleDelete, handleBatchDelete, handleSelectionChange, getSelectedRows, clearSelection, handleSearch, handleReset, handlePageChange, handleSizeChange, setData, } }