Step by Step之Vben Admin項目集成系列(四)

2024年2月6日 44点热度 0人点赞

前面三篇介紹了從基礎登錄、菜單功能、菜單管理的集成步驟,以及新功能及對應新數據表的集成(包括列表頁、修改或新增頁等),本篇在此基礎上補充一些相對復雜功能的實現。

一、圖片上傳及顯示功能

1、圖片上傳

Vben已經內置了上傳組件,圖片上傳組件為ImageUpload,參照前面幾篇介紹的代碼文件組織方法。

(1)API接口匹配

參照前後端接口,修改圖片上傳的API接口名稱

// 在/src/api/sys/upload.ts中
//...原有代碼
/**
 * @description file upload url
 */
const uploadFileUrl = uploadUrl   '<更換為與後端對應的API URL>';
//...原有代碼
  return defHttp.uploadFile<UploadApiResult>(
    {
      url: uploadFileUrl,   // 這一行進行了修改,原為uploadUrl
      onUploadProgress,
    },
    params,
  );
}

(2)表單字段配置

修改表單配置文件,一般為<model>.data.ts文件

// 在/src/view/<模塊名>/<模塊名>.data.ts中
//...原有代碼
import { uploadApi } from '@/api/sys/upload';
//...原有代碼
export const formSchema: FormSchema[] = [
//...原有代碼
  {
    label: '分類圖片',
    field: 'pic',
    component: 'ImageUpload', // 選擇圖片上傳的類型
    labelWidth: 100,
    required: true,
    componentProps: {
      api: uploadApi, // 圖片上傳API
      accept: ['png', 'jpeg', 'jpg', 'gif'], // 限定可上傳圖片格式
      maxNumber: 1, // 允許單圖片上傳
    },
  },
//...原有代碼

新增圖片的效果如下圖所示:

2、圖片顯示

(1)全局變量定義和使用

公司之前的項目將圖片上傳到後端時,隻保存相對路徑,如/2024/01/27/wo8234kd8.jpg等,顯示時需要增加圖片服務器域名,如img.site-name.com,目前都是通過在環境配置文件中定義,如在.env.development和.env.production中分別定義研發環境和生產環境的圖片服務器域名地址。

// 在.env.development中
//...原有代碼
# 靜態資源文件url
VITE_GLOB_RESOURCES_URL = 'https://img.site-name.com/'

(2)列表頁中顯示

// 在/src/view/<模塊名>/<模塊名>.data.ts中
//...原有代碼
export const columns: BasicColumn[] = [
//...原有代碼
  {
    title: '分類圖片',
    dataIndex: 'pic',
    customRender: ({ record }) => {
      return h(Image, { width: 200, height: 100, src: import.meta.env.VITE_GLOB_RESOURCES_URL   record.pic });
    },
  },
//...原有代碼

(3)修改頁中顯示

一般情況下,是不需要對Drawer頁面進行修改,但因為圖片顯示需要拼接圖片服務器地址,因此需要在調用Drawer時,復制數據前對pic字段進行修改。

// 在/src/view/<模塊名>/<模塊名>Drawer.ts中
//...原有代碼
  const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
//...原有代碼
    if (unref(isUpdate)) {
      let record = { ...data.record }; // 深層復制,因為下面的修改字段僅影響傳遞到Form中的數值
      record.pic = import.meta.env.VITE_GLOB_RESOURCES_URL   record.pic;
      setFieldsValue({
        ...record,
      });
    }
  });
//...原有代碼

二、層級功能的使用

常見的層級功能有兩類,一類是樹狀的層級,它在業務中使用非常廣泛,如組織結構中的部門層級、商品分類中的類型層級、行政區劃中的地址層級等等;另一類是子表單,即在某對象的一個實例數據中包含子對象,尤其是子對象列表。

下面分別進行演示。

1、樹狀列表

在列表頁中,要展現層級形式,需要進行屬性配置,並對API返回的數據形式有要求。

// 在/src/view/<模塊名>/index.vue中
//...原有代碼
  const [registerTable, { reload }] = useTable({
    title: '產品分類列表',
    api: getCategoryList,
    columns,
    formConfig: {
      labelWidth: 120,
      schemas: searchFormSchema,
    },
    isTreeTable: true, // 指明是一個有樹狀層級的表
    childrenColumnName: 'categories', // 指明子層級的字段名稱
//...原有代碼

即要求getCategoryList返回的數據形式為層級形式,且指向下一層級的字段名稱為categories,查閱源碼可以發現如果這個字段名稱為children則不需要在useTable的調用參數顯式指定。參考數據形式如下:

[{
  id: 1,
  categoryName: '1',
  pic: '2024/01/28/1.jpg',
  categories: [{
      id: 11,
      categoryName: '11',
      pic: '2024/01/28/11.jpg',
      categoryies: null,
    },
    {
      id: 12,
      categoryName: '12',
      pic: '2024/01/28/12.jpg',
      categoryies: null,
    },
    {
      id: 13,
      categoryName: '13',
      pic: '2024/01/28/13.jpg',
      categoryies: null,
    }],
},
{
  id: 2,
  categoryName: '2',
  pic: '2024/01/28/2.jpg',
  categories: null,
}]

在列表頁的展示效果如下,如果某行數據下面有子節點,則在第一個字段的前面有一個符號“⊕”,點擊就可以展開或者折疊。

2、樹狀選項

應用場景如下圖,即層級節點的增加或修改時,需要選擇父節點時,一般使用帶有層級的下拉框。

這個功能的實現就很簡單了,Vben Admin已經封裝得很到位,配置即可。

// 在/src/view/<模塊名>/<模塊名>.data.ts中
//...原有代碼
export const formSchema: FormSchema[] = [
//...原有代碼
  {
    label: '上級分類',
    field: 'parentId',
    component: 'ApiTreeSelect', // 選擇此類型,則調用API並返回樹形結構
    labelWidth: 100,
    componentProps: {
      api: getCategoryList, // API名稱
      labelField: 'categoryName', // 顯示字段名稱,即上圖中“選項0”對應的字段
      valueField: 'categoryId', // 選擇了這個選項後,復制給parentId的字段
    },
  },
//...原有代碼

3、子表單

項目中的場景是一張訂單中包含的商品列表,需要在列表中展現,效果圖如下:

即每個包含子表單的訂單行前面也有符號“⊕”,點擊可展開或折疊商品列表。

技術實現原理是在原來的表格上,再定義一張子表,然後在點擊展開按鈕時,動態加載這張子表,涉及的相關代碼如下:

//在/src/view/<模塊名>/<模塊名>.data.ts中
//...原有代碼
// inner table of items of order
export const innerColumns: BasicColumn[] = [
  {
    title: '訂單商品編號',
    dataIndex: 'orderItemId',
    width: 100,
    ifShow: false,
  },
  {
    title: '商品名稱',
    dataIndex: 'prodName',
    width: 200,
  },
  {
    title: '商品圖片',
    dataIndex: 'pic',
    customRender: ({ record }) => {
      return h(Image, { width: 200, height: 100, src: import.meta.env.VITE_GLOB_RESOURCES_URL   record.pic });
    },
  },
  {
    title: '單價',
    dataIndex: 'price',
    width: 100,
    customRender: ({ record }) => {
      return h(Statistic, { precision: 2, value: record.price });
    },
  },
  {
    title: '數量',
    dataIndex: 'prodCount',
    width: 80,
  },
  {
    title: '小計',
    dataIndex: 'productTotalAmount',
    width: 200,
    customRender: ({ record }) => {
      return h(Statistic, { precision: 2, value: record.productTotalAmount });
    },
  },
];
//...原有代碼
// 在/src/view/<模塊名>/index.vue中
//...原有代碼
      <template #expandedRowRender="{ record }"> // 母表的行變量
        <BasicTable
          :columns="innerColumns"
          :data-source="record.orderItems" // 子表的數據源為母表的子表單字段
          :pagination="false" // 子表不需要分頁
          :scroll="{ x: true, y: true }" // 緊湊顯示
        >
          <template #bodyCell></template>
        </BasicTable>
      </template>
    </BasicTable>
    <OrderDrawer @register="registerDrawer" @success="handleSuccess" />
//...原有代碼
  import { columns, innerColumns, searchFormSchema } from './order.data'; // 引入子表的列變量
//...原有代碼

這裡有兩個要點:一是使用#expandedRowRender來獲取母表每一行的數據,以作為子表的數據源;二是通過pagination和scroll來控制子表的顯示效果,如果子表數據量很大,可以分頁,否則一般不需要分頁,而如果不設置scroll屬性,會發現子表數據顯示完之後,會一直空白拉伸到頁面最下方,UI效果很差。

三、表單詳情頁定制開發

默認的表單詳情頁就是前面幾篇中介紹的,使用FormSchema,一個字段一個輸入組件,雖然也可以使用分割線,但總體來說非常死板。

而在項目中,訂單的詳情頁非常復雜,這就需要進行展示頁面定制,不再使用FormSchema機制實現了,效果可以參考Vben的高級詳情頁展示頁面。

1、表單字段訪問

一開始,嘗試采用Vben封裝的BasicForm,結果使用{{}}來訪問表單字段老是報錯,後來就直接用ant-design-vue的Form組件。

在最外層使用<Form>標簽包裹整個展現區域,使用model屬性綁定表單數據,在需要使用表單屬性的區域,再用<form-item>標簽包裹,包裹區域中直接訪問model綁定對象的屬性字段。

各種展現使用從ant-design-vue導入的Descriptions、Card、Steps等標簽。

當然,因為使用了直接綁定,所以原來代碼中useForm及相關代碼就需要刪除了,主要代碼展示如下:

//在/src/view/<模塊名>/<模塊名>Drawer.ts中
//...原有代碼
    <Form :model="currentOrder">
      <div class="pt-4 m-4 desc-wrap">
        <form-item>
          <Descriptions size="small" :column="3">
            <Descriptions.Item label="訂單編號">{{ currentOrder.orderNumber }}</Descriptions.Item>
//...原有代碼
<script lang="ts" setup>
//...原有代碼
  let currentOrder: Order = reactive({});
//...原有代碼
  /* const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
    labelWidth: 90,
    baseColProps: { span: 24 },
    schemas: formSchema,
    showActionButtonGroup: false,
  }); */
  const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
    // resetFields();
    setDrawerProps({ confirmLoading: false });
    isUpdate.value = !!data?.isUpdate;
    if (unref(isUpdate)) {
      //setFieldsValue({
      //  ...data.order,
      //});
      currentOrder = data.order;
    }
  });
//...原有代碼

2、嵌套子對象訪問

在詳情頁定制中,碰到了一個不大不小的問題,就是無法訪問嵌套子對象,百度了很多方法,但是都不生效,即使用form-item包裹後,無法直接訪問form-data.sub-data.item。

無奈之下,隻能繞過去,即單獨定義一個子對象的變量,在每次表單對象賦值時進行初始化,然後再進行訪問,核心代碼如下:

//在/src/view/<模塊名>/<模塊名>Drawer.ts中
//...原有代碼
        <div :model="currentOrderAddr">
          <Card title="訂單收貨信息" :bordered="false" class="mt-5">
            <Descriptions size="small" :column="3">
              <Descriptions.Item label="收貨人">{{ currentOrderAddr.receiver }}</Descriptions.Item>
              <Descriptions.Item label="手機">{{ currentOrderAddr.mobile }}</Descriptions.Item>
              <Descriptions.Item label="收貨地址">{{
                currentOrderAddr.province  
                currentOrderAddr.city  
                currentOrderAddr.area  
                currentOrderAddr.address
              }}</Descriptions.Item>
            </Descriptions>
          </Card>
        </div>
//...原有代碼
  const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
//...原有代碼
    if (unref(isUpdate)) {
      currentOrder = data.order;
      currentOrderAddr = data.order.currentOrderAddr;
    }
//...原有代碼