使用UI组件库时比较拧巴的问题整理

2,104 阅读4分钟

背景

我们的项目管理后台使用的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/coreuseVModel方法,实现父子组件传值的双向绑定, 要对父组件是否上传文件进行校验就变得很容易,可以沿用上面的校验逻辑。核心代码如下:

父组件:

        <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组件库样式的方式,将表单组件改成竖向布局。 还有一种改法,用拆解拼凑的思路,也能实现同样的效果。笔者推荐使用第二种改法, 你会发现,思路转变之后,定制额外的内容变得容易很多。 image.png

原有写法:

    <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>&nbsp;<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>

效果如下:

image.png

问题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说明下方的跳转链接之后的英文文档看完的话,你可能愣是半天,不知道怎么配置。 image.png

image.png

image.png 代码示例:

  <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>

结语

如果大家在使用的过程中也遇到过想着觉得没问题,实现起来却有些磕绊的的功能,欢迎在评论区分享探讨,我会将你们分享的案例,收录到本文中。让走过路过看过的掘友们一起收益。