JeeSite 快速開發平臺 Vue3 前端版本公測釋出開源了

2022-01-05 11:00:46

介紹

基於 Vue3、Vite、Ant-Design-Vue、TypeScript、Vue Vben Admin,最先進的技術棧,讓初學者能夠更快的入門並投入到團隊開發中去。包括模組如:組織機構、角色使用者、選單授權、資料許可權、系統引數等。強大的元件封裝,資料驅動檢視。為微小中大型專案的開發,提供現成的開箱解決方案及豐富的範例。

在 Vben Admin 基礎上做的改進:

  • 更精緻的介面細節優化改進,非常適合資訊化管理後臺
  • 主題風格改進,不同的佈局風格,選單及許可權體驗優化
  • 頂部選單、分隔選單、混合選單的活動狀態啟用和載入優化改進
  • 樹表支援非同步的封裝,提升展開摺疊效能,支援按層次展開摺疊樹表
  • 樹結構新增快捷重新整理、動態生成樹、層次獨立和不獨立的資料返回相容
  • 增加左樹右表功能展示,可摺疊左樹,樹元件增加預設圖示
  • 表單元件適應各種資料格式來源,特別是多選字串到陣列的互轉相容
  • 表單新增各種便捷屬性和表單元件,下拉框和樹選擇支援標籤名回顯
  • 表單元件,改進摺疊表單 Action 的演演算法,智慧化佈局
  • 表格元件,Action 更多,支援橫向顯示操作,更方便
  • 表格元件,子表編輯改進,表格列排序和重置改進優化
  • 新增字典元件,支援展示到表格列、表單元件下拉框單選框等
  • 字典標籤支援 Tag、Badge、自定義 class、style 等,顯示風格
  • 更方便的支援 Tab 頁面的快取,切換頁籤的時候不過載頁面內容
  • Tab 頁籤介面美化、圖示顯示、任何標籤上右鍵,可快速重新整理等等
  • 全域性 Axios 改進,相容各種資料格式,超時訊息提醒改進
  • 功能許可權鑑權改進,並相容本地路由和後臺路由同時使用
  • 等等各種細節改進,體驗優化,黑暗佈局細節優化
  • Vue端完全開源,用上你就會愛上,實在太方便了

設計特點

定義眾多元件,非常貼心的元件屬性及小功能,符合 JeeSite 以往的設計思想,列表和表單以資料驅動檢視,極大簡化了業務功能開發,註釋分解詳見本頁最下【原始碼解析】

為什麼做資料驅動檢視?前端向下相容一直是最大的問題,有了一套相應的標準,會對框架升級幫助很大。比如你可以非常小的成本,業務程式碼改動非常小的情況下,去升級前端;資料驅動檢視可以為未來自定義拖拽表單做更好的鋪墊,資料儲存結構更清晰化,更利於維護。

提示:請仔細閱讀原始碼解析,表單檢視和列表檢視上的註釋哦,複雜表單可以多表單聯合使用。

截圖鑑賞

演示地址

  1. 地址:
  2. 賬號:system
  3. 密碼:admin

學習準備

  •    - 開發環境
  •  - 熟悉 Vite 特性
  •  - 熟悉 Vue 基礎語法
  •  - 熟悉 TypeScript 基本語法
  •  - 熟悉 ES6 基本語法
  •  - 熟悉 vue-router 基本使用
  •  - 熟悉 UI 及表單列表及常用元件使用
  •  - 熟悉 UI 基本使用
  •  - mockjs 基本語法
  • JeeSite-v5 - 後臺服務

安裝使用

  • 如果沒有安裝 Node.js(不低於 14,建議 16)
下載安裝 Node.js 16:https://nodejs.org
  • 如果沒有安裝 Yarn 執行安裝(要求 Yarn v1.x)
npm i -g yarn
  • 獲取程式碼
git clone https://gitee.com/thinkgem/jeesite-vue.git
  • 安裝依賴
cd jeesite-vue

yarn config set registry https://registry.npm.taobao.org
yarn install
  • 執行
yarn serve

開發環境會載入檔案較多,便於偵錯,請耐心等待。

編譯打包後,會合並這些檔案,所以存取效能會大大提高。

  • 打包
yarn build

打包完成後,會在根目錄生成 dist 資料夾,釋出 nginx。

有一些打包引數,詳見 .env.production 裡面有註釋。

如果您使用的 VSCode 的話,推薦安裝以下外掛:

  •  - Iconify 圖示外掛
  •  - windicss 提示外掛
  •  - i18n 外掛
  •  - Vue3 開發必備(Vetur禁用)
  •  - 指令碼程式碼檢查
  •  - 程式碼格式化
  •  - CSS 格式化
  •  - .env 檔案高亮

常見問題

  • 如何將表單抽屜改為彈窗,替換 list 和 form 頁面的 Drawer 為 Modal 即可。
  • 瀏覽器支援情況:支援所有現代瀏覽器,Vue3 已不再支援 IE 瀏覽器。

原始碼解析

表單檢視

<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="ant-design:plus-circle-outlined" /> {{ t('新增') }}
        </a-button>
      </template>
    </BasicForm>
  </BasicDrawer>
</template>
<script lang="ts">
  export default defineComponent({
    // 當前元件名稱(與路由名一致,如果不一致會頁面快取失效)
    name: 'ViewsTestTestDataForm',
  });
</script>
<script lang="ts" setup>

  // 匯入當前用到的物件,部分省略
  import { defineComponent, ref } from 'vue';
  import { officeTreeData } from '/@/api/sys/office';
  import { areaTreeData } from '/@/api/sys/area';

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

  // 國際化方法呼叫,引數是國際化編碼的跟路徑
  const { t } = useI18n('test.testData');

  // 訊息彈窗方法
  const { showMessage } = useMessage();

  // 當前頁面資料記錄
  const record = ref<Recordable>({});

  // 當前頁面標題定義,來自選單管理定義
  const getTitle = {
    icon: router.currentRoute.value.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('驗證失敗')));
            });
          },
        },
      ],
      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: '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 new Error('testDataChildList valid.');
    }
    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="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 lang="ts">
  export default defineComponent({
    // 當前元件名稱(與路由名一致,如果不一致會頁面快取失效)
    name: 'ViewsTestTestDataList',
  });
</script>
<script lang="ts" setup>

  // 匯入當前用到的物件,部分省略
  import { defineComponent } from 'vue';
  import InputForm from './form.vue';

  // 國際化方法呼叫,引數是國際化編碼的跟路徑
  const { t } = useI18n('test.testData');

  // 訊息彈窗方法
  const { showMessage } = useMessage();

  // 當前頁面標題定義,來自選單管理定義
  const getTitle = {
    icon: router.currentRoute.value.meta.icon || 'ant-design:book-outlined',
    value: router.currentRoute.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',          // 列的對齊方式
      // 個性化列,可定義插槽(如樣式,增加控制元件等)
      slots: { customRender: '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: 'clarity:note-edit-line',
        title: t('編輯資料'),
        onClick: handleForm.bind(this, { id: record.id }),
        // 按鈕許可權控制,指定許可權字串
        auth: 'test:testData:edit',
      },
      {
        icon: '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: '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>
展開閱讀全文