thinkgem / jeesite-vue

基于 Vue3、Vite、Ant-Design-Vue3、TypeScript、Vue Vben Admin,最先进的技术栈,让初学者能够更快的入门并投入到团队开发中去。包括模块如:组织机构、角色用户、菜单授权、数据权限、系统参数等。强大的组件封装,数据驱动视图。为微小中大型项目的开发,提供现成的开箱解决方案及丰富的示例。
Apache License 2.0
188 stars 21 forks source link

JeeSite

JeeSite Vue3 前端框架

TypeScript-Vue3 Ant Design Vue-4.2 JeeSite-Vue star star


如果你喜欢 JeeSite,请给她一个 ⭐️ Star,您的支持将是我们前行的动力。

技术交流

   JeeSite微信公众号

框架介绍

基于 Vue3、Vite、Ant-Design-Vue、TypeScript、Vue Vben Admin,最先进的技术栈,让初学者能够更快的入门并投入到团队开发中去。包括模块如:组织机构、角色用户、菜单授权、数据权限、系统参数等。强大的组件封装,数据驱动视图。为微小中大型项目的开发,提供现成的开箱解决方案及丰富的示例。

在 Vben Admin 基础上做的改进:

设计特点

定义众多组件,非常贴心的组件属性及小功能,符合 JeeSite 以往的设计思想,列表和表单以数据驱动视图,极大简化了业务功能开发,注释分解详见【源码解析

为什么做数据驱动视图?前端向下兼容一直是最大的问题,有了一套相应的标准,会对框架升级帮助很大。比如你可以非常小的成本,业务代码改动非常小的情况下,去升级前端;数据驱动视图可以为未来自定义拖拽表单做更好的铺垫,数据存储结构更清晰化,更利于维护。

提示:请仔细阅读源码解析,表单视图和列表视图上的注释哦,复杂表单可以多表单联合使用。

演示地址

  1. 地址:http://vue.jeesite.com/

学习准备

安装使用

# 验证
node -v
npm i -g yarn
# 验证
yarn -v
git clone https://gitee.com/thinkgem/jeesite-vue.git
cd jeesite-vue

注意:不要放到中文或带空格的目录下。

yarn config set registry https://registry.npmmirror.com
yarn install
yarn serve

开发环境会加载文件较多,便于调试,请耐心等待。

yarn preview

编译打包后,会整合这些文件,所以访问性能会大大提高,生产环境可以开启 gzip

yarn build

打包完成后,会在根目录生成 dist 文件夹,发布 nginx。

详见文档:https://jeesite.com/docs/vue-install-deploy/#部署到正式服务器

后端服务

# 代理设置,可配置多个,不能换行,格式:[访问接口的根路径, 代理地址, 是否保持Host头]
# VITE_PROXY = [["/js","https://vue.jeesite.com/js",true]]
VITE_PROXY = [["/js","http://127.0.0.1:8980/js",false]]

# 访问接口的根路径(例如:https://vue.jeesite.com)
VITE_GLOB_API_URL = 

# 访问接口的前缀,在根路径之后
VITE_GLOB_API_URL_PREFIX = /js

如果您使用的 VSCode 的话,推荐安装以下插件:

常见问题

软件截图

附录

表单视图

<template>
  <!-- 弹出抽屉组件,如果想改为弹窗,Drawer 换为 Modal 即可快速替换 -->
  <BasicDrawer
    v-bind="$attrs"    -- 传递来自父组件的属性
    :showFooter="true" -- 显示弹窗底部按钮组
    :okAuth="'test:testData:edit'" -- 提交按钮权限,控制按钮是否显示
    @register="registerDrawer"     -- 弹窗后的回调方法
    @ok="handleSubmit" -- 提交按钮调用方法
    width="60%"        -- 弹窗宽度,支持按比例
  >
    <!-- 弹窗标题 -->
    <template #title>
      <Icon :icon="getTitle.icon" class="pr-1 m-1" /> -- 图标
      <span> {{ getTitle.value }} </span>  -- 标题名称
    </template>
    <!-- 表单组件 -->
    <BasicForm @register="registerForm">
      <!-- 定义表单控件插槽、个性化表单控件,如:这是一个表单子表插槽 -->
      <template #testDataChildList>
        <BasicTable
          @register="registerTestDataChildTable"
          @row-click="handleTestDataChildRowClick"
        />
        <!-- 子表新增按钮 -->
        <a-button class="mt-2" @click="handleTestDataChildAdd">
          <Icon icon="i-ant-design:plus-circle-outlined" /> {{ t('新增') }}
        </a-button>
      </template>
    </BasicForm>
  </BasicDrawer>
</template>
<!-- script name: 当前组件名称(与路由名一致,如果不一致会页面缓存失效)-->
<script lang="ts" setup name="ViewsTestTestDataForm">

  // 导入当前用到的对象,部分省略
  import { ref, unref, computed } from 'vue';
  import { officeTreeData } from '/@/api/sys/office';

  // 页面事件定义
  const emit = defineEmits(['success', 'register']);

  // 国际化方法调用,参数是国际化编码的根路径
  const { t } = useI18n('test.testData');

  // 消息弹窗方法
  const { showMessage } = useMessage();

  // 路由meta信息
  const { meta } = unref(router.currentRoute);

  // 当前页面数据记录
  const record = ref<Recordable>({});

  // 当前页面标题定义,来自菜单管理定义
  const getTitle = computed(() => ({
    icon: meta.icon || 'ant-design:book-outlined',
    value: record.value.isNewRecord ? t('新增数据') : t('编辑数据'),
  }));

  // 输入表单控件定义
  const inputFormSchemas: FormSchema[] = [
    {
      label: t('单行文本'), // 控件前面的页签
      field: 'testInput',  // 字段提交参数名
      component: 'Input',  // 控件类型(可自定义,更多查看 componentMap.ts )
      componentProps: {    // 组件属性定义
        maxlength: 200,
      },
      required: true,      // 表单验证,是否必填(快速定义)
      rules: [             // 如果不只是必填,需要通过 rules 定义,举例:
        { required: true },
        { min: 4, max: 20, message: t('请输入长度在 4 到 20 个字符之间') },
        { pattern: /^[\u0391-\uFFE5\w]+$/, message: t('不能输入特殊字符') },
        {
          validator(_rule, value) {
             return new Promise((resolve, reject) => {
              if (!value || value === '') return resolve();
              // 远程验证,访问后台校验数据是否重复
              checkTestInput(record.value.testInput || '', value)
                .then((res) => (res ? resolve() : reject(t('数据已存在'))))
                .catch((err) => reject(err.message || t('验证失败')));
            });
          },
          trigger: 'blur', // 如果是远程验证,可以减少请求频率
        },
      ],
      colProps: { lg: 24, md: 24 }, // 栅格布局(遵循 Ant Design 风格)
    },
    {
      label: t('下拉框'),
      field: 'testSelect',
      component: 'Select',    // 选择框还有 RadioGroup、CheckboxGroup
      componentProps: {
        dictType: 'sys_menu_type', // 下拉框选项数据(支持直接指定字典类型)
        allowClear: true,          // 启用空选项,可清空选择
        mode: 'multiple',          // 下拉框模块,启用多选
      },
    },
    {
      label: t('日期选择'),
      field: 'testDate',
      component: 'DatePicker',
      componentProps: {
        format: 'YYYY-MM-DD',      // 日期选择
        showTime: false,           // 关闭时间选择
      },
    },
    {
      label: t('日期时间'),
      field: 'testDatetime',
      component: 'DatePicker',
      componentProps: {
        format: 'YYYY-MM-DD HH:mm',    // 日期时间选择
        showTime: { format: 'HH:mm' }, // 设置时间的格式
      },
    },
    {
      label: t('用户选择'),
      field: 'testUser.userCode',
      fieldLabel: 'testUser.userName', //【支持返回,如下拉框或树选择的节点名】
      component: 'TreeSelect',         // 树选择控件
      componentProps: {
        api: officeTreeData,           // 数据源 API 定义,支持 ztree 格式
        params: { isLoadUser: true, userIdPrefix: '' }, // API 参数
        canSelectParent: false,        // 是否允许选择父级
        allowClear: true,
      },
    },
    {
      label: t('子表数据'),
      field: 'testDataChildList',
      component: 'Input',
      colProps: { lg: 24, md: 24 },
      slot: 'testDataChildList',      // 指定插槽、个性化控件内容
    },
  ];

  // 当前表单的参数定义
  const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
    labelWidth: 120,                  // 控件前面的标签宽度
    schemas: inputFormSchemas,        // 控件定义列表
    baseColProps: { lg: 12, md: 24 }, // 控件默认栅格布局方式(响应式)
  });

  // 当前表单子表格定义
  const [registerTestDataChildTable, testDataChildTable] = useTable({
    actionColumn: {  // 子表的操作列定义
      width: 60,     // 操作列宽度
      actions: (record: Recordable) => [
        {
          icon: 'i-ant-design:delete-outlined',
          color: 'error',
          popConfirm: { // 是否需要启用确认框
            title: '是否确认删除',
            confirm: handleTestDataChildDelete.bind(this, record),
          },
          auth: 'sys:empUser:edit',  // 按钮权限(可控制按钮是否显示)
        },
      ],
    },
    rowKey: 'id',     // 子表主键名
    pagination: false,// 关闭分页
    bordered: true,   // 开启表格边框
    size: 'small',    // 单元格间距
    inset: true,      // 是否内嵌(去除一些边距)
  });

  // 当前表单子表自动定义
  async function setTestDataChildTableData(_res: Recordable) {
    testDataChildTable.setColumns([
      {
        title: t('单行文本'),
        dataIndex: 'testInput',
        width: 230,
        align: 'left',
        editRow: true,          // 是否启用编辑
        editComponent: 'Input', // 编辑控件(可自定义,更多查看 componentMap.ts )
        editRule: true,         // 控件验证(是否必填)
      },
      {
        title: t('下拉框'),
        dataIndex: 'testSelect',
        width: 130,
        align: 'left',
        dictType: 'sys_menu_type',   // 指定字典类型,自动显示字典标签
        editRow: true,
        editComponent: 'Select',
        editComponentProps: {        // 控件属性
          dictType: 'sys_menu_type', // 下拉框的字段类型
          allowClear: true,
        },
        editRule: false,
      },
      // 更多组件控件不举例了,同表单控件 ...
    ]);
    // 设定子表数据
    testDataChildTable.setTableData(record.value.testDataChildList || []);
  }

  // 点击行,启用编辑
  function handleTestDataChildRowClick(record: Recordable) {
    record.onEdit?.(true, false);
  }

  // 添加编辑行,可指定初始数据
  function handleTestDataChildAdd() {
    testDataChildTable.insertTableDataRecord({
      id: new Date().getTime(),
      isNewRecord: true,
      editable: true,
    });
  }

  // 删除编辑行方法
  function handleTestDataChildDelete(record: Recordable) {
    testDataChildTable.deleteTableDataRecord(record);
  }

  // 获取子表数据(支持返回删除未提交的数据)
  async function getTestDataChildList() {
    let testDataChildListValid = true;
    let testDataChildList: Recordable[] = [];
    for (const record of testDataChildTable.getDataSource()) {
      // 验证控件内容,并取消行的编辑状态(如果验证失败返回false)
      if (!(await record.onEdit?.(false, true))) {
        testDataChildListValid = false;
      }
      testDataChildList.push({
        ...record,
        id: !!record.isNewRecord ? '' : record.id,
      });
    }
    for (const record of testDataChildTable.getDelDataSource()) {
      if (!!record.isNewRecord) continue;
      testDataChildList.push({
        ...record,
        status: '1',
      });
    }
    // 子表验证事件,抛出异常消息
    if (!testDataChildListValid) {
      throw { errorFields: [{ name: ['testDataChildList'] }] };
    }
    return testDataChildList;
  }

  // 弹窗后的回调事件,进行一些表单数据初始化等操作
  const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
    resetFields(); // 重置表单数据
    setDrawerProps({ loading: true }); // 显示加载框
    const res = await testDataForm(data); // 查询表单数据
    record.value = (res.testData || {}) as Recordable;
    setFieldsValue(record.value);  // 设置字段值
    setTestDataChildTableData(res);  // 设置子表数据(没有子表可不写)
    setDrawerProps({ loading: false }); // 隐藏加载框
  });

  // 表单提交按钮方法
  async function handleSubmit() {
    try {
      const data = await validate(); // 验证表单,并返回数据
      setDrawerProps({ confirmLoading: true }); // 显示提交加载中
      // 设置提交的参数(QueryString,后台 Controller 的 get 接受)
      const params: any = {
        isNewRecord: record.value.isNewRecord,
        id: record.value.id,
      };
      // 获取并设置子表数据
      data.testDataChildList = await getTestDataChildList();
      // console.log('submit', params, data, record);
      // 将数据提交给后台(如果失败跳转到 catch)
      const res = await testDataSave(params, data);
      showMessage(res.message); // 显示提交结果
      setTimeout(closeDrawer);  // 隐藏抽屉弹窗
      emit('success', data);    // 触发事件,列表数据刷新
    } catch (error: any) {
      if (error && error.errorFields) {
        showMessage(t('您填写的信息有误,请根据提示修正。'));
      }
      console.log('error', error);
    } finally {
      setDrawerProps({ confirmLoading: false }); // 隐藏提交加载中
    }
  }
</script>

列表视图

<template>
  <div>
    <!-- 表格组件 -->
    <BasicTable @register="registerTable">
      <!-- 表格标题插槽 -->
      <template #tableTitle>
        <Icon :icon="getTitle.icon" class="m-1 pr-1" />
        <span> {{ getTitle.value }} </span>
      </template>
      <!-- 表格右侧按钮插槽,其中 v-auth 是按钮权限控制 -->
      <template #toolbar>
        <a-button type="primary" @click="handleForm({})" v-auth="'test:testData:edit'">
          <Icon icon="i-fluent:add-12-filled" /> {{ t('新增') }}
        </a-button>
      </template>
      <!-- 首列插槽 -->
      <template #firstColumn="{ record }">
        <a @click="handleForm({ id: record.id })">
          {{ record.testInput }}
        </a>
      </template>
    </BasicTable>
    <!-- 点击表格行进入的输入表单弹窗 -->
    <InputForm @register="registerDrawer" @success="handleSuccess" />
  </div>
</template>
<!-- script name: 当前组件名称(与路由名一致,如果不一致会页面缓存失效)-->
<script lang="ts" setup name="ViewsTestTestDataList">

  // 导入当前用到的对象,部分省略
  import InputForm from './form.vue';

  // 国际化方法调用,参数是国际化编码的根路径
  const { t } = useI18n('test.testData');

  // 消息弹窗方法
  const { showMessage } = useMessage();

  // 路由meta信息
  const { meta } = unref(router.currentRoute);

  // 当前页面标题定义,来自菜单管理定义
  const getTitle = {
    icon: meta.icon || 'ant-design:book-outlined',
    value: meta.title || t('数据管理'),
  };

  // 表格搜索表单控件定义
  const searchForm: FormProps = {
    baseColProps: { lg: 6, md: 8 }, // 表单栅格布局
    labelWidth: 90,                 // 表单标签宽度
    schemas: [
      {
        label: t('单行文本'),        // 表单标签
        field: 'testInput',         // 字段提交参数名
        component: 'Input',         // 表单控件
      },
      {
        label: t('下拉框'),
        field: 'testSelect',
        component: 'Select',    // 选择框还有 RadioGroup、CheckboxGroup
        componentProps: {
          dictType: 'sys_menu_type', // 下拉框选项数据(支持直接指定字典类型)
          allowClear: true,          // 启用空选项,可清空选择
          mode: 'multiple',          // 下拉框模块,启用多选
        },
      },
      // 更多控件,再次不展示了,和上一节表单视图一致
    ],
  };

  // 表格列定义
  const tableColumns: BasicColumn[] = [
    {
      title: t('单行文本'),    // 表头标题
      dataIndex: 'testInput', // 表列实体属性名
      key: 'a.test_input',    // 排序数据库字段名
      sorter: true,           // 点击表头是否可排序
      width: 230,             // 列宽
      align: 'left',          // 列的对齐方式
      // 个性化列,可定义插槽(如样式,增加控件等)
      slot: 'firstColumn',
    },
    {
      title: t('下拉框'),
      dataIndex: 'testSelect',
      key: 'a.test_select',
      sorter: true,
      width: 130,
      align: 'center',
      dictType: 'sys_menu_type', // 字典列,快速显示字典标签
    },
  ];

  // 表格操作列定义
  const actionColumn: BasicColumn = {
    width: 160, // 操作列宽
    actions: (record: Recordable) => [
      {
        icon: 'i-clarity:note-edit-line',
        title: t('编辑数据'),
        onClick: handleForm.bind(this, { id: record.id }),
        // 按钮权限控制,指定权限字符串
        auth: 'test:testData:edit',
      },
      {
        icon: 'i-ant-design:stop-outlined',
        color: 'error',
        title: t('停用数据'),
        // 是否需要启用确认框
        popConfirm: {
          title: t('是否确认停用数据'),
          confirm: handleDisable.bind(this, { id: record.id }),
        },
        // 按钮权限控制,指定权限字符串
        auth: 'test:testData:edit',
        // 控制按钮是否显示(区别:show 是显示或隐藏;ifShow 是显示或移除)
        show: () => record.status === '0',
        ifShow: () => record.status === '0',
      },
    ],
    // 操作列更多按钮定义
    dropDownActions: (record: Recordable) => [
      {
        icon: 'i-ant-design:reload-outlined',
        label: t('重置密码'),
        onClick: handleResetpwd.bind(this, { userCode: record.userCode }),
        auth: 'sys:empUser:resetpwd',
      },
    ],
  };

  // 点击首列或编辑按钮是的抽屉弹窗定义
  const [registerDrawer, { openDrawer }] = useDrawer();

  // 表格定义
  const [registerTable, { reload }] = useTable({
    api: testDataListData,     // 表格数据源 API
    beforeFetch: (params) => {
      return params;           // API 提交之前的参数修改
    },
    columns: tableColumns,     // 表格列
    actionColumn: actionColumn,// 操作列
    formConfig: searchForm,    // 搜索表单
    showTableSetting: true,    // 是否显示右上角的设置按钮
    useSearchForm: true,       // 是否显示搜索表单
    canResize: true,           // 是否自适应表单高度
  });

  // 弹窗操作方法
  function handleForm(record: Recordable) {
    openDrawer(true, record);
  }

  // 操作列停用按钮方法
  async function handleDisable(record: Recordable) {
    const res = await testDataDisable(record);
    showMessage(res.message);
    handleSuccess();
  }

  // 刷新表格数据(含表单回调)
  function handleSuccess() {
    reload();
  }
</script>

授权许可协议条款

  1. 基于 Apache License Version 2.0 协议发布,可用于商业项目,但必须遵守以下补充条款。
  2. 不得将本软件应用于危害国家安全、荣誉和利益的行为,不能以任何形式用于非法为目的的行为。
  3. 在延伸的代码中(修改和有源代码衍生的代码中)需要带有原来代码中的协议、版权声明和其他原作者 规定需要包含的说明(请尊重原作者的著作权,不要删除或修改文件中的Copyright@author信息) 更不要,全局替换源代码中的 jeesite 或 ThinkGem 等字样,否则你将违反本协议条款承担责任。
  4. 基于本软件的作品,只能使用 JeeSite5 作为后台服务,除外情况不可商用且不允许二次分发或开源。
  5. 您若套用本软件的一些代码或功能参考,请保留源文件中的版权和作者,需要在您的软件介绍明显位置 说明出处,举例:本软件基于 JeeSite Vue 快速开发平台,并附带链接:http://jeesite.com
  6. 任何基于本软件而产生的一切法律纠纷和责任,均于我司无关。
  7. 如果你对本软件有改进,希望可以贡献给我们,共同进步。
  8. 本项目已申请软件著作权,请尊重开源,感谢阅读。

技术服务与支持

专业版增加的功能

  1. 主题标签页的三种风格自由切换
  2. 业务流程、流程设计、流程办理
  3. 文件管理、上传秒传、文件预览
  4. 高级折叠表单和个性化本地存储
  5. 表格个性化设置参数本地存储
  6. 租户管理功能、租户切换
  7. 动态设置页面字体大小
  8. 消息推送、消息提醒
  9. 语言国际化、本地化
  10. 更多功能详见文档