Vue实战十一:Vue3+TS+Antd实现vben项目的增删改查

文章目录


说明:之前用Vue2+ElementUI|iView用的那是相当的爽,写法和之前做移动的用的Ionic很是类似,最近新开了一个项目,用的Vue3+TS+Antd那真是一把辛酸一把泪,完全摸索+找规律+瞎蒙,开发了一个模块之后终于有所得,以此篇文章记录。

1.效果图预览

老规矩先上效果图

列表
Vue实战十一:Vue3+TS+Antd实现vben项目的增删改查

新增
Vue实战十一:Vue3+TS+Antd实现vben项目的增删改查

附件上传
Vue实战十一:Vue3+TS+Antd实现vben项目的增删改查

修改
Vue实战十一:Vue3+TS+Antd实现vben项目的增删改查

详情
Vue实战十一:Vue3+TS+Antd实现vben项目的增删改查

2.列表的实现

index.vue页面中

template布局如下,BasicTable列表页及操作列中定义按钮,DetailModal详情界面,InterfaceModal编辑及新增界面

<template>
  <PageWrapper dense
               contentFullHeight
               fixedHeight
               contentClass="flex">
    <DetailModal :info="rowInfo"
                 @register="registerDetailModal" />
    <BasicTable @register="registerTable"
                :searchInfo="searchInfo">
      <template #toolbar>
        <a-button type="primary"
                  @click="handleCreate">接口添加</a-button>
      </template>
      <template #action="{ record }">
        <TableAction :actions="[
            {
              icon: 'clarity:note-edit-line',
              tooltip: '编辑',
              onClick: handleEdit.bind(null, record),
            },
            {
              icon: 'mdi:arrow-down-bold-circle-outline',
              tooltip: '下载',
              onClick: handleDownload.bind(null, record),
              ifShow: () => {
                return record.interfaceDoc && record.docName;
              },
            },
             {
              icon: 'icon-park-outline:view-grid-detail',
              color:'success',
              tooltip: '详情',
              onClick: handleSetDetail.bind(null, record),
            },
             {
              icon: 'ant-design:delete-outlined',
              color: 'error',
              popConfirm: {
                title: '是否确认删除',
                confirm: handleDelete.bind(null, record),
              },
            },

          ]" />
      </template>
    </BasicTable>
    <InterfaceModal @register="registerModal"
               @success="handleSuccess" />

  </PageWrapper>
</template>

script中的逻辑实现

<script lang="ts">
import { defineComponent, ref, reactive } from 'vue';
import { BasicTable, useTable, TableAction } from '/@/components/Table';
import { set, del, page, pageCommon } from '/@/api/interface/list';
import { PageWrapper } from '/@/components/Page';
import { SvgIcon } from '/@/components/Icon';
import { useModal } from '/@/components/Modal';
import InterfaceModal from './InterfaceModal.vue';
import DetailModal from './DetailModal.vue';
import { columns, searchFormSchema } from './list.data';
import { useGo } from '/@/hooks/web/usePage';
import { useMessage } from '/@/hooks/web/useMessage';
import { downloadByUrl, downloadByData } from '/@/utils/file/download';
import { Alert } from 'ant-design-vue';
import { useUserStore } from '/@/store/modules/user';
import { InterFaceModel } from '/@/api/interface/model/interfaceModel';
const { createMessage } = useMessage();

const rowInfo = ref<Recordable>();

export default defineComponent({
  name: 'UserManagement',
  components: {
    BasicTable,
    PageWrapper,
    InterfaceModal,
    TableAction,
    DetailModal,
    [Alert.name]: Alert,
    SvgIcon,
  },
  setup() {
    const go = useGo();
    const [registerModal, { openModal }] = useModal();
    const [registerDetailModal, { openModal: openDetailModal }] = useModal();
    const searchInfo = reactive<Recordable>({});
    const [registerTable, { reload, updateTableDataRecord }] = useTable({
      title: '接口列表',
      api: page,
      rowKey: 'id',
      columns,
      formConfig: {
        labelWidth: 120,
        schemas: searchFormSchema,
        autoSubmitOnEnter: true,
      },
      useSearchForm: true,
      showTableSetting: true,
      bordered: true,
      handleSearchInfoFn(info) {
        console.log('handleSearchInfoFn', info);
        return info;
      },
      actionColumn: {
        width: 150,
        title: '操作',
        dataIndex: 'action',
        slots: { customRender: 'action' },
      },
    });

    const userStore = useUserStore();
    const { userId } = userStore.getUserInfo;

    function handleCreate() {
      openModal(true, {
        isUpdate: false,
      });
    }

    function handleEdit(record: Recordable) {
      openModal(true, {
        record,
        isUpdate: true,
      });
    }

    function handleDownload(record: Recordable) {
      let url =
        '/api/tzwy-component/attachment/download?fileName=' +
        record.interfaceDoc +
        '&docName=' +
        record.docName;
      handleDownloadByUrl(url);
    }

    async function handleDelete(record: Recordable) {
      await del({ id: record.id, isDel: '1', delBy: userId + '' });
      createMessage.success('删除成功!');
      handleSuccess();
    }

    function handleSuccess() {
      reload();
    }

    function handleSelect(departId = '') {
      searchInfo.departId = departId;
      reload();
    }

    function handleSetDetail(record: InterFaceModel) {
      rowInfo.value = record;
      openDetailModal(true, {
        info: record,
        isUpdate: true,
      });
    }

    function handleDetailSuccess() {
      reload();
    }

    function handleDownloadByUrl(urlValue) {
      downloadByUrl({
        url: urlValue,
        target: '_self',
      });
    }

    return {
      registerTable,
      registerModal,
      handleCreate,
      handleEdit,
      handleDelete,
      handleSuccess,
      handleSelect,
      handleView,
      handleAuth,
      searchInfo,
      handleSetDetail,
      registerDetailModal,
      handleDetailSuccess,
      handleDownloadByUrl,
      handleOpen,
      userId,
    };
  },
});
</script>

说明:新增调用handleCreate,编辑调用handleEdit,详情调用handleSetDetail,下载调用handleDownload,删除调用handleDelete

列表中显示字段定义list.data.ts文件中,在状态列中定义onChange方法实现接口开关的开启及关闭功能

export const columns: BasicColumn[] = [
    {
        title: '接口名称',
        dataIndex: 'xx1',
        width: 100,
    },
    {
        title: '接口编码',
        dataIndex: 'xx2',
        width: 100,
    },
    {
        title: '接口地址',
        dataIndex: 'xx3',
        width: 150,
    },
   ......
    {
        title: '状态',
        dataIndex: 'enabled',
        width: 120,
        customRender: ({ record }) => {
          if (!Reflect.has(record, 'pendingStatus')) {
            record.pendingStatus = false;
          }
          return h(Switch, {
            checked: record.enabled === '1',
            checkedChildren: '启用',
            unCheckedChildren: '禁用',
            loading: record.pendingStatus,
            onChange(checked: boolean) {
              record.pendingStatus = true;
              const newStatus = checked ? '1' : '0';
              const { createMessage } = useMessage();
              set({id:record.id, enabled:newStatus})
                .then(() => {
                  record.enabled = newStatus;
                  if(newStatus==='1'){
                    createMessage.success(`启用成功`);
                  }else{
                    createMessage.success(`禁用成功`);
                  }
                })
                .catch(() => {
                  createMessage.error('操作失败');
                })
                .finally(() => {
                  record.pendingStatus = false;
                });
            },
          });
        },
      },

];

在list.data.ts中定义查询接口参数

export const searchFormSchema: FormSchema[] = [
    {
        field: 'xx1',
        label: '接口名称',
        component: 'Input',
        colProps: { span: 6 },
    },
    {
        field: 'xx2',
        label: '接口描述',
        component: 'Input',
        colProps: { span: 6 },
    },
    {
        field: 'startDate',
        label: '起始时间',
        component: 'DatePicker',
        colProps: { span: 6 },
    },
    {
        field: 'endDate',
        label: '截止时间',
        component: 'DatePicker',
        colProps: { span: 6 },
    },
];

在api中定义模块文件,在模块文件下新建list.ts,在list.ts中定义接口

import {  Entity,EntityVO, EntityDTO } from './model/interfaceModel';
import { defHttp } from '/@/utils/http/axios';

enum Api {
  Page = '/xx/xx/page',
  PageCommon = '/xx/xx/openListPage',
  Set = '/xx/xx/save',
  Del = '/xx/xx/delById',
}

// 列表
export const page = (params?: EntityVO) => defHttp.get<EntityDTO>({ url: Api.Page, params });

// 列表
export const pageCommon = (params?: EntityVO) => defHttp.get<EntityDTO>({ url: Api.PageCommon, params });

// 保存
export const set = (params: Entity) => defHttp.post<Entity>({ url: Api.Set, params });

// 删除
export const del = (params: Entity) => defHttp.post<Entity>({ url: Api.Del, params });

在当前模块下新建model文件夹在interfaceModel中定义实现类、查询参数及响应模型

// 引入基础包
import { BasicPageParams, BasicFetchResult } from '/@/api/model/baseModel';

// 定义查询参数
export type EntityVO = BasicPageParams & {
  startDate?: string;
  endDate?: string;
};

// 定义对象
export interface Entity {
    xx1: string;
    xx2: number;
    xx3: string;
   ......
}

// 生成响应模型
export type EntityDTO = BasicFetchResult<EntityVO>;

至此,一个列表请求就做好了

3.数据新增、修改及附件上传的实现

数据新增及修改页面InterfaceModal.vue,在当前页面下实现template及script,

template代码如下

<template>
  <BasicModal v-bind="$attrs"
              @register="registerModal"
              :title="getTitle"
              @ok="handleSubmit">
    <div class="m-8">
      <BasicUpload :maxSize="20"
                   :maxNumber="1"
                   :accept="['doc','docx','rar','zip']"
                   @change="handleChange"
                   :api="uploadApi"
                   :showPreviewNumber="true" />
    </div>

    <BasicForm @register="registerForm" />
  </BasicModal>
</template>

script逻辑如下

<script lang="ts">
import { defineComponent, ref, computed, unref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { formSchema } from './list.data';
import { departList } from '/@/api/system/depart';
import { set } from '/@/api/模块名/list';
import { uploadApi } from '/@/api/sys/upload';
import { BasicUpload } from '/@/components/Upload';
import { PageWrapper } from '/@/components/Page';
import { Alert } from 'ant-design-vue';

export default defineComponent({
  name: 'InterfaceModal',
  components: { BasicModal, BasicForm, BasicUpload, PageWrapper, [Alert.name]: Alert },
  emits: ['success', 'register'],
  setup(_, { emit }) {
    const isUpdate = ref(true);
    const rowId = ref('');
    let fileNameValue: string;
    let bucketNameValue: string;
    const [registerForm, { setFieldsValue, updateSchema, resetFields, validate }] = useForm({
      labelWidth: 100,
      schemas: formSchema,
      showActionButtonGroup: false,
      actionColOptions: {
        span: 23,
      },
    });
    const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
      resetFields();
      setModalProps({ confirmLoading: false });
      isUpdate.value = !!data?.isUpdate;
      if (unref(isUpdate)) {
        rowId.value = data.record.id;
        setFieldsValue({
          ...data.record,
        });
      }
      const treeData = await departList();
      updateSchema([
        {
          field: 'xx',
          componentProps: { treeData },
        },
      ]);
    });

    const getTitle = computed(() => (!unref(isUpdate) ? '新增接口' : '编辑接口'));
    async function handleSubmit() {
      try {
        const values = await validate();
        setModalProps({ confirmLoading: true });
        values.docName = fileNameValue;
        values.interfaceDoc = bucketNameValue;
        if (!!unref(isUpdate)) {
          values.id = rowId.value;
        }
        await set(values);
        closeModal();
        emit('success', { isUpdate: unref(isUpdate), values: { ...values, id: rowId.value } });
      } finally {
        setModalProps({ confirmLoading: false });
      }
    }
    return {
      registerModal,
      registerForm,
      getTitle,
      handleSubmit,
      handleChange: (list) => {
        fileNameValue = list[0]?.fileName;
        bucketNameValue = list[0]?.bucketName;
        // createMessage.info(`已上传文件${JSON.stringify(list)}`);
      },
      uploadApi,
    };
  },
});
</script>

新增及修改页面按钮提交调用handleSubmit,根据isUpdate的值分别调用新增及修改接口,当前项目中新增与修改的区别是是否有id,修改时给表单的id赋值即可,最终调用await set(values);实现数据新增及修改。set方法是引用自import { set } from ‘/@/api/模块名/list’;

新增及修改字段配置import { formSchema } from ‘./list.data’;具体如下

export const formSchema: FormSchema[] = [
    {
        field: 'xx1',
        label: '接口名称',
        component: 'Input',

    },
    {
        field: 'xx2',
        label: '接口编码',
        component: 'Input',
    },
   

    {
        field: 'xx3',
        label: '所属单位',
        component: 'TreeSelect',
        componentProps: {
            replaceFields: {
                title: 'name',
                key: 'id',
                value: 'id',
            },
            getPopupContainer: () => document.body,
        },
    },
    {
        field: 'xx4',
        label: '请求方式',
        component: 'RadioButtonGroup',
        defaultValue: 'GET',
        componentProps: {
            options: [
                { label: 'GET', value: 'GET' },
                { label: 'POST', value: 'POST' },
            ],
        },
    },
    {
        field: 'xx5',
        label: '接口描述',
        component: 'InputTextArea',
    },
   ......
    {
        field: 'xx6',
        label: '参数类型',
        component: 'RadioButtonGroup',
        defaultValue: 'Json',
        helpMessage:['Json:{"interfaceName":"xxx","interfaceCode":"xxx"}','form-data:xxx...........','raw:xxx.................'],
        componentProps: {
            options: [
                { label: 'Json', value: 'Json' },
                { label: 'form-data', value: 'form-data' },
                { label: 'raw', value: 'raw' },
            ],
        },
    },
];

附件上传的实现

 <BasicUpload :maxSize="20"
                   :maxNumber="1"
                   :accept="['doc','docx','rar','zip']"
                   @change="handleChange"
                   :api="uploadApi"
                   :showPreviewNumber="true" />

参数maxNumber限制上传的最大数量,accept限制上传的附件类型,change方法是上传成功后并点击页面的保存获取到上传附件的信息数据,api是调用的上传接口,注意是通过调用/upload对应的值来实现的,可以在配置文件.env.development中修改/upload中的值来实现。

附件上传源代码修改路径/components/Upload/UploadModal.vue中存有保存附件的方法如下

   //   点击保存
    function handleOk() {
      const { maxNumber } = props;
      if (fileListRef.value.length > maxNumber) {
        return createMessage.warning(t('component.upload.maxNumber', [maxNumber]));
      }
      if (isUploadingRef.value) {
        return createMessage.warning(t('component.upload.saveWarn'));
      }
      let fileList: Array<{ bucketName: string|undefined , fileName: string|undefined, url: string|undefined}>=[];

      for (const item of fileListRef.value) {
        const { status, responseData } = item;
        if (status === UploadResultStatus.SUCCESS && responseData) {
          let obj = responseData?.data;
          let oneObj={
              bucketName:obj?.bucketName,
              fileName:obj?.fileName,
              url:obj?.url
          }
          fileList.push(oneObj);
        }
      }
      // 存在一个上传成功的即可保存
      if (fileList.length <= 0) {
        return createMessage.warning(t('component.upload.saveError'));
      }
      fileListRef.value = [];
      closeModal();
      emit('change', fileList);
    }

我们修改fileList中的值改为自己想要获取的值即可将fileList数组返回到handleChange中并接收,这里要熟悉下ts语法,要不可能变量都不会定义。

4.详情界面的实现

怀念vue2+ElementUI时详情界面的跳转在我弄了一天没搞定详情界面数据的展示时,这里涉及到Vue3的使用及TS语法,通过瞎蒙+找规律找到了setup这个关键方法,官方解释说setup在beforeCreate之前执行一次并且方法中无法使用this,vue2中用的最爽的this在vue3中没了…

详情界面template实现如下

<template>
  <BasicModal v-bind="$attrs"
              :width="1000"
              @register="registerModal"
              :title="getTitle">
    <Description @register="register" />
  </BasicModal>
</template>

script实现如下

<script lang="ts">
import { defineComponent, ref, computed, unref, reactive } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { uploadApi } from '/@/api/sys/upload';
import { BasicUpload } from '/@/components/Upload';
import { PageWrapper } from '/@/components/Page';
import { Alert } from 'ant-design-vue';
import {
  Description,
  DescItem,
  useDescription,
  UseDescReturnType,
} from '/@/components/Description/index';
import { detailFormSchema } from './list.data';
import { getDescSchema } from './data';

export default defineComponent({
  name: 'DetailModal',
  components: { Description, BasicModal, BasicForm,  PageWrapper },
  emits: ['success', 'register'],
  setup(_, { emit }) {
    const isUpdate = ref(true);
    const rowId = ref('');
    const formData = ref({});
    const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
      formData.value = await data?.info;
    });

    const [register] = useDescription({
      data: formData,
      column: 1,
      schema: detailFormSchema,
    });

    const getTitle = computed(() => ('接口详情'));
    async function handleSubmit() {
      try {
        setModalProps({ confirmLoading: true });
        closeModal();
        emit('success');
      } finally {
        setModalProps({ confirmLoading: false });
      }
    }
    return {
      registerModal,
      register,
      getTitle,
      handleSubmit,
      formData,
    };
  },
});
</script>

说明:由于对ts不太熟,刚开始初始化时给let formData:any;导致数据值无法加载,后来了解了ref和reactive的用法,觉得formData既然是对象应该用reactive,然后在formData中定义了一堆属性然后在给formData属性赋值时,只能一个字段一个字段赋值,最终用了ref代码就变的整洁了。

上一篇:如何使用注解优雅的记录操作日志


下一篇:C# 10 新功能总结