ant-design-vue 快速避坑指南

ant-design-vue 是蚂蚁金服 Ant Design 官方唯一推荐的 Vue 版 UI 组件库,它其实是 Ant Design 的 Vue 实现,组件的风格与 Ant Design 保持同步,组件的 html 结构和 css 样式也保持一致。 用下来发现它的确称得上为数不多的完整的 VUE 组件库与开发方案集成项目。
本文主要目的是总结一些开发过程中比较耗时间去查找,文档中没有具体说明的常见问题,同时希望能给新上手此框架的同学提供一些参考作用。
 

1.Table 对接后台返回数据

针对 Table 数据格式与后他接口返回数据格式不一致问题,修改 `@/components/table/index.js` 132 行起
主要修改 pageNo,pageSize,totalCount,data 这字段与后台返回字段一致就 OK 了
result.then(r => {
  this.localPagination = Object.assign({}, this.localPagination, {
    current: r.pageNo,  // 这里修改当前分页字段
    total: r.totalCount, // 这里修改总记录数字段
    showSizeChanger: this.showSizeChanger,
    pageSize: (pagination && pagination.pageSize) ||
      this.localPagination.pageSize // 这里修改总记录数当前页数字段
  })
    //r.data 中的 data 修改为返回列表字段
  if (r.data.length == 0 && this.localPagination.current != 1) {
    this.localPagination.current--
    this.loadData()
    return
  }
  !r.totalCount && ['auto', false].includes(this.showPagination) && (this.localPagination = false)
  this.localDataSource = r.data // 返回结果中的数组数据
  this.localLoading = false
});

2.table 操作栏参数问题

在 table 的 dataSource 中指定的每一个数据中,都必须包含有 name 为 key 的对象,而显示出的数据就是相应 key 对应的数据,dataIndex 就用来声明列数据在数据项中对应的 key
然而在操作列中,我们一般需要传入不知一项数据, 试了一下如下图配置 dataIndex,数据并不能正确传入 slot-scope 中
columns: [
    ...
    {
            title: '操作',
            dataIndex: 'id,text',
            key: 'id',
            scopedSlots: { customRender: 'operation' }
    }
】
多尝试后发现,其实只要不配置 dataIndex 就好了。。。slot-scope 自定义一个字段,自然就拿到了整行数据

3.table 分页组件展示条数

:pagination="{showTotal: total => ` 共 ${total} 条 `}"

4. 神奇的最后一个标签隐藏问题

使用可编辑 tags 过程中值得注意的问题,一般删除某个 tag 不止是从 DOM 中删除这个 tag,而是需要调接口修改数据,那么这时候如果选择用修改完的数据动态渲染 tag 列表,并且理所当然地认为动态绑定数据就不需要关心数据手动处理,问题就出现了:
假如一共有 5 个 tag,现在删除第一个 tag,并调用接口返回新数据,注意 tags 默认的删除操作也不是从 DOM 中删除这个 tag,而是将这个 tag 设置为 ```display:none```! 这就导致了一个很神奇的问题,此时新返回的 tags 数组长度已经 -1,而它仍然认为当前列表的第一个 Tag 是隐藏的,最后呈现的效果就是只剩 3 个 Tag,此时再接着删除第一个 tag(其实是第二个),那么就只剩 1 个 tag 了。。
<a-tag
  v-for="(tag, index) in Tags"
    :key="tag.id"
    :closable="tagCloseable"
    :afterClose="()=> handleTagStatus(0,tag)"
    >{{tag.name}}
</a-tag>
这个问题貌似没什么好的办法,只能放弃绑定动态数据,判断接口调用成功后,再用文档中的手动操作增减数据的办法:
this.Tags = this.Tags.filter(tag => tag.id !== removeTag.id)

5. 表单的各种常规操作

单独触发某个字段的校验:
this.form.validateFields(['name'], { force: true }) 
清除某个字段的值:
this.form.resetFields(`name`,'')
设置表单初始值:
this.form.resetFields(`name`,'')
注意:不初始化的值用 undefined 而非‘’, 否则下拉框会不显示 placeholder!
 
自定义文件上传的 action 函数:
<a-upload :customRequest="upLoad"></a-upload>
upLoad (info) {
    let file = info.file;
    let param = new FormData(); //创建 form 对象
    param.append('file',file);//通过 append 向 form 对象添加数据
    console.log(param.get('file')); //FormData 私有类对象访问不到,可以通过 get 判断值是否传进去
    let config = {
        headers:{'Content-Type':'multipart/form-data'}
    }; 
    this.$http.post(url, param, config).then(res => {...})
},

6. 接口跨域携带 cookie 问题

做单点登录时需要在请求头中携带 cookie,遇到了很坑人的问题,实际原因是对 mock.js 的实现不够了解。
还是在 `@/src/utils/request.js`,这里创建了 axios 实例供全局调用,根据 axios 文档,** 在创建 ** axios 实例时添加:`withCredentials: true`
const service = axios.create({baseURL: `${process.env.VUE_APP_BASEURL}/backend`,
    withCredentials: true,
  timeout: 6000 
})
结果发现接口请求仍然不带 cookie,无奈试了一下用 fetch 请求 `fetch(url, { credentials: 'include', mode: 'cors'})`,发现可以携带 cookie,百思不得其解,两者都是基于 promise 实现的,但是 fetch 在写法和拦截请求响应等方面都比较麻烦,全部替换成 fetch 也不太现实。最后才发现,是 mock.js 没有注释(`main.js` 中注释掉就好了),原来 mock.js 是通过拦截 XHR 请求来实现的接口模拟,Axios 本质上也是对原生 XHR 的封装,只不过它是 Promise 的实现版本,所以它当然被拦截了,而 fetch 脱离了 XHR,这也是 fetch 请求能正常携带 cookie 的原因,这里还没有全部梳理清楚,打算在后一篇中详细介绍一下
7. 单点登录的实现
全局的路由钩子在 `permission.js` 中,一般单点登录、权限验证都是在这里处理,这里也不例外。没什么特别的,需要注意的一点就是,不要忘记对页面其他接口的统一无权限处理,和 403 请求的响应处理。同时画个流程图会更快一些, 这里就记录一下吧:
流程图:
路由钩子的处理:
router.beforeEach((to, from, next) => {
  // 对 403 无权限的处理
  if (to.path === '/403') {next()
  } else {
    if (roles) {//已登陆
      next() } else {
            //获取用户信息,GetUserInfo 逻辑如下:
            //status=403 && reject(res),返回包含 status;
            //status=1005 && reject(res.data) 返回重定向的 URL;
            //status=1000 && resolve()
      store
        .dispatch('GetUserInfo') 
        .then(res => {next()
        })
        .catch((e) => {
          if (e.status) {
            next({ path: '/403' }) } else {
                        //拼接 URL 跳去登陆页,登陆成功会重定向回当前页(login_redirect)
            const url = e.substring(0, e.lastIndexOf('redirect')) + 'redirect=' + login_redirect
            window.location.href = url
          }
        })}}
})
`@/ src/utils/request.js` 中接口返回的统一处理:
service.interceptors.response.use((response) => {
    if (response.data.status === 1005){
            //... 同上跳去登陆页
    }else{
        //为返回数据做统一处理
        return response.data
    }
}, err) 

7. 引入 eCharts

1)npm install
2) components 下新建 barChart.vue ,import echarts from 'echarts',正常操作...
3) resize 触发图表自适应
echart 有 resizeAPI,一般是在图表组件如 barChart.vue 里面手动监听窗口 resize
mounted() {
    window.addEventListener("resize", () => {
        this.chart.resize();});
},
后面借鉴 element-admin, 利用 mixins 实现了更完善的统一处理方法:
1)定义一个 mixin:resize.js
import {debounce} from '@/utils'//防抖函数
export default {data() {
    return {
      $_sidebarElm: null
    }
  },
  mounted() {
    this.__resizeHandler = debounce(() => {
      if (this.chart) {
        this.chart.resize()}}, 100)
    window.addEventListener('resize', this.__resizeHandler)
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0<span style="color: rgba(0, 0, 0, 1)">]
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.$_sidebarElm &amp;&amp; <span style="color: rgba(0, 0, 255, 1)">this</span>.$_sidebarElm.addEventListener('transitionend', <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.$_sidebarResizeHandler)

},
beforeDestroy() {
window.removeEventListener('resize', this.__resizeHandler)

</span><span style="color: rgba(0, 0, 255, 1)">this</span>.$_sidebarElm &amp;&amp; <span style="color: rgba(0, 0, 255, 1)">this</span>.$_sidebarElm.removeEventListener('transitionend', <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.$_sidebarResizeHandler)

},
methods: {
$_sidebarResizeHandler(e) {
if (e.propertyName === 'width') {
this.__resizeHandler()
}
}
}
}

2)@/components/_utils/util.js 中添加防抖函数

export const debounce = (func, wait, immediate) => {
  let timeout, args, context, timestamp, result

const later = function() {
// 据上一次触发时间间隔
const last = +new Date() - timestamp

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait</span>
<span style="color: rgba(0, 0, 255, 1)">if</span> (last &lt; wait &amp;&amp; last &gt; 0<span style="color: rgba(0, 0, 0, 1)">) {
  timeout </span>= setTimeout(later, wait -<span style="color: rgba(0, 0, 0, 1)"> last)
} </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
  timeout </span>= <span style="color: rgba(0, 0, 255, 1)">null</span>
  <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用</span>
  <span style="color: rgba(0, 0, 255, 1)">if</span> (!<span style="color: rgba(0, 0, 0, 1)">immediate) {
    result </span>=<span style="color: rgba(0, 0, 0, 1)"> func.apply(context, args)
    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (!timeout) context = args = <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">
  }
}

}

return function(...args) {
context
= this
timestamp
= +new Date()
const callNow
= immediate && !timeout
// 如果延时不存在,重新设定延时
if (!timeout) timeout = setTimeout(later, wait)
if (callNow) {
result
= func.apply(context, args)
context
= args = null
}

</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> result

}
}

3)resize 监听方法混入图表组件即可

mixins: [resize]