260603-前端框架全局开发规范与最佳实践.md 68 KB

JeecgBoot Vue3 前端框架全局开发规范与最佳实践

分析日期:2026-06-03 框架版本:JeecgBoot 3.9.2(基于 Vben-Admin Vue3 深度定制) 分析范围:JeecgBoot 原框架(排除 views/recruitmentviews/spbb 业务目录)


一、框架概述

1.1 技术栈

层级 技术
框架基础 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

1.2 目录结构(核心)

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 表单

二、页面文件结构规范

2.1 单表 CRUD 模块(标准4件套)

模块名/
  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/

2.2 主子表(一对多)模块

模块名/
  index.vue                         # 主表列表+子表Tab区域
  xxx.data.ts                       # 主表 + 所有子表的 columns/formSchema
  xxx.api.ts                        # 主表 + 所有子表的 CRUD 接口
  MainModal.vue                     # 主表编辑弹窗
  SubTableList.vue                  # 子表列表组件(每个子表一个)
  components/
    SubModal.vue                    # 子表编辑弹窗(每个子表一个)

典型代表demo/jeecg/erplist/

2.3 树形页面模块

模块名/
  index.vue               # 左树右表主页面
  xxx.data.ts             # 表格 columns + 表单 schema
  xxx.api.ts              # 树接口 + 列表接口 + CRUD接口
  components/
    XxxModal.vue          # 编辑弹窗
    LeftTree.vue          # 左侧树组件(可选)

典型代表system/category/(纯树形)、system/depart/(左树右表)


三、API 文件编写规范

3.1 最优写法

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();
      });
    },
  });
};

3.2 API 编写要点

要点 规范
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

四、Data 文件编写规范

4.1 columns 定义

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' ? '国际订单' : '';
    },
  },
];

4.2 searchFormSchema 定义

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 },
  },
];

4.3 formSchema 定义(弹窗表单)

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,                        // 隐藏
  },
];

4.4 表单组件类型速查(componentMap 46种)

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 图标选择器 菜单图标

五、单表 CRUD 页面最优写法

5.1 列表页 index.vue(useListPage 模式 — 推荐)

<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>

5.2 弹窗 XxxModal.vue

<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>

5.3 useListPage Hook 详解

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 请求前自动注入排序

六、主子表(一对多)页面最优写法

6.1 核心架构

  • 主表列表 使用 useListPage + BasicTable
  • 子表 使用 a-tabs + 独立的子表列表组件
  • 主子表通信 通过 provide/inject 传递主表选中 ID
  • 主表单弹窗 内含子表 Tab(新增/编辑时同时操作主表+子表)

6.2 主表列表页 index.vue

<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>

6.3 子表列表组件

<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>

6.4 主子表 data.ts 规范

在同一个 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表单 */ ];

6.5 主子表 api.ts 规范

在同一个 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',
}

七、树形页面最优写法

7.1 纯树形列表(如分类字典)

关键差异: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>

7.2 左树右表(如部门管理)

  • 左侧:独立的树组件(DepartLeftTree.vue
  • 右侧:标准 BasicTable
  • 通信:树选中 → 触发表格 reload({ searchInfo: { departId: selectedKey } })

八、BasicTable / BasicModal / BasicDrawer 使用规范

8.1 BasicTable 核心 API

注册方式

// 方式一: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() 获取搜索表单实例

8.2 BasicModal 核心 API

注册方式

// 外部调用方
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等)

8.3 BasicDrawer 核心 API

注册方式

// 外部调用方
const [registerDrawer, { openDrawer, closeDrawer }] = useDrawer();

// Drawer 内部
const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
  // data = openDrawer 传入的参数
});

九、Excel 导入导出规范

9.1 使用 useListPage 模式(推荐,最少代码)

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>

9.2 使用 useMethods 模式(更多控制)

import { useMethods } from '/@/hooks/system/useMethods';
const { handleExportXls, handleImportXls } = useMethods();

// 导出
handleExportXls('文件名', exportUrl, filterParams);

// 导入(配合 a-upload)
<a-upload :customRequest="(file) => handleImportXls(file, importUrl, reload)">

9.3 弹窗导入模式(JImportModal)

<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>

十、字典使用规范

10.1 字典翻译渲染(列表列)

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'),
}

10.2 字典下拉选择(表单/搜索)

// 搜索条件中
{
  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',
  },
}

10.3 字典工具函数

import { initDictOptions } from '/@/utils/dict';

// 在组件中获取字典选项
const dictOptions = await initDictOptions('sex');
// dictOptions = [{ value: '1', text: '男' }, { value: '2', text: '女' }]

十一、权限控制规范

11.1 功能权限(按钮级)

方式一: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',
    },
  ];
}

11.2 数据权限

由后端 @PermissionData 注解 + QueryGenerator 自动处理,前端无需额外编码。

11.3 免认证配置

// 方式一:路由元信息
{
  path: '/public-page',
  meta: { ignoreAuth: true },
}

// 方式二:将 URL 加入白名单(router/guard/permissionGuard.ts)

十二、HTTP 请求封装

12.1 defHttp 使用

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 });

12.2 自动注入的请求头

Header 说明
X-Access-Token JWT Token(自动从缓存获取)
X-Tenant-Id 租户ID(自动注入)
X-Sign 请求签名(MD5)
X-TIMESTAMP 请求时间戳
X-Version 版本号
X-Low-App-ID 低代码应用ID

12.3 响应处理

  • code === 0 → 成功,返回 result 数据
  • code === 401 → Token 失效,自动跳转登录页
  • 其他 → 错误提示

十三、常用 Hooks 速查

13.1 系统 Hooks

Hook 用途 核心导出
useListPage 列表页标准开发 tableContext, onExportXls, onImportXls, doRequest
useMethods 导入导出方法 handleExportXls, handleImportXls
useJvxeMethods JVxeTable方法 行编辑表格操作

13.2 Web Hooks

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()

13.3 核心 Hooks

Hook 用途
useAttrs 透传属性
useContext 上下文
useLockFn 防重复提交
useRefs 模板 Refs 批量管理
onMountedOrActivated mounted/activated 统一入口

十四、常用工具函数速查

14.1 compUtils(最常用)

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 响应式映射

14.2 renderUtils

import { render } from '/@/utils/common/renderUtils';

render.renderDict(text, dictCode)           // 字典翻译
render.renderTag(text, color)               // 标签渲染
render.renderSwitch(text, colorMap)         // 开关渲染
render.renderDate(text)                     // 日期格式化

14.3 dict 工具

import { initDictOptions, getDictItemsByCode, ajaxGetDictItems } from '/@/utils/dict';

const options = await initDictOptions('sex');  // 获取字典选项(优先缓存)

14.4 auth 工具

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

十六、全局配置文件

16.1 projectSetting.ts(核心配置)

配置项 默认值 说明
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)

16.2 componentSetting.ts(组件默认配置)

配置项 默认值
table.pageNo 'pageNo'
table.pageSize 'pageSize'
table.records 'records'
table.total 'total'
form.labelCol { span: 4 }
form.wrapperCol { span: 18 }

十七、Pinia Store 模块

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

十八、路由体系

18.1 路由模式

模式 说明 配置
BACK(默认) 后端动态菜单 → 自动生成路由 permissionMode: 'BACK'
ROLE 后端角色 → 前端路由 permissionMode: 'ROLE'
ROUTE_MAPPING 前端静态路由 + 权限过滤 permissionMode: 'ROUTE_MAPPING'

18.2 路由守卫执行顺序

1. PageGuard          — 状态检查
2. PageLoadingGuard   — 页面加载进度条
3. HttpGuard          — 清除pending请求
4. ScrollGuard        — 滚动重置
5. MessageGuard       — 消息清理
6. ProgressGuard      — NProgress
7. PermissionGuard    — 权限核心(白名单/登录判断/动态路由构建)
8. ParamMenuGuard     — 参数菜单处理
9. StateGuard         — 登录页状态清理

18.3 免登录路由

框架内置以下免登录路由(在 routes/index.ts 定义):

路由 说明
/login 登录页
/oauth2-app/login OAuth2免登(钉钉/企微)
/tokenLogin Token静默登录
/ssoLogin 智慧人社SSO
/file/share 文件分享

十九、JVxeTable(可编辑行表格)

用于需要行内编辑的场景(如主子表子表、批量录入等)。

19.1 基本使用

<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>

19.2 单元格类型(14种)

类型 说明 对应组件
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

19.3 数据校验(validateRules)

JVxeTable 完全支持对输入数据进行校验检查,包括非空、纯数字、纯字母、正则、自定义函数等。校验规则通过列配置中的 validateRules 数组定义。

19.3.1 两级校验机制

层级 配置位置 说明
组件级 editRules prop 全局 vxe-table 原生 editRules,可对所有列施加通用校验
列级 validateRules 列属性 每列独立配置校验规则数组,优先级更高

两种校验规则通过 lodash.merge 合并后传入底层 vxe-table。

19.3.2 validateRules 规则项字段

字段 类型 说明
required boolean 是否必填(非空校验)
pattern string / RegExp 正则表达式,支持快捷值
message string 校验失败提示,支持 ${title}${key}${defaultValue} 变量替换
validator function 自定义异步校验函数(Promise 形式)
handler function 旧版回调式校验函数(兼容模式,内部转为 validator)
unique boolean 唯一性校验(同一列中值不能重复)

19.3.3 内置快捷校验值(fooPatterns)

组件内置了常用校验规则的快捷值,使用时直接传入 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,遍历整表检查同列值是否重复

19.3.4 使用示例

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}校验不通过',
        }],
    },
];

19.3.5 非编辑组件的必填标记

对于 checkboxradiouploadprogressdepartSelectuserSelectimagefile 等非文本编辑组件,如果配置了 validateRules 且包含 required,会在列头的 title 前自动加 * 标记,提示用户该列为必填。

定义位置:useColumns.ts

19.3.6 手动触发校验

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

19.4 插槽自定义字段输入(slotName + JVxeTypes.slot)

JVxeTable 支持通过插槽(slot)方式自定义某个字段的输入框内容,适用于需要自定义编辑组件(如下拉框、级联选择、日期范围等)且选项数据来自父组件的场景。

19.4.1 插槽机制原理

JVxeTable 的插槽机制分两层:

第一层:JVxeTypes.slot + slotName(推荐方式)

这是 JVxeTable 内置的插槽支持机制,完全集成到行编辑系统中。

  • 列配置中设置 type: JVxeTypes.slot,同时指定 slotName(如 'myFieldSlot'
  • JVxeTable 内部的 JVxeSlotCell 组件负责渲染该单元格
  • useColumns.ts 中的 handleSlots() 方法(源码 )会检查父组件是否传入了同名插槽,若有则将插槽函数注入渲染参数

第二层:$slots 透传至底层 vxe-grid

在 JVxeTable.ts 的 render 函数中,this.$slots 被完整透传给底层的 vxe-grid 组件。这意味着所有传递给 JVxeTable 的插槽也会被 vxe-grid 接收,可以使用 vxe-grid 原生的所有插槽能力。

19.4.2 slot 插槽接收的参数

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 触发值变更,用于将插槽组件的值更新到单元格数据中

19.4.3 下拉框选项来自父组件的实现示例

场景:在 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>

19.4.4 关键注意点

  1. triggerChange 是必须调用的:插槽组件值变更时,必须调用 triggerChange(newValue) 将值同步回 JVxeTable 的数据源,否则表格数据不会更新。

  2. 插槽名称要唯一:每个 JVxeTypes.slot 列需要指定不同的 slotName,同一个 JVxeTable 内不能重名。

  3. slot 列支持校验validateRules 对 slot 列同样生效,非空校验会在列头显示 * 标记。

  4. bordered: false:插槽内部使用 Ant Design 组件时,建议设置 :bordered="false" 以与表格单元格样式融合。

  5. 插槽作用域来自父组件:插槽模板在父组件中编写,可以直接访问父组件的所有响应式数据(props、ref、computed 等),因此下拉选项可以灵活地来自任何父组件数据源。

  6. 与普通 select 类型对比

    • JVxeTypes.select:适用于选项固定或通过 dictCode 从字典加载的场景
    • JVxeTypes.slot:适用于选项来自父组件、需要完全自定义UI、或涉及复杂交互的场景

二十、WebSocket 实时推送

import { useWebSocket } from '/@/hooks/web/useWebSocket';

// 建立连接
connectWebSocket('ws://localhost:8080/websocket');

// 添加监听
onWebSocket((data) => {
  console.log('收到消息:', data);
});

// 移除监听
offWebSocket(callback);

特性:自动重连(10次/5s间隔)、心跳(55s)、全局单例。


二十一、关键源码索引

21.1 核心 Hooks

组件 文件
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

21.2 核心组件

组件 文件
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

21.3 工具类

工具 文件
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

21.4 示例页面

类型 文件
单表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 新增 重新分析前端框架,撰写全面开发规范文档