分析日期:2026-06-03 框架版本:JeecgBoot 3.9.2(基于 Vben-Admin Vue3 深度定制) 分析范围:JeecgBoot 原框架(排除
views/recruitment和views/spbb业务目录)
| 层级 | 技术 |
|---|---|
| 框架基础 | Vue3 + TypeScript + Vite |
| UI组件库 | Ant Design Vue 3.x |
| 状态管理 | Pinia |
| 路由 | Vue Router 4.x |
| 表格核心 | Vben BasicTable + VXE Table |
| HTTP | Axios(VAxios 封装) |
| 图标 | Ant Design Icons + Iconify |
| 国际化 | vue-i18n |
| 富文本 | Tinymce |
| 图表 | ECharts |
| 工具库 | lodash-es + dayjs + big.js |
jeecgboot-vue3/src/
├── api/ # 全局公共 API(系统接口)
├── assets/ # 静态资源
├── components/ # 通用组件
│ ├── Form/ # BasicForm + 46种表单组件
│ ├── Table/ # BasicTable
│ ├── Modal/ # BasicModal / JModal
│ ├── Drawer/ # BasicDrawer
│ ├── jeecg/ # JeecgBoot 业务组件(JVxeTable/JPrompt/OnLine等)
│ └── registerGlobComp.ts # 全局组件注册
├── directives/ # 自定义指令(v-auth/v-loading等)
├── enums/ # 枚举定义
├── hooks/ # 自定义 Hooks
│ ├── system/ # useListPage/useMethods/useJvxeMethods
│ ├── web/ # usePermission/useMessage/useWebSocket/useECharts
│ ├── core/ # useAttrs/useContext/useLockFn/useRefs
│ ├── event/ # useBreakpoint/useEventListener/useScroll
│ ├── setting/ # useGlobSetting/useRootSetting/useMenuSetting
│ ├── jeecg/ # useAdaptiveWidth
│ └── component/ # useFormItem/usePageContext
├── layouts/ # 布局组件(default/iframe/page)
├── router/ # 路由配置 + 守卫
├── store/ # Pinia Store(8个模块)
├── utils/ # 工具类
│ ├── http/axios/ # HTTP 封装(VAxios + defHttp)
│ ├── auth/ # 认证工具
│ ├── cache/ # 缓存体系
│ ├── common/ # 通用工具(compUtils/renderUtils/vxeUtils)
│ └── dict/ # 字典工具
├── settings/ # 全局配置
└── views/ # 业务页面
├── system/ # 系统管理模块(17+子模块)
├── demo/ # 各种示例
└── super/online/ # Online 表单
模块名/
index.vue # 列表主页(使用 useListPage + BasicTable)
xxx.data.ts # columns + searchFormSchema + formSchema
xxx.api.ts # 所有接口定义(enum Api + 导出函数)
XxxModal.vue # 新增/编辑弹窗(使用 BasicModal + BasicForm)
典型代表:system/notice/、system/fillRule/、system/checkRule/、system/position/、system/examples/demo/
模块名/
index.vue # 主表列表+子表Tab区域
xxx.data.ts # 主表 + 所有子表的 columns/formSchema
xxx.api.ts # 主表 + 所有子表的 CRUD 接口
MainModal.vue # 主表编辑弹窗
SubTableList.vue # 子表列表组件(每个子表一个)
components/
SubModal.vue # 子表编辑弹窗(每个子表一个)
典型代表:demo/jeecg/erplist/
模块名/
index.vue # 左树右表主页面
xxx.data.ts # 表格 columns + 表单 schema
xxx.api.ts # 树接口 + 列表接口 + CRUD接口
components/
XxxModal.vue # 编辑弹窗
LeftTree.vue # 左侧树组件(可选)
典型代表:system/category/(纯树形)、system/depart/(左树右表)
import { defHttp } from '/@/utils/http/axios';
import { Modal } from 'ant-design-vue';
enum Api {
list = '/模块/实体/list',
save = '/模块/实体/add',
edit = '/模块/实体/edit',
get = '/模块/实体/queryById',
delete = '/模块/实体/delete',
deleteBatch = '/模块/实体/deleteBatch',
exportXls = '/模块/实体/exportXls',
importExcel = '/模块/实体/importExcel',
}
/** 导出 url(供导入导出组件使用) */
export const getExportUrl = Api.exportXls;
/** 导入 url */
export const getImportUrl = Api.importExcel;
/** 列表查询 */
export const list = (params) => defHttp.get({ url: Api.list, params });
/** 通过ID查询 */
export const getById = (params) => defHttp.get({ url: Api.get, params });
/** 保存或更新 */
export const saveOrUpdate = (params, isUpdate) => {
let url = isUpdate ? Api.edit : Api.save;
return defHttp.post({ url: url, params });
};
/** 单条删除 */
export const deleteOne = (params, handleSuccess) => {
return defHttp.delete({ url: Api.delete, params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
};
/** 批量删除 */
export const batchDelete = (params, handleSuccess) => {
Modal.confirm({
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
},
});
};
| 要点 | 规范 |
|---|---|
| URL定义 | 使用 enum Api 集中管理,不要散落 |
| GET请求 | defHttp.get({ url, params }) |
| POST请求 | defHttp.post({ url, params }) |
| DELETE请求 | defHttp.delete({ url, params/data }, { joinParamsToUrl: true }) |
| 导出URL | 单独 export const getExportUrl = Api.exportXls |
| 导入URL | 单独 export const getImportUrl = Api.importExcel |
| 保存/更新合并 | saveOrUpdate(params, isUpdate) 通过判断URL区分新增/编辑 |
| 删除回调 | 第二个参数 handleSuccess 通常传入 reload |
import { BasicColumn } from '/@/components/Table';
import { render } from '/@/utils/common/renderUtils';
export const columns: BasicColumn[] = [
{
title: '姓名',
dataIndex: 'name',
width: 170,
align: 'left',
resizable: true, // 允许拖拽调整列宽
sorter: { multiple: 1 }, // 支持多字段排序
},
{
title: '性别',
dataIndex: 'sex',
width: 120,
resizable: true,
sorter: { multiple: 2 },
customRender: ({ text }) => {
return render.renderDict(text, 'sex'); // 字典翻译渲染
},
},
{
title: '生日',
dataIndex: 'birthday',
width: 120,
resizable: true,
},
{
title: '订单类型', // 非字典的固定选项翻译
dataIndex: 'ctype',
width: 160,
customRender: ({ text }) => {
return text == '1' ? '国内订单' : text == '2' ? '国际订单' : '';
},
},
];
import { FormSchema } from '/@/components/Table';
export const searchFormSchema: FormSchema[] = [
{
field: 'name',
label: '姓名',
component: 'Input',
componentProps: { trim: true }, // 自动去首尾空格
colProps: { span: 6 },
},
{
field: 'birthday',
label: '生日',
component: 'RangePicker', // 日期范围查询
componentProps: { valueType: 'Date' },
colProps: { span: 8 },
},
{
field: 'age',
label: '年龄',
component: 'Input', // 区间查询通过 slot 自定义
slot: 'age',
colProps: { span: 8 },
},
{
field: 'sex',
label: '性别',
component: 'JDictSelectTag', // 字典下拉
componentProps: {
dictCode: 'sex',
placeholder: '请选择性别',
},
colProps: { span: 8 },
},
{
field: 'departId',
label: '部门',
component: 'JSelectDept', // 部门选择
colProps: { span: 6 },
},
{
field: 'status',
label: '状态',
component: 'JDictSelectTag',
componentProps: {
dictCode: 'valid_status',
placeholder: '请选择状态',
},
colProps: { span: 6 },
},
];
export const formSchema: FormSchema[] = [
{
field: 'id',
label: 'id',
component: 'Input',
show: false, // 隐藏字段(主键等)
},
{
field: 'name',
label: '名称',
component: 'Input',
required: true, // 必填校验
componentProps: { placeholder: '请输入名称' },
},
{
field: 'sex',
label: '性别',
component: 'JDictSelectTag',
defaultValue: '1', // 默认值
componentProps: {
type: 'radio', // 字典渲染为单选按钮组
dictCode: 'sex',
},
},
{
field: 'birthday',
label: '生日',
component: 'DatePicker',
componentProps: {
valueFormat: 'YYYY-MM-DD', // 日期格式化
placeholder: '请选择生日',
},
},
{
field: 'email',
label: '邮箱',
component: 'Input',
rules: [{ required: false, type: 'email', message: '邮箱格式不正确', trigger: 'blur' }],
componentProps: { placeholder: '请输入邮箱' },
},
{
field: 'orderId', // 主子表外键字段
label: 'orderId',
component: 'Input',
show: false, // 隐藏
},
];
Ant Design Vue 基础组件(22种):
| component值 | 说明 | 常用componentProps |
|---|---|---|
Input |
输入框 | trim, placeholder |
InputTextArea |
多行文本 | rows, placeholder |
InputNumber |
数字输入 | min, max, step |
InputPassword |
密码框 | placeholder |
Select |
下拉选择 | options: [{label, value}] |
TreeSelect |
树选择 | treeData, fieldNames |
Switch |
开关 | checkedChildren, unCheckedChildren |
RadioGroup |
单选组 | options: [{label, value}] |
CheckboxGroup |
复选组 | options |
Cascader |
级联选择 | options |
DatePicker |
日期选择 | valueFormat: 'YYYY-MM-DD' |
MonthPicker |
月份选择 | valueFormat |
RangePicker |
日期范围 | valueType: 'Date' |
TimePicker |
时间选择 | valueFormat |
Slider |
滑动条 | min, max |
Rate |
评分 | count |
AutoComplete |
自动完成 | options |
Divider |
分隔线 | — |
JeecgBoot 业务组件(32种,J开头为主):
| component值 | 说明 | 常用场景 |
|---|---|---|
JDictSelectTag |
字典下拉/单选/多选 | 所有字典字段 |
JInput |
增强输入框(支持模糊/不等于等查询模式) | 查询条件 |
JImageUpload |
图片上传 | 图片字段 |
JUpload |
文件上传 | 附件字段 |
JEditor |
富文本编辑器 | 富文本内容 |
JMarkdownEditor |
Markdown编辑器 | Markdown内容 |
JCodeEditor |
代码编辑器 | 代码/SQL/JS |
JPopup |
Popup弹出选择 | 弹窗选择关联数据 |
JPopupDict |
字典Popup弹框 | 表字典选择 |
JSelectDept |
部门选择 | 部门字段 |
JSelectUser / UserSelect |
用户选择 | 用户字段 |
JSelectRole / RoleSelect |
角色选择 | 角色字段 |
JSelectPosition |
岗位选择 | 岗位字段 |
JAreaLinkage / JAreaSelect |
省市区联动 | 地区字段 |
JCategorySelect |
分类选择 | 分类字典 |
JTreeDict |
字典树 | 树形字典 |
JTreeSelect |
自定义树选择 | 树形数据选择 |
JSwitch |
自定义开关 | 开关字段 |
JSelectInput |
下拉+输入组合 | 灵活输入 |
JSelectMultiple |
多选下拉 | 多选字段 |
JSearchSelect |
搜索选择 | 异步搜索选择 |
JEasyCron |
Cron表达式 | 定时任务配置 |
JCheckbox |
自定义复选框 | 多选字段 |
JInputPop |
弹出输入框 | 大文本输入 |
JLinkTableCard |
关联记录卡片 | 关联表展示 |
JAddInput |
动态添加输入框 | 多值输入 |
JRangeNumber |
数字范围输入 | 区间查询 |
JInputSelect |
输入选择 | 输入+下拉切换 |
RangeDate |
日期范围 | 日期区间 |
RangeTime |
时间范围 | 时间区间 |
ApiSelect |
API远程下拉 | 接口数据下拉 |
ApiTreeSelect |
API远程树选择 | 接口树数据 |
IconPicker |
图标选择器 | 菜单图标 |
<template>
<div>
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<template #tableTitle>
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleCreate">新增</a-button>
<a-button type="primary" preIcon="ant-design:export-outlined" @click="onExportXls">导出</a-button>
<j-upload-button type="primary" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>删除
</a-menu-item>
</a-menu>
</template>
<a-button>批量操作 <Icon icon="ant-design:down-outlined"></Icon></a-button>
</a-dropdown>
</template>
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" />
</template>
</BasicTable>
<XxxModal @register="registerModal" @success="reload" />
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { BasicTable, TableAction } from '/@/components/Table';
import { useModal } from '/@/components/Modal';
import { useListPage } from '/@/hooks/system/useListPage';
import XxxModal from './XxxModal.vue';
import { columns, searchFormSchema } from './xxx.data';
import { list, deleteOne, batchDelete, getExportUrl, getImportUrl } from './xxx.api';
const [registerModal, { openModal }] = useModal();
// useListPage 统一管理表格+导入导出
const { tableContext, onExportXls, onImportXls } = useListPage({
designScope: 'xxx-template', // 样式作用域(可选)
tableProps: {
api: list, // 列表接口
columns: columns, // 列定义
formConfig: {
schemas: searchFormSchema, // 查询表单
},
actionColumn: {
width: 180, // 操作列宽度
},
},
exportConfig: {
name: '导出文件名',
url: getExportUrl,
},
importConfig: {
url: getImportUrl,
},
});
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
function handleCreate() {
openModal(true, { isUpdate: false });
}
function handleEdit(record) {
openModal(true, { record, isUpdate: true });
}
async function handleDelete(record) {
await deleteOne({ id: record.id }, reload);
}
async function batchHandleDelete() {
await batchDelete({ ids: selectedRowKeys.value }, () => {
selectedRowKeys.value = [];
reload();
});
}
function getTableAction(record) {
return [
{ label: '编辑', onClick: handleEdit.bind(null, record) },
{
label: '删除',
popConfirm: { title: '确定删除吗?', confirm: handleDelete.bind(null, record) },
},
];
}
</script>
<template>
<BasicModal v-bind="$attrs" @register="registerModal" :title="title" @ok="handleSubmit" width="40%">
<BasicForm @register="registerForm" :disabled="isDisabled" />
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, computed, unref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form';
import { formSchema } from './xxx.data';
import { saveOrUpdate, getById } from './xxx.api';
const emit = defineEmits(['register', 'success']);
const isUpdate = ref(true);
const isDisabled = ref(false);
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
schemas: formSchema,
showActionButtonGroup: false,
});
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
await resetFields();
setModalProps({ confirmLoading: false, showOkBtn: !data?.hideFooter });
isUpdate.value = !!data?.isUpdate;
isDisabled.value = !!data?.hideFooter;
if (unref(isUpdate)) {
// 编辑模式:获取详情并回填
let record = data.record;
if (data.record?.id) {
record = await getById({ id: data.record.id });
}
await setFieldsValue({ ...record });
}
});
const title = computed(() => (!unref(isUpdate) ? '新增' : isDisabled.value ? '详情' : '编辑'));
async function handleSubmit() {
try {
const values = await validate();
setModalProps({ confirmLoading: true });
await saveOrUpdate(values, isUpdate.value);
closeModal();
emit('success', values);
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>
useListPage 是列表页开发的核心 Hook,封装了表格注册、导入导出、选择行、删除确认等通用逻辑。
接口定义:
interface ListPageOptions {
designScope?: string; // 样式作用域
tableProps: TableProps; // 表格配置(必填)
pagination?: boolean; // 是否分页
exportConfig?: { // 导出配置
url: string | (() => string);
name?: string | (() => string);
params?: object | (() => object);
};
importConfig?: { // 导入配置
url: string | (() => string);
success?: (fileInfo?: any) => void;
};
}
返回值:
| 返回值 | 说明 |
|---|---|
tableContext |
[registerTable, methods, { rowSelection, selectedRowKeys, selectedRows }] |
onExportXls() |
导出 Excel(自动获取查询条件+选中行+列设置) |
onImportXls(file) |
导入 Excel |
doRequest(api, options?) |
通用请求(自动确认+刷新+清空选择) |
doDeleteRecord(api) |
单条删除(无确认弹窗) |
prefixCls |
CSS 类名前缀 |
createMessage |
消息提示 |
createConfirm |
确认弹窗 |
useListTable 默认配置(自动应用,无需手动设置):
| 配置项 | 默认值 | 说明 |
|---|---|---|
rowKey |
'id' |
主键 |
useSearchForm |
true |
启用搜索表单 |
canResize |
true |
自适应高度 |
minHeight |
300 |
最小高度 |
bordered |
true |
边框 |
striped |
false |
斑马纹 |
showTableSetting |
true |
表格设置 |
beforeFetch |
默认排序 createTime desc |
请求前自动注入排序 |
useListPage + BasicTablea-tabs + 独立的子表列表组件provide/inject 传递主表选中 ID<template>
<div>
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<template #tableTitle>
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleCreate">新增</a-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>删除
</a-menu-item>
</a-menu>
</template>
<a-button>批量操作 <Icon icon="mdi:chevron-down"></Icon></a-button>
</a-dropdown>
</template>
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" />
</template>
</BasicTable>
<!-- 子表 Tab 区域 -->
<a-tabs defaultActiveKey="1" style="margin: 10px">
<a-tab-pane tab="子表1名称" key="1">
<SubTable1List />
</a-tab-pane>
<a-tab-pane tab="子表2名称" key="2" forceRender>
<SubTable2List />
</a-tab-pane>
</a-tabs>
</div>
<MainModal @register="registerModal" @success="handleSuccess" />
</template>
<script lang="ts" setup>
import { computed, provide } from 'vue';
import { BasicTable, TableAction } from '/@/components/Table';
import { useListPage } from '/@/hooks/system/useListPage';
import { useModal } from '/@/components/Modal';
import MainModal from './components/MainModal.vue';
import SubTable1List from './SubTable1List.vue';
import SubTable2List from './SubTable2List.vue';
import { columns, searchFormSchema } from './xxx.data';
import { list, deleteOne, batchDelete } from './xxx.api';
const [registerModal, { openModal }] = useModal();
const { tableContext } = useListPage({
tableProps: {
api: list,
columns,
rowSelection: { type: 'radio' }, // 主子表通常用单选
formConfig: { schemas: searchFormSchema },
actionColumn: { width: 180 },
pagination: { current: 1, pageSize: 5, pageSizeOptions: ['5', '10', '20'] },
},
});
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
// 通过 provide 向子表传递当前选中的主表ID
const currentId = computed(() => selectedRowKeys.value.length > 0 ? selectedRowKeys.value[0] : '');
provide('orderId', currentId);
function handleCreate() {
openModal(true, { isUpdate: false, showFooter: true });
}
function handleEdit(record) {
openModal(true, { record, isUpdate: true, showFooter: true });
}
async function handleDelete(record) {
await deleteOne({ id: record.id }, reload);
}
async function batchHandleDelete() {
await batchDelete({ ids: selectedRowKeys.value }, () => { selectedRowKeys.value = []; reload(); });
}
function handleSuccess() { reload(); }
function getTableAction(record) {
return [
{ label: '编辑', onClick: handleEdit.bind(null, record) },
{ label: '删除', popConfirm: { title: '是否确认删除', confirm: handleDelete.bind(null, record) } },
];
}
</script>
<template>
<BasicTable @register="registerTable">
<template #tableTitle>
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleAdd">新增</a-button>
</template>
<template #action="{ record }">
<TableAction :actions="getActions(record)" />
</template>
</BasicTable>
<SubModal @register="registerModal" @success="reload" />
</template>
<script lang="ts" setup>
import { watch, inject, ref } from 'vue';
import { BasicTable, TableAction } from '/@/components/Table';
import { useModal } from '/@/components/Modal';
import { useListPage } from '/@/hooks/system/useListPage';
import SubModal from './components/SubModal.vue';
import { subColumns } from '../xxx.data';
import { subList, deleteSub } from '../xxx.api';
const [registerModal, { openModal }] = useModal();
const orderId = inject('orderId', ref(''));
const { tableContext } = useListPage({
tableProps: {
api: subList,
columns: subColumns,
formConfig: { schemas: [] }, // 子表通常无搜索
useSearchForm: false,
showTableSetting: false,
actionColumn: { width: 120 },
beforeFetch: (params) => {
// 将主表ID作为查询条件
params.orderId = orderId.value;
return params;
},
},
});
const [registerTable, { reload }] = tableContext;
// 监听主表选中变化,自动刷新子表
watch(orderId, () => { reload(); });
function handleAdd() {
openModal(true, { isUpdate: false, orderId: orderId.value });
}
// ...
</script>
在同一个 data.ts 中定义主表和所有子表的配置:
// ---- 主表 ----
export const columns: BasicColumn[] = [ /* 主表列 */ ];
export const searchFormSchema: FormSchema[] = [ /* 主表搜索 */ ];
export const formSchema: FormSchema[] = [ /* 主表表单 */ ];
// ---- 子表1 ----
export const sub1Columns: BasicColumn[] = [ /* 子表1列 */ ];
export const sub1FormSchema: FormSchema[] = [ /* 子表1表单 */ ];
// ---- 子表2 ----
export const sub2Columns: BasicColumn[] = [ /* 子表2列 */ ];
export const sub2FormSchema: FormSchema[] = [ /* 子表2表单 */ ];
在同一个 api.ts 中定义主表和所有子表的接口:
enum Api {
// 主表
list = '/test/order/orderList',
save = '/test/order/add',
edit = '/test/order/edit',
deleteOne = '/test/order/delete',
deleteBatch = '/test/order/deleteBatch',
// 子表1
sub1List = '/test/order/listSub1ByMainId',
saveSub1 = '/test/order/addSub1',
editSub1 = '/test/order/editSub1',
deleteSub1 = '/test/order/deleteSub1',
// 子表2
sub2List = '/test/order/listSub2ByMainId',
saveSub2 = '/test/order/addSub2',
editSub2 = '/test/order/editSub2',
deleteSub2 = '/test/order/deleteSub2',
}
关键差异:isTreeTable: true + 异步加载子节点
<template>
<div>
<BasicTable @register="registerTable" :rowSelection="rowSelection"
:expandedRowKeys="expandedRowKeys" @expand="handleExpand" @fetch-success="onFetchSuccess">
<template #tableTitle>
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleCreate">新增</a-button>
<a-button type="primary" preIcon="ant-design:export-outlined" @click="onExportXls">导出</a-button>
<j-upload-button type="primary" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
</template>
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" />
</template>
</BasicTable>
<CategoryModal @register="registerModal" @success="handleSuccess" />
</div>
</template>
<script lang="ts" setup>
import { ref, unref } from 'vue';
import { BasicTable, TableAction } from '/@/components/Table';
import { useListPage } from '/@/hooks/system/useListPage';
import { useModal } from '/@/components/Modal';
import CategoryModal from './components/CategoryModal.vue';
import { columns, searchFormSchema } from './category.data';
import { list, deleteCategory, batchDeleteCategory, getExportUrl, getImportUrl, getChildList, getChildListBatch } from './category.api';
const expandedRowKeys = ref([]);
const [registerModal, { openModal }] = useModal();
const { tableContext, onExportXls, onImportXls } = useListPage({
designScope: 'category-template',
tableProps: {
api: list,
columns,
formConfig: { schemas: searchFormSchema },
actionColumn: { width: 180 },
isTreeTable: true, // 关键:标记为树形表格
},
exportConfig: { name: '分类字典列表', url: getExportUrl },
importConfig: { url: getImportUrl },
});
const [registerTable, { reload, updateTableDataRecord, findTableDataRecord, getDataSource }, { rowSelection, selectedRowKeys }] = tableContext;
// 树节点展开/折叠
async function handleExpand(expanded, record) {
if (expanded) {
expandedRowKeys.value.push(record.id);
if (record.children?.length > 0 && record.children[0].isLoading) {
let result = await getChildList({ pid: record.id });
record.children = result?.length > 0 ? getDataByResult(result) : null;
if (!result?.length) record.hasChild = '0';
}
} else {
let idx = expandedRowKeys.value.indexOf(record.id);
if (idx >= 0) expandedRowKeys.value.splice(idx, 1);
}
}
// 处理数据集(标记是否有子节点)
function getDataByResult(result) {
if (result?.length > 0) {
return result.map((item) => {
if (item['hasChild'] == '1') {
item.children = [{ id: item.id + '_loadChild', name: 'loading...', isLoading: true }];
}
return item;
});
}
}
// 请求成功回调
function onFetchSuccess(result) {
getDataByResult(result.items) && loadDataByExpandedRows();
}
// 添加下级
function handleAddSub(record) {
openModal(true, { record, isUpdate: false });
}
// 成功回调(区分新增根节点/子节点)
async function handleSuccess({ isUpdate, values, expandedArr }) {
if (isUpdate) {
updateTableDataRecord(values.id, values);
} else if (!values['pid']) {
reload();
} else {
for (let key of unref(expandedArr)) {
await expandTreeNode(key);
}
}
}
async function expandTreeNode(key) {
expandedRowKeys.value.push(key);
let record = findTableDataRecord(key);
let result = await getChildList({ pid: key });
record.children = result?.length > 0 ? getDataByResult(result) : null;
if (!result?.length) record.hasChild = '0';
updateTableDataRecord(key, record);
}
function getTableAction(record) {
return [
{ label: '编辑', onClick: handleEdit.bind(null, record) },
{ label: '删除', popConfirm: { title: '确定删除吗?', confirm: handleDelete.bind(null, record) } },
{ label: '添加下级', onClick: handleAddSub.bind(null, { pid: record.id }) },
];
}
</script>
DepartLeftTree.vue)reload({ searchInfo: { departId: selectedKey } })注册方式:
// 方式一:useListPage(推荐,自动处理导入导出)
const { tableContext } = useListPage({ tableProps: { ... } });
const [registerTable, { reload, setProps }, { rowSelection, selectedRowKeys }] = tableContext;
// 方式二:useTable(手动管理)
const [registerTable, { reload, setProps, getDataSource, updateTableDataRecord }] = useTable({
api: list,
columns,
formConfig: { schemas: searchFormSchema },
...
});
模板使用:
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<template #tableTitle><!-- 按钮区域 --></template>
<template #action="{ record }"><!-- 操作列 --></template>
<template #form-fieldName="{ model, field }"><!-- 自定义搜索表单项 --></template>
<template #bodyCell="{ column, record, text }"><!-- 自定义列渲染 --></template>
</BasicTable>
常用方法:
| 方法 | 说明 |
|---|---|
reload(params?) |
刷新表格(可传搜索参数) |
setProps(props) |
动态修改表格属性 |
getDataSource() |
获取当前数据源 |
updateTableDataRecord(rowKey, record) |
更新指定行数据 |
findTableDataRecord(rowKey) |
查找指定行 |
getColumns() |
获取列配置 |
setColumns(columns) |
设置列配置 |
setLoading(loading) |
设置加载状态 |
getForm() |
获取搜索表单实例 |
注册方式:
// 外部调用方
const [registerModal, { openModal, closeModal, setModalProps }] = useModal();
// Modal 内部
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
// data = openModal 传入的参数
await resetFields();
// 回填表单数据
await setFieldsValue({ ...data.record });
});
常用方法:
| 方法 | 说明 |
|---|---|
openModal(visible, data) |
打开弹窗并传参 |
closeModal() |
关闭弹窗 |
setModalProps(props) |
修改弹窗属性(confirmLoading, showOkBtn, title等) |
注册方式:
// 外部调用方
const [registerDrawer, { openDrawer, closeDrawer }] = useDrawer();
// Drawer 内部
const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
// data = openDrawer 传入的参数
});
const { onExportXls, onImportXls } = useListPage({
tableProps: { ... },
exportConfig: {
name: '导出文件名',
url: getExportUrl,
params: { customParam: 'value' }, // 自定义导出参数(可选)
},
importConfig: {
url: getImportUrl,
success: () => { /* 导入成功回调 */ },
},
});
模板中直接调用:
<!-- 导出按钮 -->
<a-button type="primary" @click="onExportXls">导出</a-button>
<!-- 导入按钮 -->
<j-upload-button type="primary" @click="onImportXls">导入</j-upload-button>
import { useMethods } from '/@/hooks/system/useMethods';
const { handleExportXls, handleImportXls } = useMethods();
// 导出
handleExportXls('文件名', exportUrl, filterParams);
// 导入(配合 a-upload)
<a-upload :customRequest="(file) => handleImportXls(file, importUrl, reload)">
<template>
<!-- ... -->
<JImportModal @register="registerImportModal" :url="getImportUrl" online />
</template>
<script setup>
import JImportModal from '/@/components/Form/src/jeecg/components/JImportModal.vue';
const [registerImportModal, { openModal: openImportModal }] = useModal();
function handleImport() {
openImportModal(true);
}
</script>
import { render } from '/@/utils/common/renderUtils';
// 方式一:普通字典翻译
{
title: '性别',
dataIndex: 'sex',
customRender: ({ text }) => render.renderDict(text, 'sex'),
}
// 方式二:表字典翻译(从其他表翻译)
{
title: '部门',
dataIndex: 'departId',
customRender: ({ text }) => render.renderDict(text, 'sys_depart,depart_name,id'),
}
// 搜索条件中
{
field: 'sex',
label: '性别',
component: 'JDictSelectTag',
componentProps: {
dictCode: 'sex',
placeholder: '请选择性别',
},
}
// 表单中 - 下拉模式
{
field: 'sex',
label: '性别',
component: 'JDictSelectTag',
componentProps: {
dictCode: 'sex',
},
}
// 表单中 - 单选按钮模式
{
field: 'sex',
label: '性别',
component: 'JDictSelectTag',
componentProps: {
type: 'radio',
dictCode: 'sex',
},
}
import { initDictOptions } from '/@/utils/dict';
// 在组件中获取字典选项
const dictOptions = await initDictOptions('sex');
// dictOptions = [{ value: '1', text: '男' }, { value: '2', text: '女' }]
方式一:v-auth 指令(控制 DOM 显隐)
<a-button v-auth="'user:add'" type="primary">新增</a-button>
<a-button v-auth="'user:edit'" @click="handleEdit">编辑</a-button>
<a-button v-auth="'user:delete'" @click="handleDelete">删除</a-button>
方式二:usePermission Hook(逻辑判断)
import { usePermission } from '/@/hooks/web/usePermission';
const { hasPermission } = usePermission();
if (hasPermission('user:add')) {
// 有权限才执行的逻辑
}
方式三:TableAction 权限控制
function getTableAction(record) {
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
auth: 'user:edit', // 权限码
},
{
label: '删除',
popConfirm: { title: '确定删除?', confirm: handleDelete.bind(null, record) },
auth: 'user:delete',
},
];
}
由后端 @PermissionData 注解 + QueryGenerator 自动处理,前端无需额外编码。
// 方式一:路由元信息
{
path: '/public-page',
meta: { ignoreAuth: true },
}
// 方式二:将 URL 加入白名单(router/guard/permissionGuard.ts)
import { defHttp } from '/@/utils/http/axios';
// GET 请求
export const list = (params) => defHttp.get({ url: '/api/list', params });
// POST 请求
export const save = (params) => defHttp.post({ url: '/api/save', params });
// PUT 请求
export const update = (params) => defHttp.put({ url: '/api/update', params });
// DELETE 请求(参数拼接到URL)
export const remove = (params) => defHttp.delete({ url: '/api/delete', params }, { joinParamsToUrl: true });
// DELETE 请求(参数在body中)
export const batchRemove = (data) => defHttp.delete({ url: '/api/batchDelete', data });
// 文件上传
export const upload = (params) => defHttp.uploadFile({ url: '/api/upload', params });
| Header | 说明 |
|---|---|
X-Access-Token |
JWT Token(自动从缓存获取) |
X-Tenant-Id |
租户ID(自动注入) |
X-Sign |
请求签名(MD5) |
X-TIMESTAMP |
请求时间戳 |
X-Version |
版本号 |
X-Low-App-ID |
低代码应用ID |
code === 0 → 成功,返回 result 数据code === 401 → Token 失效,自动跳转登录页| Hook | 用途 | 核心导出 |
|---|---|---|
useListPage |
列表页标准开发 | tableContext, onExportXls, onImportXls, doRequest |
useMethods |
导入导出方法 | handleExportXls, handleImportXls |
useJvxeMethods |
JVxeTable方法 | 行编辑表格操作 |
| Hook | 用途 | 核心导出 |
|---|---|---|
usePermission |
权限判断 | hasPermission(value), isDisabledAuth |
useMessage |
消息弹窗 | createMessage, createConfirm, createConfirmSync |
useWebSocket |
WebSocket | connectWebSocket(url), onWebSocket(cb) |
useECharts |
ECharts图表 | setOptions, resize, getInstance |
useTabs |
多标签操作 | refreshPage, closeAll, closeLeft |
useDesign |
CSS命名空间 | prefixCls = 'jeecg' |
usePage |
页面跳转 | useGo(), useRedo() |
| Hook | 用途 |
|---|---|
useAttrs |
透传属性 |
useContext |
上下文 |
useLockFn |
防重复提交 |
useRefs |
模板 Refs 批量管理 |
onMountedOrActivated |
mounted/activated 统一入口 |
import { getFileAccessHttpUrl, listToTree, filterObj, triggerWindowResizeEvent, getHeaders } from '/@/utils/common/compUtils';
| 函数 | 说明 |
|---|---|
getFileAccessHttpUrl(url) |
文件 URL 拼接(自动加域名前缀) |
listToTree(data, pid, key) |
扁平列表转树形结构 |
filterObj(obj) |
过滤空属性对象 |
triggerWindowResizeEvent() |
触发窗口 resize 事件 |
getHeaders() |
获取上传请求头(含Token+TenantId) |
mapTableTotalSummary(data, columns) |
表格合计行(big.js精度计算) |
simpleDebounce(fn, delay) |
防抖 |
dateFormat(date, fmt) |
日期格式化 |
bindMapFormSchema(schemas, span) |
表单 span 响应式映射 |
import { render } from '/@/utils/common/renderUtils';
render.renderDict(text, dictCode) // 字典翻译
render.renderTag(text, color) // 标签渲染
render.renderSwitch(text, colorMap) // 开关渲染
render.renderDate(text) // 日期格式化
import { initDictOptions, getDictItemsByCode, ajaxGetDictItems } from '/@/utils/dict';
const options = await initDictOptions('sex'); // 获取字典选项(优先缓存)
import { getToken, getTenantId, setAuthCache, getAuthCache, clearAuthCache } from '/@/utils/auth';
| 指令 | 用途 | 示例 |
|---|---|---|
v-auth |
权限控制DOM显隐 | v-auth="'user:add'" |
v-loading |
加载遮罩 | v-loading="loading" |
v-click-outside |
点击外部触发 | v-click-outside="handleClickOutside" |
v-repeat-click |
长按重复触发 | v-repeat-click="handleRepeat" |
v-ripple |
水波纹效果 | v-ripple |
| 配置项 | 默认值 | 说明 |
|---|---|---|
permissionMode |
'BACK' |
权限模式(BACK=后端动态路由) |
permissionCacheType |
'LOCAL' |
权限缓存方式 |
themeMode |
'LIGHT' |
主题模式 |
showBreadCrumb |
true |
面包屑 |
openKeepAlive |
true |
页面缓存 |
headerSetting |
— | 头部配置(fixed/showDoc/showNotice/showSearch) |
menuSetting |
— | 菜单配置(type/mode/collapsed/accordion) |
multiTabsSetting |
— | 多标签配置(cache/show/canDrag) |
| 配置项 | 默认值 |
|---|---|
table.pageNo |
'pageNo' |
table.pageSize |
'pageSize' |
table.records |
'records' |
table.total |
'total' |
form.labelCol |
{ span: 4 } |
form.wrapperCol |
{ span: 18 } |
| Store ID | 功能 | 核心状态 |
|---|---|---|
app |
应用全局 | 暗黑模式、页面加载、项目配置 |
app-user |
用户 | token、用户信息、角色列表、字典缓存、租户ID |
app-permission |
权限/路由 | permCodeList、authList、动态路由 |
app-multiple-tab |
多标签页 | tab列表、KeepAlive列表 |
app-lock |
锁屏 | 锁屏密码 |
app-locale |
国际化 | 当前语言、路径标题映射 |
app-error-log |
错误日志 | AJAX错误信息 |
defIndex |
默认首页 | url和component |
| 模式 | 说明 | 配置 |
|---|---|---|
| BACK(默认) | 后端动态菜单 → 自动生成路由 | permissionMode: 'BACK' |
| ROLE | 后端角色 → 前端路由 | permissionMode: 'ROLE' |
| ROUTE_MAPPING | 前端静态路由 + 权限过滤 | permissionMode: 'ROUTE_MAPPING' |
1. PageGuard — 状态检查
2. PageLoadingGuard — 页面加载进度条
3. HttpGuard — 清除pending请求
4. ScrollGuard — 滚动重置
5. MessageGuard — 消息清理
6. ProgressGuard — NProgress
7. PermissionGuard — 权限核心(白名单/登录判断/动态路由构建)
8. ParamMenuGuard — 参数菜单处理
9. StateGuard — 登录页状态清理
框架内置以下免登录路由(在 routes/index.ts 定义):
| 路由 | 说明 |
|---|---|
/login |
登录页 |
/oauth2-app/login |
OAuth2免登(钉钉/企微) |
/tokenLogin |
Token静默登录 |
/ssoLogin |
智慧人社SSO |
/file/share |
文件分享 |
用于需要行内编辑的场景(如主子表子表、批量录入等)。
<template>
<JVxeTable ref="tableRef" :columns="columns" :dataSource="dataSource" />
</template>
<script setup>
import { JVxeTable } from '/@/components/jeecg/JVxeTable';
const columns = [
{ key: 'name', title: '名称', type: JVxeTypes.input },
{ key: 'sex', title: '性别', type: JVxeTypes.select, options: [...] },
{ key: 'birthday', title: '生日', type: JVxeTypes.date },
{ key: 'amount', title: '金额', type: JVxeTypes.inputNumber },
];
</script>
| 类型 | 说明 | 对应组件 |
|---|---|---|
input |
输入框 | JVxeInputCell |
inputNumber |
数字输入 | JVxeInputCell |
select |
下拉选择 | JVxeSelectCell |
selectSearch |
搜索选择 | JVxeSelectCell |
selectMultiple |
多选 | JVxeSelectCell |
date / datetime |
日期 | JVxeDateCell |
time |
时间 | JVxeTimeCell |
checkbox |
复选框 | JVxeCheckboxCell |
radio |
单选 | JVxeRadioCell |
textarea |
多行文本 | JVxeTextareaCell |
upload |
文件上传 | JVxeUploadCell |
treeSelect |
树选择 | JVxeTreeSelectCell |
catTreeSelect |
分类字典树 | JVxeCategorySelectCell |
progress |
进度条 | JVxeProgressCell |
rowDragSort |
拖拽排序 | JVxeDragSortCell |
slot |
插槽自定义 | JVxeSlotCell |
JVxeTable 完全支持对输入数据进行校验检查,包括非空、纯数字、纯字母、正则、自定义函数等。校验规则通过列配置中的 validateRules
数组定义。
| 层级 | 配置位置 | 说明 |
|---|---|---|
| 组件级 | editRules prop |
全局 vxe-table 原生 editRules,可对所有列施加通用校验 |
| 列级 | validateRules 列属性 |
每列独立配置校验规则数组,优先级更高 |
两种校验规则通过 lodash.merge 合并后传入底层 vxe-table。
| 字段 | 类型 | 说明 |
|---|---|---|
required |
boolean |
是否必填(非空校验) |
pattern |
string / RegExp |
正则表达式,支持快捷值 |
message |
string |
校验失败提示,支持 ${title}、${key}、${defaultValue} 变量替换 |
validator |
function |
自定义异步校验函数(Promise 形式) |
handler |
function |
旧版回调式校验函数(兼容模式,内部转为 validator) |
unique |
boolean |
唯一性校验(同一列中值不能重复) |
组件内置了常用校验规则的快捷值,使用时直接传入 pattern 对应的快捷字符串即可,无需写正则。
定义位置:useValidateRules.ts
| 快捷值 | 含义 | 对应正则 |
|---|---|---|
* |
非空 | 等同于 required: true |
n |
纯数字(含小数) | /^-?\d+(\.?\d+\|\d?)$/ |
s |
纯字母 | /^[A-Z\|a-z]+$/ |
z |
整数 | /^-?\d+$/ |
e |
电子邮件 | /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/ |
m |
手机号码 | /^1[3456789]\d{9}$/ |
p |
邮政编码 | /^\d{6}$/ |
url |
网址 | URL 格式正则 |
money |
金额 | /^(([1-9][0-9]*)\|([0]\.\d{0,2}\|[1-9][0-9]*\.\d{0,5}))$/ |
n6-16 |
6到16位数字 | /^\d{6,16}$/ |
*6-16 |
6到16位任意字符 | /^.{6,16}$/ |
s6-18 |
6到18位字母 | /^[a-z\|A-Z]{6,18}$/ |
only |
唯一性校验 | 同 unique: true,遍历整表检查同列值是否重复 |
import {JVxeTable, JVxeTypes} from '/@/components/jeecg/JVxeTable';
const columns = [
{
key: 'name',
title: '姓名',
type: JVxeTypes.input,
// 非空校验
validateRules: [{required: true, message: '${title}不能为空'}],
},
{
key: 'age',
title: '年龄',
type: JVxeTypes.inputNumber,
// 整数校验 + 非空
validateRules: [
{required: true, message: '${title}不能为空'},
{pattern: 'z', message: '${title}必须为整数'},
],
},
{
key: 'email',
title: '邮箱',
type: JVxeTypes.input,
// 非必填但格式校验
validateRules: [{pattern: 'e', message: '${title}格式不正确'}],
},
{
key: 'phone',
title: '手机号',
type: JVxeTypes.input,
// 手机号校验
validateRules: [{pattern: 'm', message: '${title}格式不正确'}],
},
{
key: 'code',
title: '编码',
type: JVxeTypes.input,
// 纯字母校验 + 唯一性校验
validateRules: [
{required: true, message: '${title}不能为空'},
{pattern: 's', message: '${title}只能包含字母'},
{unique: true, message: '${title}不能重复'},
],
},
{
key: 'amount',
title: '金额',
type: JVxeTypes.inputNumber,
// 金额格式校验
validateRules: [{pattern: 'money', message: '${title}格式不正确'}],
},
{
key: 'idCard',
title: '身份证号',
type: JVxeTypes.input,
// 自定义正则校验
validateRules: [{
pattern: '^\\d{6}(18|19|20)?\\d{2}(0[1-9]|1[012])(0[1-9]|[12]\\d|3[01])\\d{3}(\\d|[xX])$',
message: '${title}格式不正确',
}],
},
{
key: 'customField',
title: '自定义校验',
type: JVxeTypes.input,
// 自定义校验函数(handler 回调式,兼容旧版)
validateRules: [{
handler: (event, callback) => {
const {cellValue} = event;
if (cellValue && cellValue.length < 3) {
callback(false, '长度不能少于3位');
} else {
callback(true);
}
},
message: '${title}校验不通过',
}],
},
];
对于 checkbox、radio、upload、progress、departSelect、userSelect、image、file 等非文本编辑组件,如果配置了
validateRules 且包含 required,会在列头的 title 前自动加 * 标记,提示用户该列为必填。
定义位置:useColumns.ts
JVxeTable 对外暴露了两个校验方法,可在外部通过 ref 调用:
| 方法 | 说明 |
|---|---|
validateTable(rows?) |
校验当前表格,成功返回 null,失败返回 errMap |
fullValidateTable(rows?) |
完整校验所有数据(无论是否修改过) |
<template>
<JVxeTable ref="tableRef" :columns="columns" :dataSource="dataSource"/>
</template>
<script setup>
import {ref} from 'vue';
const tableRef = ref();
async function handleSubmit() {
// 调用表格校验
const errMap = await tableRef.value.validateTable();
if (errMap) {
console.log('校验失败:', errMap);
return;
}
// 校验通过,执行后续逻辑
console.log('校验通过');
}
</script>
定义位置:useMethods.ts
JVxeTable 支持通过插槽(slot)方式自定义某个字段的输入框内容,适用于需要自定义编辑组件(如下拉框、级联选择、日期范围等)且选项数据来自父组件的场景。
JVxeTable 的插槽机制分两层:
第一层:JVxeTypes.slot + slotName(推荐方式)
这是 JVxeTable 内置的插槽支持机制,完全集成到行编辑系统中。
type: JVxeTypes.slot,同时指定 slotName(如 'myFieldSlot')JVxeSlotCell 组件负责渲染该单元格useColumns.ts 中的 handleSlots()
方法(源码
)会检查父组件是否传入了同名插槽,若有则将插槽函数注入渲染参数第二层:$slots 透传至底层 vxe-grid
在 JVxeTable.ts
的 render 函数中,this.$slots 被完整透传给底层的 vxe-grid 组件。这意味着所有传递给 JVxeTable 的插槽也会被 vxe-grid
接收,可以使用 vxe-grid 原生的所有插槽能力。
JVxeSlotCell.ts 中定义了插槽函数接收的参数对象:
| 参数 | 类型 | 说明 |
|---|---|---|
value |
any |
当前单元格的值 |
row |
object |
当前行数据 |
column |
object |
当前列配置信息 |
params |
object |
vxe-table 原始渲染参数 |
$table |
object |
vxe-table 实例,可调用内置方法 |
rowId |
string |
当前行唯一标识 |
index / rowIndex |
number |
当前行下标 |
columnIndex |
number |
当前列下标 |
scrolling |
boolean |
是否正在滚动 |
reloadEffect |
boolean |
是否开启了数据刷新特效 |
triggerChange |
(value) => void |
触发值变更,用于将插槽组件的值更新到单元格数据中 |
场景:在 JVxeTable 中,某个字段需要使用下拉选择,下拉选项来自父组件(如通过 props 传入、API 加载、或父组件的响应式数据)。
列配置(data 文件):
import {JVxeTypes} from '/@/components/jeecg/JVxeTable';
export const studyAbroadColumns: JVxeColumn[] = [
{
title: '国家/地区',
key: 'country',
type: JVxeTypes.slot, // 使用 slot 类型
slotName: 'countrySlot', // 插槽名称,与模板中的 #countrySlot 对应
width: 160,
validateRules: [{required: true, message: '${title}不能为空'}],
},
{
title: '学校名称',
key: 'schoolName',
type: JVxeTypes.input,
width: 200,
},
{
title: '开始日期',
key: 'startDate',
type: JVxeTypes.date,
width: 140,
},
// ... 其他列
];
父组件模板:
<template>
<JVxeTable
ref="studyAbroadTableRef"
:columns="computedStudyAbroadColumns"
:dataSource="studyAbroadData.dataSource"
:height="250"
keepSource
rowNumber
rowSelection
stripe
toolbar
>
<!-- 自定义国家/地区字段的下拉选择框 -->
<template #countrySlot="{ row, triggerChange }">
<a-select
:value="row.country"
:options="countryOptions"
placeholder="请选择国家/地区"
allow-clear
:bordered="false"
style="width: 100%"
@change="(val) => triggerChange(val)"
/>
</template>
</JVxeTable>
</template>
父组件脚本:
<script lang = "ts"
setup >
import {ref} from 'vue';
import {JVxeTable} from '/@/components/jeecg/JVxeTable';
import {studyAbroadColumns} from '../ResumeInfo.data';
// 下拉选项数据——来自父组件,可以是 props、API 加载、或本地定义的响应式数据
const countryOptions = ref([
{label: '美国', value: '美国'},
{label: '英国', value: '英国'},
{label: '澳大利亚', value: '澳大利亚'},
{label: '日本', value: '日本'},
{label: '新加坡', value: '新加坡'},
]);
// 也可以是通过 API 动态加载
// import { getCountryList } from '../xxx.api';
// onMounted(async () => {
// const res = await getCountryList();
// countryOptions.value = res.map(item => ({ label: item.name, value: item.code }));
// });
</script>
triggerChange 是必须调用的:插槽组件值变更时,必须调用 triggerChange(newValue) 将值同步回 JVxeTable
的数据源,否则表格数据不会更新。
插槽名称要唯一:每个 JVxeTypes.slot 列需要指定不同的 slotName,同一个 JVxeTable 内不能重名。
slot 列支持校验:validateRules 对 slot 列同样生效,非空校验会在列头显示 * 标记。
bordered: false:插槽内部使用 Ant Design 组件时,建议设置 :bordered="false" 以与表格单元格样式融合。
插槽作用域来自父组件:插槽模板在父组件中编写,可以直接访问父组件的所有响应式数据(props、ref、computed 等),因此下拉选项可以灵活地来自任何父组件数据源。
与普通 select 类型对比:
JVxeTypes.select:适用于选项固定或通过 dictCode 从字典加载的场景JVxeTypes.slot:适用于选项来自父组件、需要完全自定义UI、或涉及复杂交互的场景import { useWebSocket } from '/@/hooks/web/useWebSocket';
// 建立连接
connectWebSocket('ws://localhost:8080/websocket');
// 添加监听
onWebSocket((data) => {
console.log('收到消息:', data);
});
// 移除监听
offWebSocket(callback);
特性:自动重连(10次/5s间隔)、心跳(55s)、全局单例。
| 组件 | 文件 |
|---|---|
| useListPage | jeecgboot-vue3/src/hooks/system/useListPage.ts |
| useMethods | jeecgboot-vue3/src/hooks/system/useMethods.ts |
| usePermission | jeecgboot-vue3/src/hooks/web/usePermission.ts |
| useMessage | jeecgboot-vue3/src/hooks/web/useMessage.ts |
| useWebSocket | jeecgboot-vue3/src/hooks/web/useWebSocket.ts |
| useECharts | jeecgboot-vue3/src/hooks/web/useECharts.ts |
| 组件 | 文件 |
|---|---|
| BasicTable | jeecgboot-vue3/src/components/Table/src/BasicTable.vue |
| BasicModal | jeecgboot-vue3/src/components/Modal/src/BasicModal.vue |
| BasicDrawer | jeecgboot-vue3/src/components/Drawer/src/BasicDrawer.vue |
| BasicForm | jeecgboot-vue3/src/components/Form/src/BasicForm.vue |
| JVxeTable | jeecgboot-vue3/src/components/jeecg/JVxeTable/src/JVxeTable.ts |
| componentMap | jeecgboot-vue3/src/components/Form/src/componentMap.ts |
| 工具 | 文件 |
|---|---|
| defHttp | jeecgboot-vue3/src/utils/http/axios/index.ts |
| compUtils | jeecgboot-vue3/src/utils/common/compUtils.ts |
| renderUtils | jeecgboot-vue3/src/utils/common/renderUtils.ts |
| dict | jeecgboot-vue3/src/utils/dict/index.ts |
| auth | jeecgboot-vue3/src/utils/auth/index.ts |
| cache | jeecgboot-vue3/src/utils/cache/persistent.ts |
| 类型 | 文件 |
|---|---|
| 单表CRUD | jeecgboot-vue3/src/views/system/examples/demo/ |
| 主子表ERP | jeecgboot-vue3/src/views/demo/jeecg/erplist/ |
| 树形列表 | jeecgboot-vue3/src/views/system/category/ |
| 树形左树右表 | jeecgboot-vue3/src/views/system/depart/ |
| JVxeTable | jeecgboot-vue3/src/views/demo/jeecg/JVxeTableDemo/ |
| 文件 | 操作 | 原因 |
|---|---|---|
.docs/260603-JeecgBoot前端框架全局开发规范与最佳实践.md |
新增 | 重新分析前端框架,撰写全面开发规范文档 |