# 前端页面实现模式分析
> 分析日期: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 表单唯一校验 |