背景
我们的项目管理后台使用的UI组件库是Ant-Design-Vue3,移动端使用的UI组件库是Vant3,你可能比较好奇,为什么PC端和移动端使用了两套不同的UI组件库,开发与维护起来都不方便。因为我们的项目技术栈刚做过一次转型,从React换成了Vue的最新版本Vue3,我们也想用同一套UI组件库,无奈Ant-Design没有Vue版本的移动端组件库,所以移动端只能选择别的UI组件库,看到Vant的开发文档编排整齐,组件用法示例和API用法说明写得清晰明了,另外Vant在Vue生态圈也小有名气,果断选择了Vant组件库。
大多数用到UI组件库的应用场景,按照UI组件库的使用说明文档和用法演示,开发起来顺风顺水,轻松就能搞定。可是有些应用场景,实现起来就得动动脑筋,开发过程比较拧巴,有些磕绊。我记性不太好,解决过的问题,时间久了,就会遗忘。过了一段时间之后再次遇到同样的问题,又会被折磨一番。人都是趋利避害的,为了复用第一次解决问题的经验,避免被同样的问题反复折腾,我决定花点时间,把这些问题记录下来。
问题1 图片上传校验逻辑
Upload组件与Input,InputNumber,Select,TreeSelect,Radio,RadioGroup,Checkbox,DatePicker等组件的数据双向绑定属性不太一样,其它表单元素写法大多是v-model:value
,而Upload是v-model:fileList
,另外还有一个逻辑容易被遗忘,就是如果文件还在上传中的话,不能提交表单。
<template>
<div>
<Form :model="formData" @finishFailed="onFinishFailed" @finish="onFinish" layout="vertical">
<Form.Item name="fileList" label="上传图片" :rules="uploadRule" required>
<Upload
multiple
action="图片上传地址"
v-model:fileList="formData.fileList"
:accept="attachedContent?.fileTypes"
:maxCount="attachedContent?.maxFileSize"
@change="(files) => handleChangeUpload(files)"
/>
</Form.Item>
<Form.Item :wrapper-col="{ span: 12, offset: 6 }">
<Button type="primary" :loading="loading" html-type="submit">提交</Button>
</Form.Item>
</Form>
</div>
</template>
<script setup lang="ts">
import { Form,Upload } from 'ant-design-vue';
import { ref, reactive } from 'vue';
import type { Rule, RuleObject } from 'ant-design-vue/es/form';
const loading = ref(false);
const formData = reactive({
fileList: [],
});
const attachedContent = {
bizKey: 'attachedContent',
bizDesc: '附属内容',
fileTypes: 'jpg,png,jpeg',
maxFileSize: 2,
};
const onFinish = (values: any) => {
if (loading.value) {
return Promise.reject('图片正在上传中,请稍后提交');
}
console.log('Success:', values);
// ...
};
const onFinishFailed = (errorInfo: any) => {
console.log('Failed:', errorInfo);
};
// 处理文件状态变化
const handleChangeUpload = (fileList: any[]) => {
const fileLen=fileList.length;
const status = fileLen ? fileList[fileLen].status : '';
if (status === 'uploading') {
loading.value = true;
} else if (status === 'done') {
loading.value = false;
}
};
// 文件上传校验规则
const uploadRule: RuleObject[] = [
{
required: true,
validator: async (_rule: Rule, value: string) => {
if (value.length) {
return Promise.resolve();
}
return Promise.reject('请上传图片');
},
trigger: 'change',
},
];
</script>
如果对上传组件进行过封装的话,父组件获取子组件Upload的v-model:fileList
的值,变得有些不太方便,可以借助@vueuse/core
的useVModel
方法,实现父子组件传值的双向绑定, 要对父组件是否上传文件进行校验就变得很容易,可以沿用上面的校验逻辑。核心代码如下:
父组件:
<Form.Item name="picList" label="上传图片" :rules="uploadRule" required>
<BaseUploader
:biz-key="attachedContent?.bizKey"
:accept="attachedContent?.fileTypes"
:max-size="attachedContent?.maxFileSize"
:action="图片上传地址"
v-model:picList="formData.fileList"
@change="(files) => handleChangeUpload(files)"
/>
</Form.Item>
子组件:
<Upload
v-model:file-list="fileListModel"
:action="props.action || fileApi.uploadFileUrl"
:accept="acceptFileType"
:data="{ bizKey: props.bizKey }"
:max-count="props.maxCount"
:multiple="props.multiple"
:beforeUpload="beforeUpload"
:disabled="props.disabled"
:loading="loading"
:show-upload-list="false"
@change="handleChange"
>
import { useVModel } from '@vueuse/core'
const props = defineProps<{
picList: any[];
// ...
}>();
// 带有类型定义的写法
const emit = defineEmits<{
(e: 'update:picList', params: any[]): void;
}>();
const fileListModel = useVModel(props, 'picList', emit);
问题2 Vant表单组件纵向布局的实现
Vant表单组件只支持横向布局,不支持纵向布局,除了第一直觉本能反应,通过覆盖vant组件库样式的方式,将表单组件改成竖向布局。 还有一种改法,用拆解拼凑的思路,也能实现同样的效果。笔者推荐使用第二种改法, 你会发现,思路转变之后,定制额外的内容变得容易很多。
原有写法:
<van-field name="uploader" label="文件上传">
<template #input>
<van-uploader v-model="value" multiple :max-count="3" />
</template>
</van-field>
改造之后:
<div>
<div class="img-label"><span class="require">*</span> <span>图片</span></div>
<van-field name="uploader" label="文件上传">
<template #input>
<van-uploader v-model="value" multiple :max-count="3" />
</template>
</van-field>
<div class="num">
<span>{{ state.fileList.length }}</span>
<span>/3</span>
</div>
</div>
效果如下:
问题3 限制输入框只能输入特定字符
Vant的Field组件提供了formatter
属性,可以直接配置。
<template>
<Field
v-model="state.content"
type="textarea"
class="content"
placeholder="请填写跟进记录,最多允许200个字"
rows="8"
:maxlength="200"
show-word-limit
:disabled="props.disabled"
:formatter="contentFormat"
/>
</template>
<script setup lang="ts">
// 允许输入空格/中文/英文/数字,中英文标点符号
const contentFormat = (value: string) => {
value = value?.replace(/[^\u4E00-\u9FA5A-Za-z0-9\s,\.\?\!:\"\-\(\);,。?! : “ ” - ();]/g, '');
// 去除回车
value = value.replace(/\r\n/g, '');
value = value.replace(/\n/g, '');
// 去除表情
return filterEmoji(value);
};
</script>
Ant-Design-Vue 的Input组件无此属性,可以在Input组件的change事件回调函数中或者用watch监听实现输入内容格式化。
<template>
<Form.Item name="content" label="跟进记录">
<Textarea
v-model:value="inputText"
placeholder="请填写跟进记录,最多允许200个字"
show-count
:maxlength="200"
:rows="5"
@change="handleInputChange"
></Textarea>
</Form.Item>
</template>
<script setup lang="ts">
import { Form, Textarea } from 'ant-design-vue';
import { ref, watch } from 'vue';
const inputText = ref('');
// 方式一
const handleInputChange = (e) => {
inputText.value = contentFormat(e.target.value);
};
// 方式二
watch(inputText, (val) => {
inputText.value = contentFormat(val);
});
</script>
问题4 覆盖组件库样式
- 第一种方式是用:deep穿透子组件样式,这种方法有个细节要注意,就是:deep要有父级元素,用父级元素定位 :deep(子组件样式类名),修改子组件的样式才能生效。代码示例:
子组件:
<template>
<div class="deep">deep</div>
</template>
<style lang="less" scoped>
.deep {
color: red;
}
</style>
父组件不能覆盖子组件同样式名样式
<template>
<Child />
</template>
<script setup lang="ts">
import Child from './child.vue';
</script>
<style lang="less" scoped>
:deep(.deep) {
color: green;
}
</style>
父组件可以覆盖子组件同样式名样式
<template>
<div class="father"><Child /></div>
</template>
<script setup lang="ts">
import Child from './child.vue';
</script>
<style lang="less" scoped>
.father :deep(.deep) {
color: green;
}
</style>
- 第二种方式是定义全局样式进行覆盖,加一个本页面特有的样式类进行标识与选择。
<template>
<Table class="product-list" :dataSource="state.dataSource" :columns="columns" :pagination="false">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action' && record.status === 1">
<Button type="link" @click="download(record)"> 下载 </Button>
</template>
</template>
</Table>
</template>
<script setup lang="ts"></script>
<!-- 方式一 有scoped作用域限制,这里:deep(.ant-table-thead)会生效是因为.ant-table-thead这个类上层有父元素 -->
<style lang="less" scoped>
:deep(.ant-table-thead) > tr > th {
color: red;
}
</style>
<!-- 方式二 移除scoped,定义全局样式 -->
<style lang="less">
.product-list .ant-table-thead > tr > th {
color: red;
}
</style>
问题5 表格默认不显示总条数
遇到这个问题,你要是处于比较着急的状态下,没有耐心把Table组件pagination Api说明下方的跳转链接之后的英文文档看完的话,你可能愣是半天,不知道怎么配置。
代码示例:
<Table
:columns="columns"
:rowKey="rowKey"
:data-source="tableData"
:loading="loading"
:pagination="{
showTotal: (total) => `共 ${total} 条记录`,
showSizeChanger: true,
current,
pageSize,
total,
}"
@change="handleTableChange"
>
<template #[item]="data" v-for="item in Object.keys($slots)" :key="item">
<slot :name="item" v-bind="data"></slot>
</template>
</Table>
结语
如果大家在使用的过程中也遇到过想着觉得没问题,实现起来却有些磕绊的的功能,欢迎在评论区分享探讨,我会将你们分享的案例,收录到本文中。让走过路过看过的掘友们一起收益。