文章目录
说明:之前用Vue2+ElementUI|iView用的那是相当的爽,写法和之前做移动的用的Ionic很是类似,最近新开了一个项目,用的Vue3+TS+Antd那真是一把辛酸一把泪,完全摸索+找规律+瞎蒙,开发了一个模块之后终于有所得,以此篇文章记录。
1.效果图预览
老规矩先上效果图
列表
新增
附件上传
修改
详情
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代码就变的整洁了。