260603-前端框架全局开发规范与最佳实践.md 92 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)、全局单例。


二十一、树形表格(Tree Table)

21.1 概述

本项目的树形表格功能没有独立的 JTreeTable 组件,而是完全集成在 BasicTable 核心组件中,通过设置 isTreeTable: true 属性启用。底层基于 Ant Design Vue 的 Table 组件原生树形数据支持(带有 children 字段的数据结构),框架在此基础上封装了展开/折叠全部、层级选择联动等功能。

21.2 核心属性说明

属性 类型 默认值 说明
isTreeTable boolean false 是否启用树形表格
childrenColumnName string 'children' 子节点字段名
indentSize number 24 树形缩进像素
expandedRowKeys string[] 展开的行 key 数组
defaultExpandAllRows boolean 是否默认展开所有行
defaultExpandedRowKeys string[] 默认展开的行 key

重要约束:树形表格与序号列互斥。启用 isTreeTable: true 后,showIndexColumn 会自动失效(框架内部在 useColumns.ts 中处理)。

21.3 方式一:useListPage 模式(推荐,业务最常用)

适用于后端接口返回树形数据结构的场景,如菜单管理、分类字典等。

<template>
  <div>
    <BasicTable @register="registerTable" :rowSelection="rowSelection">
      <template #tableTitle>
        <a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleCreate">新增</a-button>
      </template>
      <template #action="{ record }">
        <TableAction :actions="getTableAction(record)" />
      </template>
      <!-- 工具栏:展开/折叠全部 -->
      <template #toolbar>
        <a-button type="primary" @click="expandAll">展开全部</a-button>
        <a-button type="primary" @click="collapseAll">折叠全部</a-button>
      </template>
    </BasicTable>
  </div>
</template>

<script lang="ts" setup>
  import { BasicTable, TableAction } from '/@/components/Table';
  import { useListPage } from '/@/hooks/system/useListPage';
  import { columns, searchFormSchema } from './xxx.data';
  import { list } from './xxx.api';

  const { tableContext } = useListPage({
    tableProps: {
      title: '树形数据列表',
      api: list,
      columns,
      isTreeTable: true,        // 关键:启用树形表格
      pagination: false,        // 树形表格通常不分页
      showIndexColumn: false,   // 树形表格不显示序号列
      formConfig: { schemas: searchFormSchema },
    },
  });

  // expandAll 和 collapseAll 来自 tableContext 解构
  const [registerTable, { reload, expandAll, collapseAll }, { rowSelection, selectedRowKeys }] = tableContext;

  function handleCreate() {
    // TODO: 打开新增弹窗
  }

  function getTableAction(record) {
    return [
      { label: '编辑', onClick: () => { /* TODO */ } },
      { label: '添加下级', onClick: () => { /* TODO */ } },
      { label: '删除', popConfirm: { title: '确定删除吗?', confirm: () => { /* TODO */ } } },
    ];
  }
</script>

21.4 方式二:useTable 模式(简洁,Demo 常用)

适用于本地静态数据或简单场景。

<script lang="ts">
  import { defineComponent } from 'vue';
  import { BasicTable, useTable } from '/@/components/Table';

  export default defineComponent({
    components: { BasicTable },
    setup() {
      const [register, { expandAll, collapseAll }] = useTable({
        title: '树形表格示例',
        isTreeTable: true,                    // 启用树形表格
        rowSelection: { type: 'checkbox' },
        columns: [
          { title: '名称', dataIndex: 'name', key: 'name' },
          { title: '年龄', dataIndex: 'age', key: 'age' },
          { title: '地址', dataIndex: 'address', key: 'address' },
        ],
        dataSource: [
          {
            id: '1',
            name: '父节点1',
            age: 60,
            address: '地址1',
            children: [
              { id: '1-1', name: '子节点1-1', age: 30, address: '地址1-1' },
              { id: '1-2', name: '子节点1-2', age: 28, address: '地址1-2' },
            ],
          },
          {
            id: '2',
            name: '父节点2',
            age: 50,
            address: '地址2',
            children: [
              { id: '2-1', name: '子节点2-1', age: 22, address: '地址2-1' },
            ],
          },
        ],
        rowKey: 'id',
      });

      return { register, expandAll, collapseAll };
    },
  });
</script>

21.5 方式三:async/await 异步懒加载子节点

适用于子节点数据量大的场景,通过 @expand 事件监听节点展开,动态加载子节点数据。

<template>
  <div>
    <BasicTable @register="registerTable" @expand="handleExpand">
      <template #tableTitle>
        <a-button type="primary" @click="handleCreate">新增</a-button>
      </template>
      <template #action="{ record }">
        <TableAction :actions="getTableAction(record)" />
      </template>
    </BasicTable>
  </div>
</template>

<script lang="ts" setup>
  import { ref } from 'vue';
  import { BasicTable, TableAction } from '/@/components/Table';
  import { useListPage } from '/@/hooks/system/useListPage';
  import { columns, searchFormSchema } from './xxx.data';
  import { list, getChildList } from './xxx.api';

  const expandedRowKeys = ref([]);

  const { tableContext } = useListPage({
    tableProps: {
      api: list,
      columns,
      isTreeTable: true,
      formConfig: { schemas: searchFormSchema },
    },
  });

  const [registerTable, { reload, updateTableDataRecord, findTableDataRecord }] = 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') {
          // 有子节点的,先占位一个 loading 标记,展开时再加载
          item.children = [{ id: item.id + '_loadChild', name: 'loading...', isLoading: true }];
        }
        return item;
      });
    }
  }

  // 列表数据加载成功后,对顶层数据也做标记处理
  function onFetchSuccess(result) {
    getDataByResult(result.items);
  }

  function handleCreate() { /* TODO */ }

  function getTableAction(record) {
    return [
      { label: '编辑', onClick: () => { /* TODO */ } },
      { label: '添加下级', onClick: () => { /* TODO: openModal with pid */ } },
      { label: '删除', popConfirm: { title: '确定删除吗?', confirm: () => { /* TODO */ } } },
    ];
  }
</script>

21.6 方式四:直接使用 BasicTable props(组件式)

适用于不方便使用 useTable/useListPage 的场景,直接以 props 方式传入所有配置。

<template>
  <BasicTable
    ref="tableRef"
    :isTreeTable="true"
    :expandedRowKeys="expandedRowKeys"
    @expandedRowsChange="handleExpandedRowsChange"
    @expand="handleExpand"
    rowKey="id"
    :canResize="true"
    :bordered="true"
    :showIndexColumn="false"
    :loading="loading"
    :columns="columns"
    :dataSource="dataSource"
    :pagination="pagination"
    :rowSelection="rowSelection"
    :actionColumn="actionColumn"
    :showTableSetting="true"
    :clickToRowSelect="false"
    @table-redo="reload"
  />
</template>

<script lang="ts" setup>
  import { ref } from 'vue';
  import { BasicTable } from '/@/components/Table';

  const tableRef = ref();
  const expandedRowKeys = ref([]);
  const loading = ref(false);
  const columns = ref([ /* ... */ ]);
  const dataSource = ref([ /* ... */ ]);

  function handleExpandedRowsChange(keys) {
    expandedRowKeys.value = keys;
  }

  async function handleExpand(expanded, record) {
    // 异步加载子节点逻辑
  }

  function reload() {
    // 刷新逻辑
  }
</script>

21.7 层级选择联动

树形表格支持父子节点层级关联选择(如勾选父节点自动全选子节点)。useCustomSelection.tsx 中实现了两个关键方法:

  • onSelectChild:选中/取消节点时,联动其所有下级节点
  • onSelectParent:根据子节点的选中状态,自动更新父节点的选中状态(如子节点全选则父节点选中,否则半选)

使用 rowSelection 配置即可自动启用:

const { tableContext } = useListPage({
  tableProps: {
    isTreeTable: true,
    rowSelection: { type: 'checkbox' },  // 支持 checkbox 多选和 radio 单选
    // ...
  },
});

21.8 常见树形表格方法

来自 TableActionType 接口(table.ts):

方法 说明
expandAll() 展开所有树节点
collapseAll() 折叠所有树节点

使用方式

// useListPage 模式
const [registerTable, { expandAll, collapseAll }] = tableContext;

// useTable 模式
const [register, { expandAll, collapseAll }] = useTable({ ... });

模板中通过工具栏按钮触发:

<template #toolbar>
  <a-button type="primary" @click="expandAll">展开全部</a-button>
  <a-button type="primary" @click="collapseAll">折叠全部</a-button>
</template>

21.9 业务使用示例索引

页面 文件 模式 特点
菜单管理 views/system/menu/index.vue useListPage 不分页、后端树形数据
分类字典 views/system/category/index.vue useListPage 异步懒加载子节点
在线表单树列表 views/super/online/cgform/auto/tree/OnlineAutoTreeList.vue 直接 props 动态列、复杂交互
树形表格 Demo views/demo/table/TreeTable.vue useTable 本地静态数据
异步树表格 Demo views/demo/jeecg/AsyncTreeTable.vue useTable 异步懒加载示例
文档树表格 Demo views/demo/document/table/TreeTableDemo.vue useListPage 带工具栏按钮

21.10 关键源码文件

文件 职责
components/Table/src/BasicTable.vue 核心表格组件,集成树形表格展开逻辑
components/Table/src/hooks/useTableExpand.ts 树形表格展开/折叠 Hook(expandAll/collapseAll/getExpandOption)
components/Table/src/hooks/useTable.ts useTable 暴露 expandAll/collapseAll 方法
components/Table/src/hooks/useColumns.ts 树形表格时跳过序号列(isTreeTable 互斥)
components/Table/src/hooks/useCustomSelection.tsx 树形表格层级选择联动(onSelectChild/onSelectParent)
components/Table/src/props.ts isTreeTable/expandedRowKeys/indentSize 等属性定义
components/Table/src/types/table.ts 完整 TypeScript 类型定义(BasicTableProps / TableActionType)
components/Table/src/components/settings/ColumnSetting.vue 树形表格时禁用序号列配置项

二十二、树形下拉选择组件(TreeSelect 表单组件族)

22.1 概述

本项目在表单(编辑/查询)中提供了 5 个树形下拉选择组件,全部基于 Ant Design Vue 的 <a-tree-select> 封装,已在 componentMap.ts 中注册,可通过 schema 的 component 字段直接使用。

组件名 注册名 适用场景 数据来源
TreeSelect TreeSelect 通用树选择(需手动传 treeData) 静态数据或自行加载
JTreeSelect JTreeSelect 最常用,数据库表异步树 通过 dict 指定表名加载
ApiTreeSelect ApiTreeSelect API 接口驱动的树选择 传入 API 函数加载
JTreeDict JTreeDict 分类字典树选择 /sys/category/ 系列 API
JCategorySelect JCategorySelect 分类下拉树选择(旧版) /sys/category/ 系列 API

22.2 TreeSelect — Ant Design Vue 原生组件

直接使用 a-tree-select,未做二次封装,是最基础的树下拉组件。

注册位置:componentMap.ts#L25

componentMap.set('TreeSelect', createAsyncComponent(() => import('ant-design-vue/es/tree-select')));

Schema 用法

{
  field: 'parentId',
  label: '上级节点',
  component: 'TreeSelect',
  componentProps: {
    treeData: [                          // 手动传入树形数据
      {
        title: '根节点',
        value: '1',
        children: [
          { title: '子节点1', value: '1-1' },
          { title: '子节点2', value: '1-2' },
        ],
      },
    ],
    placeholder: '请选择上级节点',
    treeDefaultExpandAll: true,          // 默认展开所有节点
    allowClear: true,
    showSearch: true,                    // 支持搜索
    treeNodeFilterProp: 'title',         // 按 title 过滤
  },
}

适用场景:选项固定或前端已知树数据,需要完全控制 treeData

22.3 JTreeSelect — 异步数据库树下拉(最常用)

文件:JTreeSelect.vue

注册位置:componentMap.ts#L80

通过配置 dict 表名即可自动从数据库加载树形数据,支持异步懒加载子节点、单选/多选、搜索过滤、复选框勾选等。

Props 详解

属性 类型 默认值 说明
dict string 'id' 核心属性,格式:表名,显示字段,存储字段,如 sys_depart,depart_name,id
pidField string 'pid' 父节点字段名
pidValue string '' 根节点的父 ID 值(空表示查询父节点为空的根记录)
hasChildField string '' 判断是否有子节点的字段名(如 has_child
converIsLeafVal number 1 hasChildField 配合,值为该值时表示叶子节点
condition string '' 额外查询条件,JSON 字符串格式
multiple boolean false 是否多选
treeCheckAble boolean false 是否显示复选框(父子联动勾选)
url string '' 自定义数据加载 URL(覆盖默认的 /sys/dict/loadTreeData
params object {} 配合 url 使用的请求参数
hiddenNodeKey string '' 隐藏指定节点(编辑时防止选自己为父节点)
loadTriggleChange boolean false 单选模式下,初始加载后是否触发 change 事件
reload number 1 改变此值强制重新加载树数据

数据加载机制

  1. 根节点加载:根据 dict 解析表名、文本字段、存储字段,调用 /sys/dict/loadTreeData 接口传入 pidFieldpidValue 等参数获取根节点
  2. 子节点懒加载:展开节点时,调用同一接口传入当前节点的 key 作为 pid 获取子节点
  3. 回显翻译:根据 value 调用 /sys/dict/loadDictItem/{dict} 接口翻译为标签文本
  4. 自定义 URL:配置 url 后,所有数据加载走自定义接口,不依赖默认字典接口

Schema 使用示例

基础用法 — 单选

{
  field: 'deptId',
  label: '所属部门',
  component: 'JTreeSelect',
  componentProps: {
    dict: 'sys_depart,depart_name,id',   // 表名,显示字段,存储字段
    pidField: 'parent_id',
    pidValue: '',                         // 空=查根节点
    placeholder: '请选择部门',
  },
}

多选 + 复选框

{
  field: 'roles',
  label: '角色',
  component: 'JTreeSelect',
  componentProps: {
    dict: 'sys_role,role_name,id',
    multiple: true,
    treeCheckAble: true,                  // 显示复选框,父子联动
    placeholder: '请选择角色',
  },
}

自定义 URL 加载

{
  field: 'categoryId',
  label: '分类',
  component: 'JTreeSelect',
  componentProps: {
    url: '/biz/category/treeData',        // 自定义数据接口
    params: { type: 'product' },          // 额外请求参数
    placeholder: '请选择分类',
  },
}

编辑时隐藏自身节点(防止选自己为父节点):

{
  field: 'parentId',
  label: '上级节点',
  component: 'JTreeSelect',
  componentProps: {
    dict: 'sys_category,name,id',
    hiddenNodeKey: recordId,              // 隐藏当前编辑记录的节点
  },
}

搜索表单中使用

export const searchFormSchema: FormSchema[] = [
  {
    field: 'deptId',
    label: '部门',
    component: 'JTreeSelect',
    componentProps: {
      dict: 'sys_depart,depart_name,id',
      pidField: 'parent_id',
      placeholder: '请选择部门',
    },
    colProps: { span: 6 },
  },
];

22.4 ApiTreeSelect — API 函数驱动的树下拉

文件:ApiTreeSelect.vue

注册位置:componentMap.ts#L26

传入一个返回树数据的 API 函数,组件自动调用获取数据并渲染为下拉树。适合接口返回格式与默认字典接口不一致的场景。

Props 详解

属性 类型 默认值 说明
api Function 核心属性,返回树数据的异步函数,签名为 (arg?: Recordable) => Promise<Recordable>
params Object 传给 api 函数的参数,变化时自动重新加载(deep watch)
immediate boolean true 是否在 mounted 时立即加载
resultField string '' 如果 API 返回的是 { data: treeData } 格式,用此字段指定取数据的路径

Schema 使用示例

基础用法

import { defHttp } from '/@/utils/http/axios';

{
  field: 'areaId',
  label: '所属区域',
  component: 'ApiTreeSelect',
  componentProps: {
    api: (params) => defHttp.get({ url: '/biz/area/tree', params }),
    params: { type: 'district' },
    placeholder: '请选择区域',
    treeDefaultExpandAll: true,
  },
}

带 resultField 的用法

{
  field: 'orgId',
  label: '组织机构',
  component: 'ApiTreeSelect',
  componentProps: {
    api: (params) => defHttp.get({ url: '/sys/org/tree', params }),
    resultField: 'data.items',            // 返回 { data: { items: [...] } } 时指定路径
    placeholder: '请选择组织机构',
  },
}

特点

  • 支持 params 变化时自动重新加载
  • 加载中时 suffixIcon 显示旋转 Loading 图标
  • 可透传 a-tree-select 的所有原生属性(通过 useAttrs

22.5 JTreeDict — 分类字典树下拉

文件:JTreeDict.vue

注册位置:componentMap.ts#L75

专门用于分类字典数据的树形下拉,数据从 /sys/category/ 系列接口加载。

Props 详解

属性 类型 默认值 说明
value string '' v-model 绑定值
field string 'id' 存储字段,'id' 存主键或 'code' 存编码
parentCode string '' 父节点编码,用于指定加载哪个分类树
async boolean false 是否启用异步加载子节点(懒加载)

Schema 使用示例

{
  field: 'categoryId',
  label: '所属分类',
  component: 'JTreeDict',
  componentProps: {
    parentCode: 'product_type',           // 分类编码
    field: 'id',                          // 存储 id(默认),也可用 'code' 存储编码
    async: true,                          // 子节点懒加载
    placeholder: '请选择分类',
  },
}

22.6 JCategorySelect — 分类下拉树(旧版)

文件:JCategorySelect.vue

注册位置:componentMap.ts#L68

另一个分类树下拉选择组件,与 JTreeDict 功能类似但接口调用方式不同。通过 pcode 指定分类编码,使用 loadTreeData / loadDictItem API 加载数据。

Props 详解

属性 类型 默认值 说明
value string/array v-model 绑定值
placeholder string '请选择' 占位文本
disabled boolean false 是否禁用
condition string '' 额外查询条件(JSON 字符串)
multiple boolean/string false 是否多选
pid string '' 指定根节点的父 ID
pcode string '' 分类编码(常用),默认为 '0'
back string '' 回传字段名,选中时将 label 回传到 change 事件的第二个参数
loadTriggleChange boolean false 初始加载后是否触发 change

Schema 使用示例

{
  field: 'category',
  label: '分类',
  component: 'JCategorySelect',
  componentProps: {
    pcode: 'product_category',           // 分类编码
    multiple: false,
    placeholder: '请选择分类',
  },
}

22.7 组件选型指南

需要树形下拉选择?
├── 数据来自数据库表(表名已知)
│   └── 使用 JTreeSelect + dict 属性                 ← 最常用
├── 数据来自自定义 API(接口格式不标准)
│   └── 使用 ApiTreeSelect + api 函数
├── 数据来自分类字典(/sys/category/)
│   ├── 新版分类字典 → 使用 JTreeDict
│   └── 旧版分类字典 → 使用 JCategorySelect
└── 数据来自前端静态变量
    └── 使用 TreeSelect(原生 ant-design-vue)

22.8 通用注意事项

  1. 值绑定格式:所有树选择组件内部使用 labelInValue 模式({ value, label } 对象),向外部 emit 时会自动转换为纯字符串/数组,表单通过 v-model 绑定时无需关心内部格式
  2. 下拉容器:默认强制 getPopupContainer: (node) => node?.parentNode,使下拉菜单挂载到父节点而非 body,避免滚动时位置偏移
  3. 回显翻译:编辑回填时,组件会根据 value 自动请求接口获取对应的 label 文本展示
  4. 搜索支持:所有组件基于 a-tree-select,天然支持输入搜索过滤节点
  5. 懒加载JTreeSelectJTreeDictJCategorySelect 均支持展开节点时异步加载子节点,避免一次性加载大量数据

22.9 关键源码文件

文件 职责
components/Form/src/jeecg/components/JTreeSelect.vue 核心异步树下拉组件(dict 模式)
components/Form/src/components/ApiTreeSelect.vue API 驱动树下拉组件
components/Form/src/jeecg/components/JTreeDict.vue 分类字典树下拉
components/Form/src/jeecg/components/JCategorySelect.vue 分类下拉树(旧版)
components/Form/src/componentMap.ts 组件注册映射表
components/Form/src/types/index.ts 表单组件类型定义
components/jeecg/JVxeTable/src/components/cells/JVxeTreeSelectCell.vue JVxeTable 表格内树选择单元格

二十三、关键源码索引

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

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

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

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