# 前端页面实现模式分析 > 分析日期:2026-06-01\ > 项目版本:JeecgBoot 3.9.2 *** ## 一、概述 本文档深入分析湛江人社项目前端(`jeecgboot-vue3/`)中**数据列表页面**和**数据编辑页面**的实现方式,涵盖查询表单、操作按钮、表单Input组件等核心 UI 模式的实现与使用规范。提炼出前端开发中的**最大规范公约数**,指导新的业务模块快速开发。 *** ## 二、前端项目架构概览 ### 2.1 技术栈 | 层次 | 技术 | 版本 | |------|------|------| | 框架 | Vue 3 | 3.5.22 | | 构建工具 | Vite | 6.3.6 | | UI框架 | Ant Design Vue | 4.2.6 | | 状态管理 | Pinia | 2.1.7 | | HTTP | Axios(封装为 defHttp) | 1.12.2 | | 表格 | VXE-Table / BasicTable | 4.13.31 | | 语言 | TypeScript | 5.9.3 | ### 2.2 核心目录结构 ``` jeecgboot-vue3/src/ ├── api/ # API 接口层 │ ├── sys/ # 系统管理API │ └── demo/ # 示例API ├── components/ # 公共组件层 │ ├── Table/ # BasicTable 核心表格组件 │ ├── Form/ # BasicForm 核心表单组件 │ ├── Modal/ # BasicModal 弹窗组件 │ ├── Drawer/ # BasicDrawer 抽屉组件 │ ├── jeecg/ # JeecgBoot 专用业务组件 │ └── Button/ # 按钮封装 ├── hooks/ │ ├── system/ │ │ ├── useListPage.ts # ★ 列表页面核心钩子 │ │ └── useMethods.ts # ★ 导入导出方法 │ └── web/ # 通用钩子 ├── views/ # 页面级组件 │ ├── system/ # 系统管理页面(标准模式) │ │ ├── user/ # 用户管理 │ │ ├── role/ # 角色管理 │ │ ├── dict/ # 字典管理 │ │ └── notice/ # 通知公告 │ ├── spbb/ # ★ 自定义业务模块(代码生成模式) │ │ ├── feedback/ # 视频帮办-反馈信息 │ │ └── video/ # 视频管理 │ └── super/online/ # Online 低代码动态表单 │ └── cgform/auto/ # 自动生成的列表/表单页面 └── settings/ # 全局配置 ``` *** ## 三、前端页面文件的组织规范 每个业务模块按功能分为四个文件(标准模式)或三个文件(代码生成模式): ### 3.1 标准模式(系统管理模块) ``` views/system/xxx/ ├── index.vue # 列表页面(模板 + 逻辑) ├── xxx.data.ts # 数据定义(列定义 + 查询表单 + 编辑表单) ├── xxx.api.ts # API 接口定义 ├── components/ # 子组件 │ ├── XxxDrawer.vue # 编辑抽屉(或 XxxModal.vue 编辑弹窗) │ └── ... # 其他子组件 ``` ### 3.2 代码生成模式(自定义业务模块) ``` views/spbb/xxx/ ├── XxxList.vue # 列表页面 ├── Xxx.data.ts # 数据定义 ├── Xxx.api.ts # API 接口定义 └── components/ ├── XxxModal.vue # 编辑弹窗(Modal容器层) └── XxxForm.vue # 编辑表单(表单具体实现) ``` *** ## 四、数据列表页面实现模式 ### 4.1 标准模式(使用 useListPage + BasicTable) 以 [用户管理列表页](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/system/user/index.vue)、[角色管理列表页](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/system/role/index.vue) 为代表。 #### 4.1.1 页面模板结构 ```vue 新增 导出 导入 删除 批量操作 ``` #### 4.1.2 Script 核心逻辑 ```typescript import { BasicTable, TableAction } from '/@/components/Table'; import { useDrawer } from '/@/components/Drawer'; import { useListPage } from '/@/hooks/system/useListPage'; import { columns, searchFormSchema } from './user.data'; import { list, deleteUser, getExportUrl, getImportUrl } from './user.api'; // 注册编辑组件(Drawer 或 Modal) const [registerDrawer, { openDrawer }] = useDrawer(); // 使用 useListPage 整合列表页公共方法 const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({ designScope: 'user-list', tableProps: { title: '用户列表', api: list, // 列表数据 API columns: columns, // 列定义 canResize: true, useSearchForm: true, // 启用搜索表单 formConfig: { schemas: searchFormSchema, // 搜索条件配置 }, actionColumn: { width: 120, }, }, exportConfig: { name: '用户列表', url: getExportUrl, }, importConfig: { url: getImportUrl, }, }); // 注册表格 const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext; // 新增 function handleCreate() { openDrawer(true, { isUpdate: false }); } // 编辑 function handleEdit(record: Recordable) { openDrawer(true, { record, isUpdate: true }); } // 删除 async function handleDelete(record) { await deleteUser({ id: record.id }, reload); } // 操作栏(主操作 - 直接显示) function getTableAction(record): ActionItem[] { return [ { label: '编辑', onClick: handleEdit.bind(null, record) }, ]; } // 下拉操作栏(更多操作) function getDropDownAction(record): ActionItem[] { return [ { label: '详情', onClick: handleDetail.bind(null, record) }, { label: '删除', popConfirm: { title: '是否确认删除', confirm: handleDelete.bind(null, record), }, }, ]; } ``` ### 4.2 代码生成模式(使用 j-modal + 手动管理) 以 [反馈信息列表页](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/spbb/feedback/SpbbFeedbackList.vue) 为代表。 #### 4.2.1 核心特点 - 使用 `super-query` 组件提供高级查询能力 - 使用 `useSearchForm: false` 关闭内置搜索表单 - 编辑表单通过 `defineExpose` 暴露方法供 Modal 调用 - 使用 `v-auth` 指令控制按钮权限 ```typescript // 注册表格(不使用内置搜索表单) const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({ tableProps: { api: list, columns, useSearchForm: false, // 不启用内置搜索 actionColumn: { width: 120, fixed: 'right' }, beforeFetch: async (params) => { return Object.assign(params, queryParam); }, }, exportConfig: { name: '视频帮办-反馈信息', url: getExportUrl }, importConfig: { url: getImportUrl }, }); // 编辑表单通过 ref 调用 const registerModal = ref(); function handleAdd() { registerModal.value.disableSubmit = false; registerModal.value.add(); } function handleEdit(record) { registerModal.value.disableSubmit = false; registerModal.value.edit(record); } ``` ### 4.3 useListPage 核心钩子分析 [useListPage](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/hooks/system/useListPage.ts) 是列表页面的核心封装,提供以下能力: #### 4.3.1 默认表格配置 | 配置项 | 默认值 | 说明 | |-------|--------|------| | `rowKey` | `'id'` | 主键字段 | | `useSearchForm` | `true` | 启用搜索表单 | | `formConfig.compact` | `true` | 紧凑模式 | | `formConfig.autoSubmitOnEnter` | `true` | 回车自动提交 | | `formConfig.showAdvancedButton` | `true` | 显示展开/收起按钮 | | `formConfig.autoAdvancedCol` | `3` | 超过3列自动折叠 | | `striped` | `false` | 斑马纹(默认关闭) | | `canResize` | `true` | 可自适应高度 | | `minHeight` | `300` | 表格最小高度 | | `bordered` | `true` | 显示边框 | | `showIndexColumn` | `false` | 不显示序号列 | | `showTableSetting` | `true` | 显示表格设置 | | `showActionColumn` | `true` | 显示操作列 | | `actionColumn.width` | `120` | 操作列宽度 | | 默认排序 | `createTime DESC` | 无排序参数时默认 | #### 4.3.2 自适应列配置 ```typescript const adaptiveColProps = { xs: 24, // <576px sm: 12, // ≥576px md: 12, // ≥768px lg: 8, // ≥992px xl: 8, // ≥1200px xxl: 6, // ≥1600px }; ``` #### 4.3.3 提供的方法 | 方法 | 参数 | 说明 | |------|------|------| | `onExportXls()` | 无 | 导出 Excel(合并搜索条件 + 选中行) | | `onImportXls(file)` | file | 导入 Excel | | `doRequest(api, options?)` | api + 配置 | 通用请求(自动显示确认框、加载、刷新、清空选择) | | `doDeleteRecord(api)` | api | 快捷删除(无确认框) | | `tableContext` | - | 表格上下文(解构出 registerTable, reload 等) | *** ## 五、查询表单(Search Form)实现模式 ### 5.1 标准模式 —— BasicTable 内置搜索 `useListPage` 默认提供了内置搜索表单。查询条件定义在 `.data.ts` 文件的 `searchFormSchema` 数组中: ```typescript // role.data.ts export const searchFormSchema: FormSchema[] = [ { field: 'roleName', label: '角色名称', component: 'Input', // 使用的组件 colProps: { span: 6 }, // 栅格占比 }, { field: 'roleCode', label: '角色编码', component: 'Input', colProps: { span: 6 }, }, ]; ``` 当超过 `autoAdvancedCol`(默认3列)时,多余的条件会自动折叠,显示"展开/收起"按钮。 ### 5.2 搜索表单支持的组件类型 | 组件名 | 使用示例 | 说明 | |-------|---------|------| | `Input` | `component: 'Input'` | 普通文本输入 | | `JInput` | `component: 'JInput'` | 支持模糊查询(传递 `*` 前缀) | | `JDictSelectTag` | `{ dictCode: 'sex' }` | 字典下拉选择 | | `JSelectDept` | `{ placeholder: '请选择部门' }` | 部门树选择 | | `ApiSelect` | `{ api: xxx }` | 异步数据下拉 | ### 5.3 代码生成模式 —— Online 动态查询 Online 低代码模式中,[OnlineQueryForm](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/super/online/cgform/auto/comp/OnlineQueryForm.vue) 动态渲染查询条件: - 通过 `/online/cgform/api/getQueryInfoVue3/{id}` 接口获取字段配置 - 使用 `FormSchemaFactory.createFormSchema()` 将字段配置转换为 `FormSchema` - 支持范围查询模板:`groupDate`、`groupDatetime`、`groupTime`、`groupNumber` - 支持前3列为"常用查询",超出的自动折叠 - 默认值优先级:`config(配置) > cache(路由缓存) > param(地址栏参数)` *** ## 六、编辑页面实现模式 ### 6.1 模式一:Drawer 抽屉编辑(标准模式最常用) 以 [用户编辑](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/system/user/UserDrawer.vue)、[角色编辑](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/system/role/components/RoleDrawer.vue) 为代表。 #### 6.1.1 模板结构 ```vue ``` #### 6.1.2 Script 核心逻辑 ```typescript import { BasicForm, useForm } from '/@/components/Form/index'; import { BasicDrawer, useDrawerInner } from '/@/components/Drawer'; import { formSchema } from '../role.data'; import { saveOrUpdateRole } from '../role.api'; const isUpdate = ref(true); // 注册表单(showActionButtonGroup: false 隐藏自带按钮,使用抽屉的 footer 按钮) const [registerForm, { setProps, resetFields, setFieldsValue, validate }] = useForm({ labelWidth: 90, schemas: formSchema, showActionButtonGroup: false, }); // 注册抽屉(data 从 openDrawer() 传入) const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => { resetFields(); isUpdate.value = !!data?.isUpdate; setDrawerProps({ confirmLoading: false }); if (unref(isUpdate)) { setFieldsValue({ ...data.record }); } }); // 提交事件 async function handleSubmit() { const values = await validate(); setDrawerProps({ confirmLoading: true }); await saveOrUpdateRole(values, isUpdate.value); closeDrawer(); emit('success'); } ``` ### 6.2 模式二:Modal 弹窗编辑 以 [字典编辑](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/system/dict/components/DictModal.vue)、[通知公告编辑](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/system/notice/NoticeModal.vue) 为代表。 #### 6.2.1 模板结构 ```vue ``` #### 6.2.2 与 Drawer 模式的对比 - `useDrawerInner` -> `useModalInner` - `BasicDrawer` -> `BasicModal` - `closeDrawer` -> `closeModal` - 其他逻辑完全一致 ### 6.3 模式三:代码生成模式(Modal + Form 分离) 以 [反馈信息编辑](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/spbb/feedback/components/SpbbFeedbackModal.vue + SpbbFeedbackForm.vue) 为代表。 #### 6.3.1 特点 - **Modal 和 Form 分离**为两个组件 - Modal 使用 `j-modal`(非 BasicModal),通过 `ref` 控制 - Form 使用传统的 `a-form`(非 BasicForm),通过 `Form.useForm` API - 通过 `defineExpose` 暴露 `add()`、`edit()`、`submitForm()` 方法 #### 6.3.2 Modal 容器组件 ```vue 取消 确认 ``` #### 6.3.3 表单组件 ```vue ``` ### 6.4 三种编辑模式对比 | 特性 | Drawer 模式 | Modal 模式 | 代码生成模式 | |------|-----------|-----------|------------| | 容器组件 | `BasicDrawer` | `BasicModal` | `j-modal` | | 表单组件 | `BasicForm` | `BasicForm` | `a-form` | | 数据绑定 | `setFieldsValue` | `setFieldsValue` | 手动 Object.assign | | 验证方式 | `validate()` | `validate()` | `Form.useForm.validate()` | | 组件通信 | `useDrawerInner` | `useModalInner` | `defineExpose` + ref | | 适用场景 | 内容较多的编辑页 | 内容适中的编辑页 | 代码生成模块 | *** ## 七、Form Schema 配置详解 ### 7.1 FormSchema 标准结构 ```typescript interface FormSchema { field: string; // 字段名 label: string; // 标签文本 component: string; // 组件名 required?: boolean; // 是否必填 defaultValue?: any; // 默认值 show?: boolean; // 是否显示 ifShow?: ({ values }) => boolean; // 条件显示 dynamicDisabled?: ({ values }) => boolean; // 条件禁用 componentProps?: object | (({ formActionType, formModel }) => object); // 组件属性 rules?: Rule[]; // 验证规则 dynamicRules?: ({ model, schema, values }) => Rule[]; // 动态验证规则 colProps?: { span: number }; // 栅格配置 slot?: string; // 自定义插槽 } ``` ### 7.2 表单组件使用大全 所有组件的配置方式,从 [用户数据定义](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/system/user/user.data.ts) 和 [角色数据定义](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/system/role/role.data.ts) 中提取: | 组件名 | 引入/来源 | 关键配置属性 | 用途 | |-------|----------|-------------|------| | `Input` | Ant Design Vue | `placeholder`, `readOnly` | 普通文本输入 | | `InputPassword` | Ant Design Vue | `autocomplete`, `visibilityToggle` | 密码输入 | | `InputTextArea` | Ant Design Vue | `rows`, `placeholder` | 多行文本输入 | | `InputNumber` | Ant Design Vue | `min`, `max`, `step`, `precision` | 数字输入 | | `JDictSelectTag` | Jeecg 组件 | `dictCode`, `placeholder`, `type`(`radio`/`radioButton`), `stringToNumber`, `multiple` | 字典选择 | | `JSelectDept` | Jeecg 组件 | `sync`, `checkStrictly`, `defaultExpandLevel`, `onSelect`, `onChange` | 部门树选择 | | `JSelectDepartPost` | Jeecg 组件 | `multiple`, `izShowDepPath`, `params` | 部门岗位选择 | | `JImageUpload` | Jeecg 组件 | `fileMax` | 图片上传 | | `JSearchSelect` | Jeecg 组件 | `dict`(格式:`表名,显示字段,值字段`), `async`, `multiple` | 搜索选择(如租户) | | `JSwitch` | Jeecg 组件 | `options`(`['1', '0']`), `query` | 开关 | | `JInput` | Jeecg 组件 | 同 Input,增加模糊查询能力 | 支持模糊查询的输入 | | `JRangeNumber` | Jeecg 组件 | 同 InputNumber | 范围数值输入(Online) | | `StrengthMeter` | Jeecg 组件 | `autocomplete` | 密码强度检测 | | `ApiSelect` | Ant Design Vue | `api`, `labelField`, `valueField`, `mode`, `immediate`, `params` | 异步下拉选择 | | `Select` | Ant Design Vue | `mode`, `options`, `tagRender` | 普通下拉选择 | | `RadioGroup` | Ant Design Vue | `options`(`{label, value, key}[]`) | 单选框组 | | `DatePicker` | Ant Design Vue | `valueFormat`, `placeholder` | 日期选择 | | `Switch` | Ant Design Vue | `checkedValue`, `uncheckedValue` | 开关 | | `EditorWidget` | Jeecg 组件 | `ckeditor`, `ueditor` | 富文本编辑器 | | `FileWidget` | Jeecg 组件 | 文件上传配置 | 文件上传 | | `AreaLinkage` | Jeecg 组件 | 省市区联动配置 | 行政区划选择 | ### 7.3 动态控制功能 #### 条件显示(ifShow) ```typescript { field: 'departIds', component: 'Select', ifShow: ({ values }) => values.userIdentity == 2, // 当身份为"上级"时才显示 } ``` #### 条件禁用(dynamicDisabled) ```typescript { field: 'username', component: 'Input', dynamicDisabled: ({ values }) => { return !!values.id; // 编辑时禁用,新增时可编辑 }, } ``` #### 动态验证(dynamicRules) ```typescript // 唯一性校验 { field: 'roleCode', component: 'Input', dynamicRules: ({ values, model }) => { return [ { required: true, validator: (_, value) => { return isRoleExist({ id: model.id, roleCode: value }) .then(res => res.success ? resolve() : reject(res.message)); }, }, ]; }, } // 重复校验工具函数 dynamicRules: ({ model, schema }) => rules.duplicateCheckRule('sys_table', 'field_name', model, schema, true), ``` #### 动态组件属性(componentProps 函数形式) ```typescript { field: 'selecteddeparts', component: 'JSelectDept', componentProps: ({ formActionType, formModel }) => { return { onSelect: (options, values) => { // 部门选择后更新岗位下拉数据 formActionType.updateSchema([...]); }, }; }, } ``` ### 7.4 列定义(BasicColumn)配置 ```typescript export const columns: BasicColumn[] = [ { title: '用户账号', // 列标题 dataIndex: 'username', // 对应字段 width: 120, // 列宽 resizable: true, // 可调整列宽 sorter: true, // 可排序 align: 'center', // 对齐方式 // 自定义渲染 customRender: ({ text }) => { return render.renderDict(text, 'sex'); // 字典值翻译 }, // 或使用: customRender: ({ record, text }) => { return getDepartName(record.orgCodeTxt); }, }, ]; ``` *** ## 八、操作按钮与操作栏 ### 8.1 表格标题按钮(tableTitle 插槽) 所有按钮都放在 `#tableTitle` 插槽中: | 按钮 | 图标 | 使用的API/方法 | 条件显示 | |------|------|---------------|---------| | **新增** | `ant-design:plus-outlined` | `openDrawer(true, { isUpdate: false })` | 始终显示 | | **导出** | `ant-design:export-outlined` | `onExportXls()` | 始终显示 | | **导入** | `ant-design:import-outlined` | `onImportXls()` | 始终显示 | | **刷新缓存** | `ant-design:sync-outlined` | 自定义API | 字典等特定页面 | | **回收站** | `ant-design:hdd-outlined` | `openModal(true, {})` | 用户、字典等特定页面 | | **批量操作** | `ant-design:down-outlined` | 下拉菜单 | `selectedRowKeys.length > 0` | 批量操作通过 `a-dropdown` 包裹,在选择了数据后才出现。 ### 8.2 行操作栏(#action 插槽) 使用 `TableAction` 组件分为主操作和下拉操作: ```typescript // 主操作(直接显示为按钮) function getTableAction(record): ActionItem[] { return [ { label: '编辑', onClick: handleEdit.bind(null, record) }, ]; } // 下拉操作(点击"更多"展开) function getDropDownAction(record): ActionItem[] { return [ { label: '详情', onClick: handleDetail.bind(null, record) }, { label: '删除', popConfirm: { // 弹出确认框 title: '是否确认删除', confirm: handleDelete.bind(null, record), }, }, ]; } ``` ### 8.3 按钮权限控制 - **指令方式**:`v-auth="'system:user:add'"` - **函数方式**:`hasPermission('system:user:edit')` - **auth属性**:`{ label: '编辑', auth: 'spbb:spbb_feedback:edit' }` ### 8.4 操作按钮的典型实现 ```typescript // 删除单个 async function handleDelete(record) { await deleteUser({ id: record.id }, reload); } // 批量删除 async function batchHandleDelete() { await batchDeleteUser({ ids: selectedRowKeys.value }, () => { selectedRowKeys.value = []; reload(); }); } // 批量删除(带确认框 - API 层实现) // API 文件中的实现: export const batchDelete = (params, handleSuccess) => { createConfirm({ title: '确认删除', content: '是否删除选中数据', onOk: () => { return defHttp.delete({ url: Api.deleteBatch, data: params }) .then(() => handleSuccess()); }, }); } ``` *** ## 九、API 层实现模式 ### 9.1 标准 API 定义 ```typescript // xxx.api.ts import { defHttp } from '/@/utils/http/axios'; enum Api { list = '/sys/xxx/list', add = '/sys/xxx/add', edit = '/sys/xxx/edit', deleteOne = '/sys/xxx/delete', deleteBatch = '/sys/xxx/deleteBatch', importExcel = '/sys/xxx/importExcel', exportXls = '/sys/xxx/exportXls', } export const getExportUrl = Api.exportXls; export const getImportUrl = Api.importExcel; export const list = (params) => defHttp.get({ url: Api.list, params }); export const saveOrUpdate = (params, isUpdate) => { let url = isUpdate ? Api.edit : Api.add; return defHttp.post({ url, params }); }; export const deleteOne = (params, handleSuccess) => { return defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }) .then(() => handleSuccess()); }; export const batchDelete = (params, handleSuccess) => { // 带确认框的批量删除 createConfirm({ title: '确认删除', content: '是否删除选中数据', onOk: () => defHttp.delete({ url: Api.deleteBatch, data: params }) .then(() => handleSuccess()), }); }; ``` ### 9.2 API 层规范 | 规范 | 说明 | |------|------| | URL 定义 | 使用 `enum Api {}` 管理所有 URL | | 导入导出 | 导出 URL 用 `getExportUrl` 导出,导入 URL 用 `getImportUrl` 导出 | | 导出/导入方法 | 由 `useListPage` 和 `useMethods` 统一处理 | | 删除回调 | 删除方法接受 `handleSuccess` 回调,完成 API 后调用 | | HTTP 方法 | list(`GET`), add(`POST`), edit(`POST`), delete(`DELETE`) | | `joinParamsToUrl` | DELETE 请求需要设置 `{ joinParamsToUrl: true }` | ### 9.3 defHttp 调用方式 | 场景 | 调用方式 | |------|---------| | GET 请求 | `defHttp.get({ url, params })` | | POST 请求 | `defHttp.post({ url, params })` | | DELETE 请求 | `defHttp.delete({ url, params }, { joinParamsToUrl: true })` | | 文件上传 | `defHttp.uploadFile({ url }, { file: data.file }, { success })` | | 文件下载 | `defHttp.get({ url, params, responseType: 'blob' }, { isTransformResponse: false, isReturnNativeResponse: true })` | *** ## 十、Online 动态表单的组件工厂模式 ### 10.1 组件工厂架构 [组件工厂](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/super/online/cgform/auto/comp/factory/) 是 Online 低代码的核心,将后端配置的字段定义动态转化为前端表单组件。 ``` FormSchemaFactory.createFormSchema(key, item) ↓ 根据 item.view 分发 各个 Widget 实现类 ├── InputWidget → text 文本输入 ├── NumberWidget → number 数值输入 ├── SelectWidget → select 下拉选择 ├── RadioWidget → radio 单选框 ├── CheckboxWidget → checkbox 多选框 ├── SwitchWidget → switch 开关 ├── DateWidget → date 日期选择 ├── TimeWidget → time 时间选择 ├── TextAreaWidget → textarea 多行文本 ├── PasswordWidget → password 密码输入 ├── FileWidget → file 文件上传 ├── ImageWidget → image 图片上传 ├── EditorWidget → editor 富文本编辑器 ├── PopupWidget → popup 弹出框选择 ├── PopupDictWidget → popup_dict 弹出字典 ├── LinkTableWidget → link_table 关联表 ├── LinkDownWidget → link_down 下拉联动 ├── TreeSelectWidget → tree_select 树选择 ├── TreeCategoryWidget → tree_category 树分类 ├── SelectSearchWidget → select_search 搜索选择 ├── SelectDepartWidget → depart 部门选择 ├── SelectUserWidget → user 用户选择(单选) ├── SelectUser2Widget → user2 用户选择(多选) ├── SelectMultiWidget → select_multi 多选 ├── CascaderPcaForQueryWidget → pca 省市区级联 ├── PcaWidget → pca 省市区 ├── AreaLinkage → area_linkage 区划联动 ├── SlotWidget → 自定义插槽 └── MarkdownWidget → markdown 编辑器 ``` ### 10.2 Widget 实现接口 ```typescript // IFormSchema.ts interface IFormSchema { field: string; // 字段名 setFormRef(formRef); // 设置表单 ref getFormItemSchema(); // 获取表单项配置 asSearchForm(); // 转换为搜索表单配置 isHidden(); // 设置为隐藏 noChange(); // 不监听变化 } ``` ### 10.3 组件注册机制 组件通过 `index.ts` 注册到全局: ```typescript // components/jeecg/ 下的组件 // 和 components/Form/src/jeecg/ 下的组件 // 在 registerGlobComp.ts 中统一注册 import JDictSelectTag from './components/jeecg/JDictSelectTag.vue'; app.component('JDictSelectTag', JDictSelectTag); ``` 所有 Jeecg 专用组件以 `J` 开头的命名约定: - `JInput`、`JSwitch`、`JImageUpload`、`JSelectDept`、`JSelectDepartPost`、`JSearchSelect` - `JDictSelectTag`、`JRangeNumber`、`JFormContainer` *** ## 十一、最大规范公约数(前端) ### 11.1 列表页面公约数 #### 标准模式模板(8步) ```vue 新增 导出 导入 删除 批量操作 ``` ```typescript // 第1步:导入基础设施 import { BasicTable, TableAction } from '/@/components/Table'; import { useDrawer } from '/@/components/Drawer'; import { useListPage } from '/@/hooks/system/useListPage'; import { columns, searchFormSchema } from './xxx.data'; import { list, deleteOne, getExportUrl, getImportUrl } from './xxx.api'; // 第2步:注册编辑组件 const [registerDrawer, { openDrawer }] = useDrawer(); // 第3步:调用 useListPage const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({ tableProps: { title: '模块名称', api: list, columns, formConfig: { schemas: searchFormSchema }, }, exportConfig: { name: '导出文件名', url: getExportUrl }, importConfig: { url: getImportUrl }, }); // 第4步:解构表格上下文 const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext; // 第5步:新增/编辑/删除函数 function handleCreate() { openDrawer(true, { isUpdate: false }); } function handleEdit(record) { openDrawer(true, { record, isUpdate: true }); } async function handleDelete(record) { await deleteOne({ id: record.id }, reload); } async function batchHandleDelete() { await batchDelete({ ids: selectedRowKeys.value }, reload); } // 第6步:主操作栏 function getTableAction(record): ActionItem[] { return [{ label: '编辑', onClick: handleEdit.bind(null, record) }]; } // 第7步:下拉操作栏 function getDropDownAction(record): ActionItem[] { return [ { label: '详情', onClick: handleDetail.bind(null, record) }, { label: '删除', popConfirm: { title: '是否确认删除', confirm: handleDelete.bind(null, record) }}, ]; } ``` ### 11.2 编辑页面公约数(Drawer + BasicForm 模式) ```vue ``` ```typescript import { BasicForm, useForm } from '/@/components/Form'; import { BasicDrawer, useDrawerInner } from '/@/components/Drawer'; import { formSchema } from '../xxx.data'; import { saveOrUpdate } from '../xxx.api'; const isUpdate = ref(true); const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({ labelWidth: 90, schemas: formSchema, showActionButtonGroup: false, }); const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => { await resetFields(); isUpdate.value = !!data?.isUpdate; setDrawerProps({ confirmLoading: false }); if (unref(isUpdate)) await setFieldsValue({ ...data.record }); }); const getTitle = computed(() => !unref(isUpdate) ? '新增' : '编辑'); async function handleSubmit() { const values = await validate(); setDrawerProps({ confirmLoading: true }); await saveOrUpdate(values, isUpdate.value); closeDrawer(); emit('success'); } ``` ### 11.3 data.ts 文件公约数 ```typescript import { BasicColumn } from '/@/components/Table'; import { FormSchema } from '/@/components/Table'; // 1. 列定义 export const columns: BasicColumn[] = [ { title: '名称', dataIndex: 'name', width: 120, resizable: true }, { title: '创建时间', dataIndex: 'createTime', width: 100 }, ]; // 2. 搜索表单 export const searchFormSchema: FormSchema[] = [ { label: '名称', field: 'name', component: 'JInput', colProps: { span: 6 } }, ]; // 3. 编辑表单 export const formSchema: FormSchema[] = [ { label: '', field: 'id', component: 'Input', show: false }, // 隐藏ID { label: '名称', field: 'name', required: true, component: 'Input' }, // 基本信息 { label: '类型', field: 'type', component: 'JDictSelectTag', // 字典 componentProps: { dictCode: 'xxx_type', stringToNumber: true }}, { label: '部门', field: 'deptId', component: 'JSelectDept' }, // 部门 { label: '日期', field: 'date', component: 'DatePicker' }, // 日期 { label: '备注', field: 'remark', component: 'InputTextArea' }, // 多行 { label: '排序', field: 'sort', component: 'InputNumber', // 数字 componentProps: { min: 1, precision: 0 }}, ]; ``` ### 11.4 api.ts 文件公约数 ```typescript import { defHttp } from '/@/utils/http/axios'; enum Api { list = '/模块/实体/list', add = '/模块/实体/add', edit = '/模块/实体/edit', deleteOne = '/模块/实体/delete', deleteBatch = '/模块/实体/deleteBatch', importExcel = '/模块/实体/importExcel', exportXls = '/模块/实体/exportXls', } export const getExportUrl = Api.exportXls; export const getImportUrl = Api.importExcel; export const list = (params) => defHttp.get({ url: Api.list, params }); export const saveOrUpdate = (params, isUpdate) => defHttp.post({ url: isUpdate ? Api.edit : Api.add, params }); export const deleteOne = (params, handleSuccess) => defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }).then(() => handleSuccess()); export const batchDelete = (params, handleSuccess) => defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => handleSuccess()); ``` ### 11.5 开发一个前端新模块的最小步骤 1. **创建目录**:`views/模块名/` 2. **创建 api.ts**:定义 7 个标准 API URL 3. **创建 data.ts**:定义 columns + searchFormSchema + formSchema 4. **创建编辑组件**:`components/XxxDrawer.vue`(Drawer + BasicForm 模式) 5. **创建列表页**:`index.vue`(BasicTable + useListPage 模式) 6. **注册路由**:在路由配置中添加新页面路由 *** ## 十二、关键数据结构 ### 12.1 FormSchema 完整类型 ```typescript interface FormSchema { field: string; label: string; labelWidth?: number | string; component: string; componentProps?: object | ((opt: { formModel, formActionType }) => object); required?: boolean; rules?: Recordable[]; dynamicRules?: (opt: { values, model, schema }) => Recordable[]; ifShow?: (opt: { values, model, schema }) => boolean; show?: boolean; slot?: string; colProps?: { span: number; xs: number; sm: number; md: number; lg: number; xl: number; xxl: number }; defaultValue?: any; dynamicDisabled?: (opt: { values, model, schema }) => boolean; helpMessage?: string | string[]; } ``` ### 12.2 BasicColumn 标准类型 ```typescript interface BasicColumn { title: string; dataIndex: string; width?: number; resizable?: boolean; sorter?: boolean; align?: 'left' | 'center' | 'right'; fixed?: 'left' | 'right' | false; slots?: { customRender: string }; customRender?: (opt: { text, record, index }) => any; defaultHidden?: boolean; // 默认隐藏(表格设置中可配置显示) } ``` *** ## 十三、涉及的文件清单 | 文件 | 操作 | 原因 | |-----|------|------| | `AIWork/260601-前端页面实现模式分析.md` | 新增 | 本次分析文档 | *** ## 十四、关键源码索引 | 组件/文件 | 路径 | |-----------|------| | useListPage 钩子 | [`jeecgboot-vue3/src/hooks/system/useListPage.ts`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/hooks/system/useListPage.ts) | | useMethods 钩子 | [`jeecgboot-vue3/src/hooks/system/useMethods.ts`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/hooks/system/useMethods.ts) | | BasicTable props | [`jeecgboot-vue3/src/components/Table/src/props.ts`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Table/src/props.ts) | | BasicForm props | [`jeecgboot-vue3/src/components/Form/src/props.ts`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Form/src/props.ts) | | 用户管理列表页 | [`jeecgboot-vue3/src/views/system/user/index.vue`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/system/user/index.vue) | | 用户编辑抽屉 | [`jeecgboot-vue3/src/views/system/user/UserDrawer.vue`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/system/user/UserDrawer.vue) | | 用户数据定义 | [`jeecgboot-vue3/src/views/system/user/user.data.ts`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/system/user/user.data.ts) | | 用户API定义 | [`jeecgboot-vue3/src/views/system/user/user.api.ts`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/system/user/user.api.ts) | | 角色管理列表页 | [`jeecgboot-vue3/src/views/system/role/index.vue`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/system/role/index.vue) | | 角色编辑抽屉 | [`jeecgboot-vue3/src/views/system/role/components/RoleDrawer.vue`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/system/role/components/RoleDrawer.vue) | | 角色数据定义 | [`jeecgboot-vue3/src/views/system/role/role.data.ts`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/system/role/role.data.ts) | | 字典管理列表页 | [`jeecgboot-vue3/src/views/system/dict/index.vue`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/system/dict/index.vue) | | 字典编辑弹窗 | [`jeecgboot-vue3/src/views/system/dict/components/DictModal.vue`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/system/dict/components/DictModal.vue) | | 通知公告列表页 | [`jeecgboot-vue3/src/views/system/notice/index.vue`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/system/notice/index.vue) | | 通知公告编辑弹窗 | [`jeecgboot-vue3/src/views/system/notice/NoticeModal.vue`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/system/notice/NoticeModal.vue) | | 反馈信息列表页 | [`jeecgboot-vue3/src/views/spbb/feedback/SpbbFeedbackList.vue`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/spbb/feedback/SpbbFeedbackList.vue) | | 反馈信息编辑弹窗 | [`jeecgboot-vue3/src/views/spbb/feedback/components/SpbbFeedbackModal.vue`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/spbb/feedback/components/SpbbFeedbackModal.vue) | | 反馈信息编辑表单 | [`jeecgboot-vue3/src/views/spbb/feedback/components/SpbbFeedbackForm.vue`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/spbb/feedback/components/SpbbFeedbackForm.vue) | | Online 查询表单 | [`jeecgboot-vue3/src/views/super/online/cgform/auto/comp/OnlineQueryForm.vue`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/super/online/cgform/auto/comp/OnlineQueryForm.vue) | | 组件工厂入口 | [`jeecgboot-vue3/src/views/super/online/cgform/auto/comp/factory/FormSchemaFactory.ts`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/super/online/cgform/auto/comp/factory/FormSchemaFactory.ts) | | 组件工厂接口 | [`jeecgboot-vue3/src/views/super/online/cgform/auto/comp/factory/IFormSchema.ts`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/super/online/cgform/auto/comp/factory/IFormSchema.ts) | *** ## 十五、文件上传组件实现模式 > 追加日期:2026-06-01 JeecgBoot 框架前端内置了完整的文件上传组件体系,与后端 `/sys/common/upload` 接口无缝对接。 ### 15.1 核心上传组件概览 项目中主要有 **3 种** 文件上传组件,全部在 [componentMap.ts](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Form/src/componentMap.ts) 中注册: | 组件名 | 注册键 | 源文件路径 | 用途 | |--------|--------|-----------|------| | `JImageUpload` | `'JImageUpload'` | [JImageUpload.vue](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Form/src/jeecg/components/JImageUpload.vue) | 专用图片上传 | | `JUpload` | `'JUpload'` | [JUpload.vue](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Form/src/jeecg/components/JUpload/JUpload.vue) | 通用文件/图片上传 | | `Upload` | `'Upload'` | [BasicUpload.vue](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Upload/src/BasicUpload.vue) | 基础上传组件 | 辅助组件: - **`JUploadButton`** — 全局注册的导入按钮组件 - **`JUploadModal`** — 弹窗式上传组件 - **`JImportModal`** — Excel 导入弹窗 ### 15.2 JImageUpload 组件详解 **底层实现**:基于 `a-upload` 组件封装,设置 `accept="image/*"` **核心 Props**: | Prop | 类型 | 默认值 | 说明 | |------|------|--------|------| | `value` | `string \| array` | - | 绑定值,逗号分隔的路径字符串 | | `listType` | `string` | `'picture-card'` | 列表样式:picture-card / picture | | `text` | `string` | `'上传'` | 按钮文本 | | `bizPath` | `string` | `'temp'` | 上传业务路径(对应后端 biz 参数) | | `disabled` | `boolean` | `false` | 是否禁用 | | `fileMax` | `number` | `1` | 最大上传数量 | | `uploadUrl` | `string` | 系统上传URL | **可自定义上传地址** | | `previewWidth` | `number` | `520` | 预览宽度 | **值格式**:逗号分隔的路径字符串(如 `"temp/20260601/xxx.jpg,temp/20260601/yyy.jpg"`) **使用示例**(在 FormSchema 中): ```typescript { label: '头像', field: 'avatar', component: 'JImageUpload', componentProps: { fileMax: 1, bizPath: 'avatar', }, } ``` **使用示例**(在原生表单中直接使用): ```vue ``` ### 15.3 JUpload 组件详解 **底层实现**:基于 `a-upload`,功能更丰富,支持文件和图片两种模式 **上传类型枚举**([upload.data.ts](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Form/src/jeecg/components/JUpload/upload.data.ts)): ```typescript export enum UploadTypeEnum { all = 'all', // 所有文件 image = 'image', // 仅图片 file = 'file', // 仅文件 } ``` **核心 Props**: | Prop | 类型 | 默认值 | 说明 | |------|------|--------|------| | `value` | `string \| array` | - | 绑定值 | | `text` | `string` | `'上传'` | 按钮文本 | | `fileType` | `string` | `'all'` | 文件类型:all/image/file | | `bizPath` | `string` | `'temp'` | 上传业务路径 | | `returnUrl` | `boolean` | `true` | true=仅返回url,false=返回fileName/filePath/fileSize | | `maxCount` | `number` | `0` | 最大上传数量,0=不限制 | | `multiple` | `boolean` | `true` | 是否允许多选 | | `mover` | `boolean` | `true` | 是否显示左右移动按钮 | | `download` | `boolean` | `true` | 是否显示下载按钮 | | `removeConfirm` | `boolean` | `false` | 删除时是否确认 | | `disabled` | `boolean` | `false` | 是否禁用 | | `replaceLastOne` | `boolean` | `false` | 超出最大数量依然允许上传 | **值格式**: - `returnUrl: true`(默认):逗号分隔的路径字符串 - `returnUrl: false`:JSON 字符串数组 `[{fileName, filePath, fileSize}]` **使用示例**(在 FormSchema 中): ```typescript // 附件上传 { field: 'files', label: '通告附件', component: 'JUpload', componentProps: { text: '文件上传', maxCount: 20, download: true, }, } // 图片上传(限制1张) { field: 'uploadImageMax', component: 'JUpload', label: '上传图片(1)', componentProps: { fileType: UploadTypeEnum.image, maxCount: 1, }, } ``` ### 15.4 上传 API 层 #### 上传地址定义 [api/common/api.ts](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/api/common/api.ts): ```typescript const baseUploadUrl = globSetting.uploadUrl; export const uploadUrl = `${baseUploadUrl}/sys/common/upload`; ``` 这个 `uploadUrl` 被 `JImageUpload` 和 `JUpload` 组件直接引用作为默认上传地址。 #### defHttp.uploadFile 方法 [Axios.ts](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/utils/http/axios/Axios.ts) 提供编程式上传方法: ```typescript uploadFile(config: AxiosRequestConfig, params: UploadFileParams, callback?: UploadFileCallBack) { const formData = new window.FormData(); formData.append(customFilename, params.file, params.filename); config.baseURL = glob.uploadUrl; return this.axiosInstance.request({ ...config, method: 'POST', data: formData, headers: { 'Content-type': ContentTypeEnum.FORM_DATA }, }); } ``` #### API 文件中的使用 ```typescript // 通用上传 export const uploadFile = (params, success) => { return defHttp.uploadFile({ url: uploadUrl }, params, { success }); }; ``` ### 15.5 文件 URL 构造机制 [getFileAccessHttpUrl](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/utils/common/compUtils.ts) 是文件 URL 构造的核心函数: ```typescript export const getFileAccessHttpUrl = (fileUrl, prefix = 'http') => { let result = fileUrl; try { if (fileUrl && fileUrl.length > 0 && !fileUrl.startsWith(prefix)) { let isArray = fileUrl.indexOf('[') != -1; if (!isArray) { let prefix = `${baseApiUrl}/sys/common/static/`; if (!fileUrl.startsWith(prefix)) { result = `${prefix}${fileUrl}`; } } } } catch (e) {} return result; }; ``` **关键行为**:对于非 `http` 开头的相对路径,自动添加 `${baseApiUrl}/sys/common/static/` 前缀。这意味着: - 后端返回 `"temp/20260601/xxx.jpg"` → 前端自动构造为 `http://localhost:8080/jeecg-boot/sys/common/static/temp/20260601/xxx.jpg` - 如果路径已经是完整 URL(以 `http` 开头),则直接使用 ### 15.6 上传组件的认证机制 `JImageUpload` 和 `JUpload` 都通过 `getHeaders()` 函数自动附加 Token: ```typescript import { getHeaders } from '/@/utils/common/compUtils'; const headers = getHeaders(); ``` `getHeaders()` 内部调用 `getToken()` 获取当前用户的 JWT Token,添加到请求头中。 ### 15.7 上传流程总结 ``` 1. 组件层(JImageUpload / JUpload) ├── 使用 a-upload 的 action 属性直接上传到后端 ├── 默认上传地址:${globSetting.uploadUrl}/sys/common/upload ├── 通过 bizPath 参数指定业务子路径 └── 通过 getHeaders() 自动附加 Token 2. 后端处理 ├── CommonController.upload() 接收文件 ├── 安全校验:SsrfFileTypeFilter.checkUploadFileType() ├── 根据 uploadType 选择存储方式(local/minio/alioss) └── 返回 Result(success=true, message=相对路径) 3. 前端值存储 ├── 上传成功后从 response.message 提取相对路径 ├── 默认以逗号分隔的路径字符串存储(returnUrl: true) └── 编辑回显时通过 getFileAccessHttpUrl() 构造完整 URL 4. 文件访问 ├── 本地存储:GET /sys/common/static/{相对路径} ├── 云存储:直接使用返回的完整 URL └── 静态文件服务接口免 Token 认证(Shiro anon 配置) ``` ### 15.8 自定义上传场景的注意事项 当业务需要使用自定义上传接口(非 `/sys/common/upload`)时,需要注意以下限制: | 组件 | 是否支持自定义上传地址 | 限制说明 | |------|---------------------|----------| | `JImageUpload` | ✅ 支持(`uploadUrl` prop) | 但内部使用 `getFileAccessHttpUrl` 显示图片,该函数会自动添加 `/sys/common/static/` 前缀,与自定义路径不兼容 | | `JUpload` | ❌ 不支持 | 没有 `uploadUrl` prop,上传地址硬编码为系统默认地址 | | `a-upload` | ✅ 支持(`action` prop) | 完全自定义,需手动处理上传响应和 URL 构造 | **解决方案**:在自定义上传场景中,使用 `a-upload` 直接控制,手动处理: 1. 通过 `action` 属性指定自定义上传地址 2. 通过 `headers` 属性传入认证 Token(使用 `getHeaders()`) 3. 在 `@change` 回调中从 `response.message` 提取相对路径 4. 自定义 `getSpbbFileUrl()` 函数构造文件访问 URL(而非使用 `getFileAccessHttpUrl`) ### 15.9 关键源码索引(文件上传) | 组件/文件 | 路径 | |-----------|------| | JImageUpload 组件 | [`jeecgboot-vue3/src/components/Form/src/jeecg/components/JImageUpload.vue`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Form/src/jeecg/components/JImageUpload.vue) | | JUpload 组件 | [`jeecgboot-vue3/src/components/Form/src/jeecg/components/JUpload/JUpload.vue`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Form/src/jeecg/components/JUpload/JUpload.vue) | | 上传类型枚举 | [`jeecgboot-vue3/src/components/Form/src/jeecg/components/JUpload/upload.data.ts`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Form/src/jeecg/components/JUpload/upload.data.ts) | | 上传弹窗组件 | [`jeecgboot-vue3/src/components/Form/src/jeecg/components/JUpload/JUploadModal.vue`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Form/src/jeecg/components/JUpload/JUploadModal.vue) | | 导入按钮组件 | [`jeecgboot-vue3/src/components/Button/src/JUploadButton.vue`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Button/src/JUploadButton.vue) | | 通用 API 定义 | [`jeecgboot-vue3/src/api/common/api.ts`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/api/common/api.ts) | | 上传 API 封装 | [`jeecgboot-vue3/src/api/sys/upload.ts`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/api/sys/upload.ts) | | Axios 上传方法 | [`jeecgboot-vue3/src/utils/http/axios/Axios.ts`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/utils/http/axios/Axios.ts) | | 文件URL构造 | [`jeecgboot-vue3/src/utils/common/compUtils.ts`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/utils/common/compUtils.ts) | | 用户头像上传示例 | [`jeecgboot-vue3/src/views/system/user/user.data.ts`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/system/user/user.data.ts) | | 通知公告附件示例 | [`jeecgboot-vue3/src/views/system/notice/notice.data.ts`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/system/notice/notice.data.ts) | | JUpload Demo | [`jeecgboot-vue3/src/views/demo/jeecg/JUploadDemo.vue`](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/demo/jeecg/JUploadDemo.vue) | *** ## 十六、表单校验实现方式与代码规范 > 追加日期:2026-06-01 ### 16.1 表单校验体系总览 本项目前端存在 **三大表单校验体系**,分别服务于不同的业务场景: | 体系 | 核心组件 | 校验引擎 | 适用场景 | |------|---------|----------|---------| | **BasicForm** | `components/Form` | Ant Design Vue Form(底层 async-validator) | 主表单、一对一子表、通用表单 | | **原生 a-form** | `ant-design-vue` | Ant Design Vue Form(底层 async-validator) | 登录/注册/忘记密码页面 | | **JVxeTable** | `components/jeecg/JVxeTable` | VXE-Table 内置 EditRules | 一对多子表(行编辑表格) | **Online 动态表单** 组合使用 BasicForm + JVxeTable 两种引擎。 ### 16.2 BasicForm 校验体系(主体系) #### 架构层次 ``` Ant Design Vue Form (底层,内置 async-validator) └── BasicForm 组件 (封装层) ├── useForm.ts — 外部调用 Hook ├── useFormEvents.ts — 内部事件处理(validate/validateFields/clearValidate) ├── FormItem.vue — 校验规则处理核心(handleRules) ├── helper.ts — 校验辅助工具(占位提示/规则类型设置) └── validator.ts — 通用校验规则库(email/phone/duplicateCheck等) ``` #### 表单注册与校验调用 ```typescript // 1. 在 xxx.data.ts 中定义 formSchema(含校验规则) export const formSchema: FormSchema[] = [ { field: 'username', label: '用户名', component: 'Input', required: true }, { field: 'email', label: '邮箱', component: 'Input', dynamicRules: ({ model, schema }) => rules.duplicateCheckRule('sys_user', 'email', model, schema, false) }, ]; // 2. 在 Vue 组件中注册表单 const [registerForm, { validate, resetFields, setFieldsValue }] = useForm({ schemas: formSchema, showActionButtonGroup: false, }); // 3. 提交时校验 async function handleSubmit() { try { const values = await validate(); // ... 提交逻辑 } catch (error) { // 校验失败,error 包含 errorFields } } ``` #### 校验规则的三种定义方式 **方式一:`required` 属性(简单必填)** ```typescript // 布尔值 { field: 'username', required: true } // 函数形式(动态判断) { field: 'accessed', required: ({ values }) => values.needAccess === true } ``` **方式二:`rules` 数组(静态规则)** ```typescript { field: 'phone', rules: [ { required: false, message: '请输入正确的手机号', pattern: /^1[3456789]\d{9}$/, trigger: 'blur' } ]} { field: 'password', rules: [ { required: true, message: '请输入密码' }, { pattern: /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[~!@#$%^&*()_+`\-={}:";'<>?,./]).{8,}$/, message: '密码需8位以上,包含数字、大小写字母和特殊符号' } ]} ``` **方式三:`dynamicRules` 函数(动态规则 — 推荐)** ```typescript // 唯一性校验(调后端接口) { field: 'username', dynamicRules: ({ model, schema }) => rules.duplicateCheckRule('sys_user', 'username', model, schema, true) } // 确认密码校验(依赖其他字段值) { field: 'confirmPassword', dynamicRules: ({ values }) => rules.confirmPassword(values, true) } // 邮箱格式 + 唯一性组合 { field: 'email', dynamicRules: ({ model, schema }) => [ { ...rules.duplicateCheckRule('sys_user', 'email', model, schema, false)[0], trigger: 'blur' }, { ...rules.rule('email', false)[0], trigger: 'blur' }, ] } // 自定义 validator(如角色编码唯一性) { field: 'roleCode', dynamicRules: ({ values, model }) => [{ required: true, validator: (_, value) => { if (!value) return Promise.reject('请输入角色编码'); return new Promise((resolve, reject) => { isRoleExist({ id: model.id, roleCode: value }) .then(res => res.success ? resolve() : reject(res.message || '校验失败')) .catch(err => reject(err.message || '验证失败')); }); }, }] } ``` #### `handleRules()` 核心逻辑 [FormItem.vue](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Form/src/components/FormItem.vue) 中的 `handleRules()` 函数是校验规则生成的核心: ``` handleRules() 执行流程: 1. 禁用状态 → clearValidate + 返回空规则 2. 无权限/隐藏 → 返回空规则 3. 有 dynamicRules → 执行函数获取规则数组 + required 补充 + hijackValidator 4. 有 rules → 深拷贝 + required 默认 validator 生成 + message 补充 + pattern 转正则 + hijackValidator 5. 仅 required → 生成内置 validator(覆盖 undefined/null/空数组/空字符串/Tree未选中等场景) ``` **关键特性**: - **禁用状态跳过校验**:全局或单项禁用时直接 `clearValidate` 并返回空规则 - **权限控制**:`v-auth` 指令控制显隐的字段,无权限时不校验 - **防重复校验**:`hijackValidator` 通过 100ms 开关机制防止 validator 被重复调用 - **pattern 字符串转正则**:支持将字符串类型的 pattern 转为 RegExp 对象 - **自动补充 message**:根据组件类型自动生成"请输入"/"请选择" + label 的提示信息 #### trigger 触发机制 | trigger 值 | 触发时机 | 适用组件 | |-----------|---------|---------| | `'change'` | 值变化时 | Select、Checkbox、Radio、Switch | | `'blur'` | 失焦时 | Input、InputTextArea、InputNumber | | `['change', 'blur']` | 值变化或失焦 | 通用 | | 不设置 | 默认(Ant Design Vue 内部决定) | 通用 | **FormItem.vue 中的特殊处理**: ```typescript // 如果规则中有 trigger: 'blur',则 change 事件时不自动校验 const findItem = getRules().find((item) => item?.trigger === 'blur'); if (!findItem) { // 没有blur触发器 → change时立即校验 props.validateFields([field]).catch((_) => {}); } ``` #### 校验方法 API | 方法 | 说明 | 来源 | |------|------|------| | `validate()` | 校验整个表单,返回 Promise<表单值> | useForm | | `validateFields(nameList)` | 校验指定字段 | useForm | | `clearValidate(name?)` | 清除校验信息 | useForm | | `scrollToField(name, options)` | 滚动到校验失败字段 | useForm | | `setFieldsValue(values)` | 设置字段值后自动触发 `validateFields` | useForm | ### 16.3 通用校验规则库 — validator.ts [validator.ts](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/utils/helper/validator.ts) 提供了项目级的通用校验方法: | 方法 | 功能 | 默认 trigger | |------|------|-------------| | `rules.rule(type, required)` | 根据 type 分发到 email/phone | 'change' | | `rules.email(required)` | 邮箱格式校验 | 'change' | | `rules.phone(required)` | 手机号格式校验 `/^1[3456789]\d{9}$/` | 'change' | | `rules.startTime(endTime, required)` | 开始时间 < 结束时间 | 'change' | | `rules.endTime(startTime, required)` | 结束时间 > 开始时间 | 'change' | | `rules.confirmPassword(values, required)` | 确认密码一致性校验 | 无(默认) | | `rules.duplicateCheckRule(tableName, fieldName, model, schema, required?)` | 后端唯一性校验(调 `/sys/duplicate/check`) | 无(默认) | | `duplicateValidate(tableName, fieldName, fieldVal, dataId)` | 原生 `` 用的唯一校验函数 | - | **使用规范**: ```typescript import { rules } from '/@/utils/helper/validator'; // 必填 + 唯一性校验 { field: 'username', required: true, dynamicRules: ({ model, schema }) => rules.duplicateCheckRule('sys_user', 'username', model, schema, true) } // 非必填 + 格式校验 + 唯一性校验(组合使用) { field: 'email', dynamicRules: ({ model, schema }) => [ { ...rules.duplicateCheckRule('sys_user', 'email', model, schema, false)[0], trigger: 'blur' }, { ...rules.rule('email', false)[0], trigger: 'blur' }, ] } // 确认密码 { field: 'confirmPassword', dynamicRules: ({ values }) => rules.confirmPassword(values, true) } ``` ### 16.4 原生 a-form 校验体系(登录页面) 登录/注册/忘记密码页面使用原生 `` 而非 BasicForm: ```vue ``` [useLogin.ts](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/sys/login/useLogin.ts) 提供两个 Hook: - **`useFormRules()`**:根据当前登录状态(登录/注册/重置密码/手机号)返回不同的校验规则 - **`useFormValid(formRef)`**:封装 `formRef.validate()` 为 `validForm()` 方法 | 登录状态 | 校验字段 | |---------|---------| | `LOGIN` | account 必填, password 必填 | | `REGISTER` | account 后端唯一, password 必填, mobile 格式+后端唯一, sms 必填, confirmPassword 一致性, policy 勾选 | | `RESET_PASSWORD` | username 必填, confirmPassword 一致性, sms 必填, mobile 必填 | | `MOBILE` | sms 必填, mobile 必填 | ### 16.5 JVxeTable 校验体系(行编辑表格) 在列配置中通过 `validateRules` 数组定义: ```typescript const columns = [ { key: 'name', title: '姓名', type: JVxeTypes.input, validateRules: [ { required: true, message: '姓名不能为空' }, { pattern: 'only', message: '姓名不能重复' }, // 唯一校验 ], }, { key: 'email', title: '邮箱', type: JVxeTypes.input, validateRules: [ { pattern: 'e', message: '邮箱格式不正确' }, // 预置正则 ], }, ]; ``` **预置正则规则**(Online 兼容,定义在 [useValidateRules.ts](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/jeecg/JVxeTable/src/hooks/useValidateRules.ts)): | 值 | 说明 | 正则 | |----|------|------| | `*` | 非空 | `/^.+$/` | | `n6-16` | 6到16位数字 | `/^\d{6,16}$/` | | `*6-16` | 6到16位任意字符 | `/^.{6,16}$/` | | `s6-18` | 6到18位字母 | `/^[a-z\|A-Z]{6,18}$/` | | `url` | 网址 | `/^((ht\|f)tps?):\/\/...$/` | | `e` | 电子邮件 | `/^[a-zA-Z0-9_-]+@...$/` | | `m` | 手机号码 | `/^1[3456789]\d{9}$/` | | `p` | 邮政编码 | `/^\d{6}$/` | | `s` | 字母 | `/^[A-Z\|a-z]+$/` | | `n` | 数字 | `/^-?\d+(\.?\d+\|\d?)$/` | | `z` | 整数 | `/^-?\d+$/` | | `money` | 金额 | `/^(([1-9][0-9]*)\|...)$/` | **校验执行方法**: ```typescript // 校验当前可见行(快速) const errMap = await instance.validateTable(); // 校验所有行(含虚拟滚动隐藏的行) const errMap = await instance.fullValidateTable(); ``` **唯一校验器**(前端遍历表格数据,非后端接口): ```typescript function uniqueValidator({ methods }) { return function (event) { const { cellValue, column, rule } = event; if (cellValue == '') return Promise.resolve(); let tableData = methods.getTableData(); let findCount = 0; for (let rowData of tableData) { if (rowData[column.params.key] === cellValue) { if (++findCount >= 2) { return Promise.reject(new Error(rule.message)); } } } return Promise.resolve(); }; } ``` ### 16.6 Online 动态表单校验 **校验流程**: ``` handleSubmit() ├── 单表 → validate() [BasicForm] └── 主子表 → validateAll() ├── 第1步:validate() [主表 BasicForm] ├── 第2步:validateSubTableFields() │ ├── 一对一子表 → instance.getAll() → validate() [BasicForm] │ └── 一对多子表 → instance.fullValidateTable() [JVxeTable] └── 第3步:合并所有数据 ``` **校验失败自动定位**: - **Tab 自动切换**:校验失败时自动切换到出错子表 - **滚动定位**:`scrollToField(errorFields[0].name, { behavior: 'smooth', block: 'center' })` - **一对多子表切换后重新校验**:`sleep(300, () => instance?.validateTable())` **Online 唯一校验**(后端 API): ```typescript function checkOnlyFieldValue(rule, value) { return new Promise((resolve) => { if (!value) resolve(''); let param = { tableName: realTableName, fieldName: rule.field, fieldVal: value }; if (formData.id) param['dataId'] = formData.id; duplicateCheck(param) .then(res => res.success ? resolve('') : resolve(res.message)) .catch(msg => resolve(msg)); }); } ``` ### 16.7 校验代码规范 #### 校验规则定义规范 | 规范项 | 要求 | 示例 | |--------|------|------| | **规则定义位置** | 统一放在 `xxx.data.ts` 文件中 | `user.data.ts`、`role.data.ts` | | **简单必填** | 使用 `required: true` | `{ field: 'name', required: true }` | | **格式校验** | 使用 `rules` 数组 + `trigger: 'blur'` | `{ rules: [{ pattern: /.../, trigger: 'blur' }] }` | | **唯一性校验** | 使用 `dynamicRules` + `rules.duplicateCheckRule()` | `dynamicRules: ({ model, schema }) => rules.duplicateCheckRule(...)` | | **依赖其他字段的校验** | 使用 `dynamicRules` | `dynamicRules: ({ values }) => rules.confirmPassword(values, true)` | | **后端校验** | 使用 `dynamicRules` + 自定义 `validator` | `dynamicRules: ({ model }) => [{ validator: (_, value) => apiCheck(value) }]` | #### trigger 使用规范 | 组件类型 | 推荐 trigger | 原因 | |---------|-------------|------| | Input / InputTextArea / InputNumber | `'blur'` | 避免输入过程中频繁触发校验 | | Select / Checkbox / Radio / Switch | `'change'` | 选择即确定,可立即校验 | | 唯一性校验 | `'blur'` | 减少后端请求频率 | | 格式校验(邮箱/手机/正则) | `'blur'` | 用户完成输入后再校验 | | 确认密码 | 不设置(默认) | 值变化时立即校验一致性 | #### message 提示规范 | 场景 | message 格式 | 示例 | |------|-------------|------| | 必填(输入类) | `请输入${label}` | `请输入用户名` | | 必填(选择类) | `请选择${label}` | `请选择部门` | | 格式错误 | `${label}格式不正确` | `手机号码格式有误` | | 唯一性失败 | `${label}已存在` | `用户名已存在` | | 长度限制 | `长度在 ${min} 到 ${max} 个字符` | `长度在 2 到 30 个字符` | | 自定义错误 | 后端返回的 message | `duplicateCheck` 接口返回值 | **自动 message 生成**:当 `required: true` 但未指定 `message` 时,FormItem.vue 会根据组件类型自动生成"请输入"/"请选择" + label 的提示。 #### 校验调用规范 ```typescript // ✅ 推荐:使用 useForm 返回的 validate() async function handleSubmit() { try { const values = await validate(); await saveApi(values); emit('success'); } catch (error) { // 校验失败,Ant Design Vue 自动显示错误提示 } finally { setDrawerProps({ confirmLoading: false }); } } // ❌ 不推荐:手动 try-catch validate 后不处理错误 async function handleSubmit() { const values = await validate(); // 可能抛出异常未捕获 await saveApi(values); } ``` ### 16.8 校验流程全景图 ``` ┌─────────────────────────────────────────────────────────────────┐ │ 表单校验执行流程 │ │ │ │ 用户输入/选择 │ │ ↓ │ │ 值变更事件 (change/blur) │ │ ↓ │ │ FormItem.renderComponent() │ │ ├── 有 trigger:'blur' 规则? │ │ │ ├── 是 → 等 blur 事件由 Ant Design Vue 内部触发校验 │ │ │ └── 否 → 立即 validateFields([field]) │ │ ↓ │ │ FormItem.handleRules() 生成规则 │ │ ├── 禁用/隐藏/无权限 → 返回空规则 │ │ ├── dynamicRules → 执行函数获取规则 │ │ ├── rules → 深拷贝 + 增强 │ │ └── required → 生成内置 validator │ │ ↓ │ │ hijackValidator() 防重复执行 │ │ ↓ │ │ Ant Design Vue Form.Item rules 绑定 │ │ ↓ │ │ async-validator 执行校验 │ │ ↓ │ │ 校验结果:通过 → 无提示 / 失败 → 红色提示 │ │ ↓ │ │ 提交时:validate() 全量校验 │ │ ├── 成功 → 返回表单值 │ │ └── 失败 → 抛出异常(含 errorFields) │ │ └── scrollToField() 滚动到错误字段 │ └─────────────────────────────────────────────────────────────────┘ ``` ### 16.9 关键源码索引(表单校验) | 文件 | 作用 | |------|------| | [useForm.ts](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Form/src/hooks/useForm.ts) | useForm Hook,外部调用入口 | | [useFormEvents.ts](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Form/src/hooks/useFormEvents.ts) | 表单事件处理(validate/validateFields) | | [FormItem.vue](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Form/src/components/FormItem.vue) | 校验规则处理核心(handleRules) | | [form.ts](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Form/src/types/form.ts) | FormSchema/Rule 类型定义 | | [helper.ts](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Form/src/helper.ts) | 校验辅助(占位提示/规则类型) | | [validator.ts](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/utils/helper/validator.ts) | 通用校验规则库 | | [useValidateRules.ts](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/jeecg/JVxeTable/src/hooks/useValidateRules.ts) | JVxeTable 校验规则 | | [useLogin.ts](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/sys/login/useLogin.ts) | 登录表单校验 Hook | | [user.data.ts](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/system/user/user.data.ts) | 用户表单校验规则示例 | | [IFormSchema.ts](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/super/online/cgform/auto/comp/factory/IFormSchema.ts) | Online 表单 Schema 基类 | | [useAutoForm.ts](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/super/online/cgform/hooks/auto/useAutoForm.ts) | Online 表单唯一校验 | *** ## 十七、BasicTable 插槽(Slot)用法详解 > 追加日期:2026-06-02 ### 17.1 插槽机制概述 BasicTable 的插槽机制由以下核心文件协同实现: | 文件 | 作用 | |----------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------| | [BasicTable.vue](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Table/src/BasicTable.vue) | 主组件,模板中定义插槽透传逻辑 | | [useTableHeader.ts](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Table/src/hooks/useTableHeader.ts) | 处理表头区域插槽(tableTitle, toolbar, headerTop, tableTop, alertAfter) | | [useTableForm.ts](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Table/src/hooks/useTableForm.ts) | 处理搜索表单插槽(form-xxx) | | [TableHeader.vue](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Table/src/components/TableHeader.vue) | 表头区域布局组件,渲染 headerTop/tableTitle/toolbar/tableTop/alertAfter | **插槽分类流程 **([BasicTable.vue](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Table/src/BasicTable.vue) 中 `slotNamesGroup` 计算属性): ``` 用户在 中定义的插槽 | v slotNamesGroup 计算属性自动分类 | +----+----+ | | v v native组 custom组 + 单独处理的插槽 | | | v v v 直接透传 列级自定义 bodyCell: 优先匹配 slotsBak.customRender 给 antd 插槽渲染 form-xxx: 去掉前缀后传给 BasicForm tableTitle/toolbar/headerTop/tableTop/alertAfter: 由 useTableHeader 传给 TableHeader 组件 expandedRowRender/summary: 直接在 模板中使用 ``` ### 17.2 完整插槽列表 #### 第一类:表头区域插槽 ##### 1. `tableTitle` — 表格标题/操作按钮区域 - **位置**:表格标题栏左侧 - **用途**:放置新增、导出、导入、批量操作等按钮,**最常用的插槽** - **作用域参数**:无 ```vue 新增 导出 导入 删除 批量操作 ``` **源码实现 **:[useTableHeader.ts](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Table/src/hooks/useTableHeader.ts) 将 `tableTitle` 插槽通过 `h()` 函数传递给 [TableHeader.vue](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Table/src/components/TableHeader.vue) ,TableHeader 中将其渲染在左侧 `__tableTitle` 区域。当未使用此插槽但配置了 `title` prop 时,会渲染默认的 `TableTitle` 组件。 ##### 2. `toolbar` — 工具栏区域 - **位置**:表格标题栏右侧,与 TableSetting(列配置按钮)同行 - **用途**:放置工具按钮,如"获取表单数据"、"切换自适应高度"等 - **作用域参数**:无 ```vue 获取表单数据 ``` **源码实现**:TableHeader 中 `toolbar` 插槽渲染在右侧 `__toolbar` 区域,后面紧跟 `TableSetting` 组件。当 `toolbar` 插槽和 `showTableSetting` 同时存在时,中间会自动添加竖线分隔符 `Divider`。 ##### 3. `headerTop` — 表头顶部区域 - **位置**:表格标题栏上方,独立一行 - **用途**:放置提示信息、自定义选中状态显示等 - **作用域参数**:无 ```vue 已选中{{ checkedKeys.length }}条记录(可跨页) 清空 未选中任何项目 ``` **源码实现**:TableHeader 中 `headerTop` 插槽在最顶部渲染,外层包裹 `margin: 5px` 的 div。 ##### 4. `tableTop` — 表头下方区域(替代默认选中提示) - **位置**:标题栏下方,默认是选中行数的 alert 提示 - **用途**:自定义选中提示区域,或用空内容隐藏默认提示 - **作用域参数**:无 ```vue 自定义提示内容 ``` **源码实现**:TableHeader 中 `tableTop` 插槽有默认内容——一个 `` 显示"已选中 X 条记录"和"清空"链接。使用此插槽会* *完全替换**默认提示。 ##### 5. `alertAfter` — 选中提示后方区域 - **位置**:紧跟在默认选中提示 alert 之后 - **用途**:在选中提示后追加自定义内容 - **作用域参数**:无 **源码实现**:`alertAfter` 插槽嵌套在 `tableTop` 默认内容的 `` 内部,仅在选中行数 > 0 时显示。项目暂未发现实际使用案例。 #### 第二类:表格内容区域插槽 ##### 6. `bodyCell` — 单元格内容(antdv3.x 兼容) - **位置**:表格所有单元格 - **用途**:统一拦截所有单元格渲染,根据 `column.key` 判断是否自定义渲染 - **作用域参数**:`{ column, record, text, index, renderIndex }` ```vue ``` **重要机制 **:[BasicTable.vue](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Table/src/BasicTable.vue) 中 `bodyCell` 插槽有特殊逻辑: ```vue ``` 即:如果列配置了 `slots.customRender`,会优先使用列级命名插槽;否则走 `bodyCell` 全局插槽。 ##### 7. `expandedRowRender` — 展开行内容 - **位置**:点击行展开图标后显示的子内容区域 - **用途**:展示子表、详情信息、嵌套表格等 - **作用域参数**:`{ record, index, indent, expanded }` ```vue No: {{ record.no }} ``` ##### 8. `summary` — 表尾合计区域 - **位置**:表格底部合计行 - **用途**:自定义合计行渲染,覆盖默认的 TableSummary 组件 - **作用域参数**:`{ pageData }`(当前页数据) ```vue 合计 {{ totalAmount }} ``` **生效条件**:此插槽仅在 `showSummaryRef` 为 true(即配置了 `summaryFunc` 或 `summaryData` 且有数据)且 `showSummary` prop 为 false 时生效。默认情况下使用内置 TableSummary 组件渲染。 ##### 9. `headerCell` — 表头单元格 - **位置**:表头单元格 - **用途**:自定义表头单元格渲染 - **作用域参数**:`{ column, title }` **特殊处理**:当列是自定义选择列时,会渲染 `CustomSelectHeader` 而非用户自定义内容(解决全选框不显示的问题)。 #### 第三类:列级自定义插槽 ##### 10. 列内容自定义插槽(`slots.customRender`) - **定义方式**:在列配置中设置 `slots: { customRender: 'slotName' }` - **作用域参数**:`{ text, record, column, index, renderIndex }` ```typescript // 列配置 const columns: BasicColumn[] = [ {title: 'ID', dataIndex: 'id', slots: {customRender: 'id'}}, {title: '编号', dataIndex: 'no', slots: {customRender: 'no'}}, {title: '图片', dataIndex: 'imgArr', slots: {customRender: 'img'}}, ]; ``` ```vue ID: {{ record.id }} {{ record.no }} ``` **Online 表单中的典型用法**: ```vue 无文件 下载 无图片 {{ getPcaText(text, column) }} {{ getFormatDate(text, column) }} ``` ##### 11. 列标题自定义插槽(`slots.title`) - **定义方式**:在列配置中设置 `slots: { title: 'slotName' }` - **作用域参数**:无 ```typescript // 列配置 { dataIndex: 'name', slots : { title: 'customTitle' } } { dataIndex: 'address', slots : { title: 'customAddress' } } ``` ```vue 姓名 地址 ``` ##### 12. `action` — 操作列插槽(最常用的列级插槽) - **定义方式**:通过 `actionColumn` 配置自动生成,内部设置 `slots: { customRender: 'action' }` - **作用域参数**:`{ record, index }` ```vue ``` #### 第四类:搜索表单插槽 ##### 13. `form-xxx` — 搜索表单字段自定义插槽 - **定义方式**:在 FormSchema 中配置 `slot: 'slotName'`,使用时插槽名为 `form-slotName` - **作用域参数**:`{ model, field, values }`(model 是表单数据对象,field 是字段名) ```typescript // FormSchema 配置 { field: 'email', component : 'JInput', slot : 'email' } { field: 'age', component : 'JInput', slot : 'age' } ``` ```vue ~ ``` **机制说明 **:[useTableForm.ts](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Table/src/hooks/useTableForm.ts) 中,`getFormSlotKeys` 计算属性会自动识别所有以 `form-` 开头的插槽,`replaceFormSlotKey` 函数将 `form-` 前缀去掉后映射到 BasicForm 的对应插槽。 #### 第五类:Ant Design 原生插槽透传 `slotNamesGroup` 计算属性会将非 `bodyCell` 且非列级自定义的插槽归入 `native` 组,直接透传给底层 `` 组件。因此 Ant Design Vue Table 支持的所有原生插槽都可以直接使用,包括但不限于: | 原生插槽 | 作用域参数 | 用途 | |--------------------|----------------------------------|----------| | `headerCell` | `{ column, title }` | 自定义表头单元格 | | `customFilterIcon` | `{ filtered }` | 自定义筛选图标 | | `expandIcon` | `{ expanded, onExpand, record }` | 自定义展开图标 | | `emptyText` | 无 | 空数据提示 | ### 17.3 插槽分类汇总表 | 插槽名 | 类别 | 作用域参数 | 典型放置组件 | 使用频率 | |---------------------|-------|---------------------------------------|---------------------------------------------|------| | `tableTitle` | 表头区域 | 无 | a-button, a-dropdown, j-upload-button, Icon | 极高 | | `toolbar` | 表头区域 | 无 | a-button | 中 | | `headerTop` | 表头区域 | 无 | a-alert | 低 | | `tableTop` | 表头区域 | 无 | 自定义内容/空span(隐藏默认提示) | 低 | | `alertAfter` | 表头区域 | 无 | 自定义内容 | 极低 | | `bodyCell` | 表格内容 | `{ column, record, text, index }` | Avatar, Tag, 任意组件 | 中 | | `expandedRowRender` | 表格内容 | `{ record, index, indent, expanded }` | a-tabs, 子表格, 详情面板 | 中 | | `summary` | 表格内容 | `{ pageData }` | TableSummary | 低 | | `headerCell` | 表格内容 | `{ column, title }` | 自定义表头 | 低 | | `action` | 列级自定义 | `{ record, index }` | TableAction | 极高 | | 自定义列插槽 | 列级自定义 | `{ text, record, column, index }` | Tag, TableImg, a-button, img | 高 | | 自定义标题插槽 | 列级自定义 | 无 | BasicHelp, Icon, 自定义标题 | 低 | | `form-xxx` | 搜索表单 | `{ model, field, values }` | a-input, 自定义表单控件 | 中 | | Ant原生插槽 | 原生透传 | 视具体插槽而定 | 视具体插槽而定 | 低 | ### 17.4 插槽中可放置的前端组件 BasicTable 的插槽本质上是 Vue 的标准插槽机制,可以放置**任何合法的 Vue 组件或 HTML 内容**。项目中实际使用的组件包括: #### Ant Design Vue 组件 | 组件 | 常用插槽 | 用途 | |-----------------------------------------|-----------------------------|-----------| | `a-button` | tableTitle, toolbar, action | 操作按钮(最常见) | | `a-dropdown` / `a-menu` / `a-menu-item` | tableTitle | 下拉菜单/批量操作 | | `a-alert` | headerTop, tableTop | 提示信息 | | `a-input` | form-xxx | 输入框(表单插槽) | | `a-tabs` / `a-tab-pane` | expandedRowRender | 标签页(展开行) | | `Tag` | 列自定义插槽 | 标签展示 | | `Avatar` | bodyCell | 头像展示 | | `a-divider` | tableTitle | 分隔线 | #### 项目自定义组件 | 组件 | 常用插槽 | 用途 | |--------------------------|---------------------|---------------------------| | `TableAction` | action | 操作列组件(配合 action 插槽使用,最核心) | | `TableImg` | 列自定义插槽 | 图片展示组件 | | `BasicHelp` | 列标题插槽 | 帮助提示组件 | | `Icon` | tableTitle, toolbar | 图标组件 | | `j-upload-button` | tableTitle | 上传按钮 | | `OnlineSuperQuery` | tableTitle | 高级查询组件 | | `OnlCgformInnerSubTable` | expandedRowRender | 子表组件 | | `import-excel-progress` | tableTitle | 导入进度组件 | #### 原生 HTML - `span`, `div`, `img`, `a` 等基础标签 - `v-html` 渲染富文本内容 #### 任意自定义 Vue 组件 只要是合法的 Vue 组件,都可以放入插槽中使用。 ### 17.5 插槽使用代码规范 #### 规范一:操作按钮统一放在 `tableTitle` 插槽 ```vue 新增 导出 导入 ``` #### 规范二:操作列统一使用 `action` 插槽 + `TableAction` 组件 ```vue 编辑 删除 ``` #### 规范三:列自定义渲染优先使用 `slots.customRender` 而非 `bodyCell` ```typescript // ✅ 推荐:使用列级插槽,语义清晰 { title: '状态', dataIndex : 'status', slots : { customRender: 'status' } } // 对应模板: ... // ❌ 不推荐:使用 bodyCell 全局拦截,所有列都在一个插槽中判断 // // ... // ... // ``` #### 规范四:搜索表单自定义控件使用 `form-xxx` 插槽 ```typescript // data.ts 中定义 { field: 'age', component : 'JInput', slot : 'age' } ``` ```vue ``` #### 规范五:隐藏默认选中提示时使用空 `tableTop` 插槽 ```vue ``` #### 规范六:批量操作按钮使用 `a-dropdown` 并条件显示 ```vue 删除 批量操作 ``` #### 规范七:按钮权限控制 ```vue 新增 function getTableAction(record): ActionItem[] { return [ { label: '编辑', onClick: handleEdit.bind(null, record), auth: 'spbb:sppb_video:edit' }, ]; } ``` ### 17.6 关键源码索引(BasicTable 插槽) | 文件 | 作用 | |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------| | [BasicTable.vue](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Table/src/BasicTable.vue) | 主组件,定义插槽透传逻辑和 slotNamesGroup 分类 | | [useTableHeader.ts](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Table/src/hooks/useTableHeader.ts) | 处理表头区域插槽传递给 TableHeader | | [useTableForm.ts](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Table/src/hooks/useTableForm.ts) | 处理 form-xxx 插槽的识别和前缀替换 | | [TableHeader.vue](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Table/src/components/TableHeader.vue) | 表头布局组件,渲染 headerTop/tableTitle/toolbar/tableTop/alertAfter | | [CustomerCell.vue](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/demo/table/CustomerCell.vue) | 列自定义插槽 + bodyCell 使用示例 | | [ExpandTable.vue](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/demo/table/ExpandTable.vue) | expandedRowRender 使用示例 | | [FormTable.vue](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/demo/table/FormTable.vue) | headerTop/toolbar/form-xxx 使用示例 | | [BasicTableDemoAjax.vue](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/demo/document/table/BasicTableDemoAjax.vue) | form-email 自定义搜索示例 | | [OnlCgformInnerTableList.vue](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/super/online/cgform/auto/innerTable/OnlCgformInnerTableList.vue) | expandedRowRender + Online 列插槽综合示例 |