260603-前端框架全局开发规范与最佳实践.md 54 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

二十、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 新增 重新分析前端框架,撰写全面开发规范文档