-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcontent.json
1 lines (1 loc) · 45.7 KB
/
content.json
1
{"meta":{"title":"hzyhbk","subtitle":null,"description":null,"author":null,"url":"http://hzyhbk.github.io","root":"/"},"pages":[{"title":"404 Not Found","date":"2020-08-17T03:19:09.416Z","updated":"2020-08-17T03:19:09.416Z","comments":true,"path":"404.html","permalink":"http://hzyhbk.github.io/404.html","excerpt":"","text":"404 Not Found 很抱歉,您访问的页面不存在可能是输入地址有误或该地址已被删除"},{"title":"关于我","date":"2020-09-26T08:23:24.743Z","updated":"2020-09-26T08:23:24.743Z","comments":true,"path":"about/index.html","permalink":"http://hzyhbk.github.io/about/index.html","excerpt":"","text":"介绍自己 我叫慌张 杭州电子科技大学-软件工程专业-2019级毕业生 杭州大搜车-无线基础技术部-高级无线开发工程师 关于主题本站主题是:Material XMac用户可以下载客户端哦: MaterialX-Installer.dmg"},{"title":"tags","date":"2019-12-28T07:14:53.000Z","updated":"2020-08-17T03:19:11.802Z","comments":true,"path":"archives/index.html","permalink":"http://hzyhbk.github.io/archives/index.html","excerpt":"","text":""},{"title":"tags","date":"2019-12-28T07:14:53.000Z","updated":"2020-08-17T03:19:11.814Z","comments":true,"path":"tags/index.html","permalink":"http://hzyhbk.github.io/tags/index.html","excerpt":"","text":""},{"title":"categories","date":"2019-12-28T07:14:03.000Z","updated":"2020-08-17T03:19:11.805Z","comments":true,"path":"categories/index.html","permalink":"http://hzyhbk.github.io/categories/index.html","excerpt":"","text":""}],"posts":[{"title":"使用函数优雅地创建弹框抽屉","slug":"start/vue-create-dm","date":"2020-09-26T08:00:31.000Z","updated":"2020-09-26T08:20:10.085Z","comments":true,"path":"start/vue-create-dm/","link":"","permalink":"http://hzyhbk.github.io/start/vue-create-dm/","excerpt":"使用函数优雅地创建 ant-design-vue、view-design、ElementUI 的 Drawer 和 Modal 例子vue-create-dm使用例子在线地址 代码见 example 文件夹","text":"使用函数优雅地创建 ant-design-vue、view-design、ElementUI 的 Drawer 和 Modal 例子vue-create-dm使用例子在线地址 代码见 example 文件夹 特性 通过函数来创建Modal或Drawer组件 Modal、Drawer的内容子组件的created、mounted、destoryed生命周期按照正常逻辑触发 Modal、Drawer支持分别注册全局头部组件(需要接收名为 title 的 props) 支持传入 title、content、footer 插槽 支持Modal、Drawer与父组件通信 支持子组件获取 this.$store 和 this.$router 支持传入路由来匹配内容组件, 若传入url比如 https://www.baidu.com ,则以 iframe形式展示 若传入 相对路由(比如 /foo, /bar),则获取匹配的路由组件展示 支持微前端中跨项目相互调用 为什么在使用弹窗抽屉组件的过程中,你是否也曾遇到过以下场景: 一个项目里有许多的弹窗和抽屉类型的交互,有时甚至一个页面组件里就有许多弹窗和抽屉组件,原生的使用方式是先在父组件中写好弹框抽屉组件,然后通过visible变量来控制弹窗的显示隐藏,当弹窗抽屉一多,看着各种xxVisible让人感觉很混乱 弹窗抽屉内包含的子组件的生命周期并没有按我们预想的逻辑触发,我们想打开弹窗抽屉的时候才触发内容子组件的created和mounted生命周期,然而实际上却并不是;我们希望关闭的时候可以调用子组件的destoryed生命周期,可是目前的UI框架大多只是把组件设置为display:none了,并没有完全卸载子组件, antd提供了destroyOnClose参数支持关闭时销毁子元素,但也没法解决上面说到的1,2两点问题组件库虽然也有提供通过函数打开弹窗的方法,但那些都是一些简单的弹框,可配置的参数不多,自由度也不够高 因此就有了vue-create-dm这个库,dm就是分别取了Drawer和Modal的第一个字母组合在一起(为什么不是md呢,因为md是markdown的缩写…)目前内置支持了ant-design-vue、view-design和ElementUI三个组件库的的弹框抽屉组件,并且提供了各种工具函数可以自己支持其他组件库的弹框抽屉组件 安装1yarn add vue-create-dm 统一注册注意1如果要在子组件内获取 this.$store 和 this.$router 请把 VueCreateDM 的注册放到 Vuex 和 VueRouter 实例生成之后,并且传入这两个实例 注意2如果要自定义全局头部组件,请传入modalGlobalHeader,drawerGlobalHeader这两个参数,分别对应Modal组件的全局头部和Drawer组件的全局头部 下面演示如何进行全量注册:12345678910111213141516171819202122import Vue from 'vue';import VueCreateDM from 'vue-create-dm';import { Modal as antdModal, Drawer as antdDrawer } from 'ant-design-vue';import { Modal as viewModal, Drawer as viewDrawer } from 'view-design';import { Dialog as eleModal, Drawer as eleDrawer } from 'element-ui';import store from './store'import router from './router';import modalGlobalHeader from './components/modalGlobalHeader';import drawerGlobalHeader from './components/drawerGlobalHeader';Vue.use(VueCreateDM, { antdModal, antdDrawer, viewModal, viewDrawer, eleModal, eleDrawer, store, router, modalGlobalHeader, drawerGlobalHeader,}); 单个注册注意1如果要在子组件内获取 this.$store 和 this.$router 请把 VueCreateDM 的注册放到 Vuex 和 VueRouter 实例生成之后,并且传入这两个实例 注意2如果要自定义全局头部组件,请传入globalHeader参数 下面演示如何单个注册,其中component属性必传,其余几个都是可选参数:12345678910111213import Vue from 'vue';import store from './store';import router from './router';import { createAntdDrawer } from 'vue-create-dm';import { Drawer } from 'ant-design-vue';import globalHeader from '../components/globalHeader';Vue.use(createAntdDrawer, { component: Drawer, router, // 子组件需要用到 this.$router 就传 store // 子组件需要用到 this.$store 就传 globalHeader, // 全局配置头部组件}); 使用创建抽屉123this.$createAntdDrawer(options, arg1, arg2);this.$createViewDrawer(options, arg1, arg2);this.$createEleDrawer(options, arg1, arg2); 代码示例12345678910111213141516171819this.$createAntdDrawer({ drawerProps: { title: '标题', width: '500px', mask: false, }, content: { template: HelloWorld, props: { msg: 'Welcome to Your Vue.js App', }, }, beforeClose: function() { console.log('我要关闭了'); }, afterClose: function() { console.log('我已经关闭了'); },}); 创建弹框123this.$createAntdModal(options, arg1, arg2);this.$createViewModal(options, arg1, arg2);this.$createEleModal(options, arg1, arg2); 代码示例12345678910111213141516171819202122232425262728this.$createAntdModal({ modalProps: { title: '标题', width: '500px', mask: false, }, content: { template: HelloWorld, props: { msg: 'Welcome to Your Vue.js App', }, }, beforeClose: function() { console.log('我要关闭了'); }, afterClose: function() { console.log('我已经关闭了'); }, async onOk() { const res = await new Promise((resolve) => { setTimeout(() => { console.log('点了确定'); resolve(false); }, 3000); }); return res; },}); 微前端中使用基于single-spa, vue-create-dm也支持在微前端项目中跨项目调用抽屉、弹框 前提条件 需要互相调用抽屉、弹框的子项目必须都是 vue 技术栈 并且都安装注册了vue-create-dm 要跨项目调用的页面注册成路由页面 示例项目参考 micro-frontend-example 文件夹 主项目使用 注册子应用的方式需要改成config配置文件的方式。 123456789const config = [{ name: 'sub1', app: () => loadSubApp('sub1', 'http://localhost:8081/manifest-initial.json'), activeWhen: (location) => location.pathname.startsWith('/sub1'), customProps: { domElement: '#app-sub-wrapper', },}]export default config; main.js加入如下代码 123456import { listenOpenDrawerAction, triggerOpenDrawerAction } from 'vue-create-dm';import config from './micro-frontend/config/config';// 监听打开抽屉事件listenOpenDrawerAction(config, '$createAntdDrawer');// 子项目调用打开抽屉的函数window.triggerOpenDrawerAction = triggerOpenDrawerAction; 子项目使用 除了 single-spa 要求导出的生命周期方法之外,子项目的main.js需要额外导出一个router实例和一个空的vue实例。 1234567891011121314151617181920import Vue from 'vue';import router from './router';import store from './store';import VueCreateDM from 'vue-create-dm';import { Modal as antdModal, Drawer as antdDrawer } from 'ant-design-vue';Vue.use(VueCreateDM, { antdDrawer, antdModal, router, store,});export const bootstrap = function(){...}export const mount = function(){...}export const unmount = function(){...}export const update = function(){...}// 额外导出export const $router = router;export const $Vue = new Vue(); 需要被跨项目调用的组件,请在子项目的VueRouter路由配置文件中声明 子项目触发openDrawer、openModal事件123456789window.triggerOpenDrawerAction({ appName: 'sub2', path: '/about', drawerProps: { title: '子应用二抽屉', width: '50%', }, content: {},}); PS: 如果觉得不错, 欢迎前往 Github vue-create-dm 点个star 友情链接: vue-create-dm文档地址 react-create-dm文档地址","categories":[{"name":"vue","slug":"vue","permalink":"http://hzyhbk.github.io/categories/vue/"}],"tags":[{"name":"javascript","slug":"javascript","permalink":"http://hzyhbk.github.io/tags/javascript/"},{"name":"vue","slug":"vue","permalink":"http://hzyhbk.github.io/tags/vue/"}]},{"title":"彻底理解Promise.then回调的执行顺序","slug":"start/learn-promise-then","date":"2020-09-26T07:52:35.000Z","updated":"2020-09-26T08:20:59.484Z","comments":true,"path":"start/learn-promise-then/","link":"","permalink":"http://hzyhbk.github.io/start/learn-promise-then/","excerpt":"前两天看一篇公众号文章从一道面试题谈谈对 EventLoop 的理解,里面讲到了JS的事件循环,开篇第一题和打怪进阶的第一题“黄金题”我还都会做,但是到了打怪进阶的第二题“砖石题”,我就懵了,怎么也想不通为什么。","text":"前两天看一篇公众号文章从一道面试题谈谈对 EventLoop 的理解,里面讲到了JS的事件循环,开篇第一题和打怪进阶的第一题“黄金题”我还都会做,但是到了打怪进阶的第二题“砖石题”,我就懵了,怎么也想不通为什么。然后开始查阅各种资料,看 Promise A+ 规范,也没搞懂为什么。这个问题在困扰了我两天之后,在昨晚和同事的交流中,终于得到了解答。归根结底还是我自己对规范理解的不够透彻。 所以下面先跟着我一起来重新理解一下Promise A+ 规范规范吧。 A promise must provide a then method to access its current or eventual value or reason.A promise’s then method accepts two arguments: promise.then(onFulfilled, onRejected) 2.2.2 If onFulfilled is a function: 2.2.2.1 it must be called after promise is fulfilled, with promise’s value as its first argument. 2.2.2.2 it must not be called before promise is fulfilled. 2.2.2.3 it must not be called more than once. 翻译一下就是: promise必须提供一个then方法来存取它当前或最终的值或者原因。promise的then方法接收两个参数:1promise.then(onFulfilled, onRejected) 如果 onFulfilled 是函数,此函数必须在promise 完成(fulfilled)后被调用,并把promise 的值作为它的第一个参数;此函数在promise完成(fulfilled)之前绝对不能被调用;此函数绝对不能被调用超过一次。 2.2.6 then may be called multiple times on the same promise. 2.2.6.1 If/when promise is fulfilled, all respective onFulfilled callbacks must execute in the order of their originating calls to then. 2.2.6.2 If/when promise is rejected, all respective onRejected callbacks must execute in the order of their originating calls to then. 翻译一下就是: then 在同一个promise里可以被调用多次,并且当promise的状态变为fulfilled或者rejected时,onFulfilled和onRejected回调函数的调用顺序将会按照在then里定义的顺序进行调用。 也就是像下面的代码那样:123456789101112const promise1 = new Promise((resolve, reject) => { console.log(1); resolve();});promise1.then(() => { console.log(2);});promise1.then(() => { console.log(3);}) 2.2.7 then must return a promise. 翻译一下就是: then 必须返回一个promise。也是因为这个规范,所以 promise 支持链式调用。也就是像下面的代码那样:123456789101112131415161718192021222324const promise1 = new Promise((resolve, reject) => { console.log(1); resolve();});const promise2 = promise1.then(() => { console.log(2);});const promise3 = promise2.then(() => { console.log(3);})// ornew Promise((resolve, reject) => { console.log(1); resolve();}).then(() => { console.log(2);}).then(() => { console.log(3);}); 上面的代码打印顺序就是1,2,3。下面我们对代码来做一点改动:1234567891011121314151617181920new Promise((resolve, reject) => { console.log(1); resolve();}).then((a) => { console.log(2); new Promise((resolve,reject) => { console.log(3); resolve(); }) .then((c) => { console.log(4); }) .then((d) => { console.log(6); })}).then((b) => { console.log(5);}); ok,结合前面理解的几条Promise A+规范,让我们来一起分析一下这段代码的执行顺序: 首先打印 1 ,后面跟着个resolve调用,说明 promise 的状态变为fulfilled,所以把它的下一个 then 的回调即 a回调 放入微任务队列等待执行;根据规范2.2.7,then必须返回一个promise ,因此包含a回调的这个then返回了一个新的promise,我们记为promise1 ,注意此时a回调还没被执行,也就是这个promise1的状态还是pending;根据规范2.2.2.1,then回调的执行必须在上一个promise的状态为fulfilled,所以下一个then,也就是b回调其实是被缓存在promise1内部的回调队列里,等promise1的状态改变再放入微任务队列。 接着执行a回调,先打印2,然后接着往下执行,遇到了一个新的 promise,我们记为promise2, 接着先打印3,然后后面跟着个resolve调用,说明这个promise2的状态变为fulfilled,所以把它的下一个 then 的回调也就是c回调放入微任务队列等待执行,同样根据规范2.2.7,包含c回调的这个then也返回了一个新的promise,我们记为promise3,此时c回调还没有执行,也就是这个promise3的状态还是pending,同样根据规范2.2.2.1,所以下一个then,也就是d回调其实是被缓存在promise3内部的回调队列里,等promise3的状态改变再放入微任务队列。 接着a回调执行完了,没有返回东西,可以理解为返回undefined ,根据规范2.3.4,如果 x 既不是对象也不是函数,用x完成(fulfill)promise,说明上面的promise1的状态变为了fulfilled,因此之前的b回调此时可以被放入微任务队列里等待执行了。 经过上面的步骤,此时微任务队列里存在c回调和b回调。 接着先执行c回调,打印4,c回调执行完成没有问题,根据规范2.3.4,也就是上面说到的promise3的状态变为了fulfilled,此时d回调可以被放入微任务队列等待执行了。 接着执行b回调,打印5。 接着执行d回调,打印6。 综上所述,打印顺序为1,2,3,4,5,6。 理解了上面的执行顺序,我们再来稍微改变一下上面的代码,我们给内部的这个 promise 添加一个 return,来看看打印顺序会不会发生改变。1234567891011121314151617181920new Promise((resolve, reject) => { console.log(1); resolve();}).then((a) => { console.log(2); return new Promise((resolve,reject) => { console.log(3); resolve(); }) .then((c) => { console.log(4); }) .then((d) => { console.log(6); })}).then((b) => { console.log(5);}); 根据规范2.3.2,如果 x 是一个promise,采用promise的状态,如果 x 是请求状态(pending), promise (也就是这个then代表的promise)必须保持pending直到 x fulfilled 或 rejected;如果 x 是完成态(fulfilled),用相同的值完成fulfill promise ;如果 x 是拒绝态(rejected),用相同的原因reject promise 。 我的理解:此时包含a回调的这个then返回了一个新的promise,再链式调用的话,相当于包含b回调的这个then是被跟在返回的这个新的promise上。因此上面的代码可以直接理解为下面的代码:1234567891011121314151617181920new Promise((resolve, reject) => { console.log(1); resolve();}).then((a) => { console.log(2); return new Promise((resolve,reject) => { console.log(3); resolve(); }) .then((c) => { console.log(4); }) .then((d) => { console.log(6); }) .then((b) => { console.log(5); });}) 换成这段代码,再让你说出输出顺序就没问题了吧?答案是1,2,3,4,6,5。 参考文章: https://promisesaplus.com/ https://juejin.im/post/6844903649852784647 https://blog.csdn.net/w993263495/article/details/88918458 https://yuchengkai.cn/docs/frontend/#promise-%E5%AE%9E%E7%8E%B0","categories":[{"name":"js基础","slug":"js基础","permalink":"http://hzyhbk.github.io/categories/js基础/"}],"tags":[{"name":"javascript","slug":"javascript","permalink":"http://hzyhbk.github.io/tags/javascript/"},{"name":"promise","slug":"promise","permalink":"http://hzyhbk.github.io/tags/promise/"}]},{"title":"antd表格合并单元格,并且支持导出excel","slug":"start/export-excel","date":"2020-01-07T11:48:09.000Z","updated":"2020-08-17T03:19:15.966Z","comments":true,"path":"start/export-excel/","link":"","permalink":"http://hzyhbk.github.io/start/export-excel/","excerpt":"最近接到个报表的需求,需要展示合并单元格的表格,并且还要支持导出excel。粗略一看,不就两个功能嘛,但是在实现的过程中发现其实要做的还是挺多的,所以在这里记录分享一下。 一、合并单元格的实现","text":"最近接到个报表的需求,需要展示合并单元格的表格,并且还要支持导出excel。粗略一看,不就两个功能嘛,但是在实现的过程中发现其实要做的还是挺多的,所以在这里记录分享一下。 一、合并单元格的实现 合并单元格这个功能的实现用的是antd的table组件。下面是antd-table组件行列合并功能的使用介绍: 表格支持行/列合并,使用 render 里的单元格属性 colSpan 或者 rowSpan 设值为 0 时,设置的表格不会渲染。 光看这句话好像还不太好理解,看下官方例子的代码就好理解多了。123456789101112131415161718{ title: 'Name', dataIndex: 'name', render: (text, row, index) => { const obj = { children: text, props: {}, }; if (index === 2) { obj.props.rowSpan = 2; } // These two are merged into above cell if (index === 3) { obj.props.rowSpan = 0; } return obj; },}, 我来解读一下上面这一项的意思:当 index === 2,把这一行的 rowSpan 设置为2,也就是第三条数据的时候,这一个单元格需要占两格,那么对应的它后面的一行,也就是 index === 3 这一行就只能占0格了,也就是需要把index === 3时候的 rowSpan设置为0。 了解了antd-table组件怎么设置单元格合并,下面就可以开始实现了。无非就是计算一下每一列里,每一项出现的次数,然后再设置下 rowSpan 的值就好了。但是在开始计算之前,还需要做一些准备工作: 后端在返回数据的时候是通过 树形结构 返回的,而渲染表格用到的数据是 数组 的形式,所以我需要手动先转化一遍数据; 其次还要对数据按照表格每一列来排一次序,至于为什么排序,后面再说 1. 处理原始数据后端返回的数据是这样子的:123456789101112131415161718192021const mockData = [{ children: [{ children: [{ children: [], rate: 0.3333333333333333, name: '需求1', cost: 3, id: 5, projectId: 1, projectName: 'test1', assigner: '张三', }], name: '迭代一', rate: 0.75, cost: 9, id: 2, }], name: '项目一', cost: 12, id: 1,}]; 上面的数据层级有三级,表示的含义就是表格至少会有三列,而且前三列的数据是需要做合并单元格操作的。树形结构数据首先就想到了用递归的方式,因此我需要通过递归来把这种类型的数据给拍平成数组,并且拿到每一级的信息。下面是我拍平之后的结果(firstColName、secondColName、thirdColName 就是表格的列标题,也可以是表格每一列的dataIndex字段,这里为了省事,我直接拿列标题来用了)。123456789101112131415161718192021222324252627const flatMockData = [ { firstColName: { name: '项目一', cost: 12, id: 1, children: //... }, secondColName: { name: '迭代一', rate: 0.75, cost: 9, id: 2, children: //... }, thirdColName: { rate: 0.3333333333333333, name: '需求一', cost: 3, id: 5, projectId: 1, projectName: 'test1', assigner: '张三', children: //..., } },] 要让 树形结构每一级 和 表格的每一列 对应起来,原始数据肯定是不够的,还需要额外一个参数来表示每一级的深度,这样我就能把每一级的数据和表格列名对应起来。 1.1 树形结构数据加上层级参数这一步很好实现,一个递归就好了,直接上代码。12345678910function addDeepsToTreeData( data: ITreeDataItem[], depsNum: number = 0,): ITreeDataDepsItem[] { return data.map(item => ({ ...item, __deps: depsNum, // 表示层级的参数 children: addDeepsToTreeData(item.children, depsNum + 1), }));} 1.2 拍平树形结构数据因为之前我们已经给树形结构的每一层都加了表示层级的参数,这样做就是为了在拍平数据这一步的时候能很快和表格的列名对应起来。1234567891011121314151617181920212223242526272829303132const colNameList = ['firstColName', 'secondColName', 'thirdColName'];function getFlatData(data: ITreeDepsDataItem[], colNameList: string[]) { let array: { [k: string]: ITreeDepsDataItem }[] = []; function convert( data: ITreeDepsDataItem[], parentItem: { [k: string]: any } = {}, // 上一级的信息 ) { _.forEach(data, item => { if (item.children && item.children.length !== 0) { if (parentItem) { // 如果自己的children不为空,并且有上几级信息 // 就加上自己这一级的信息,继续往下传递 convert(item.children, { ...parentItem, [colNameList[item.__deps]]: item, }); } else { // 如果自己的children不为空 ,但是没有上几级的信息没有上一级信息 // 就把自己这一级的信息传递下去 convert(item.children, { [colNameList[item.__deps]]: item }); } } else { // children为空说明走到最后一级了 // 这时,表格一行里所有列的数据都获取到了,就push到临时数组里 array.push({ ...parentItem,[colNameList[item.__deps]]: item, }); } }); } convert(data); return array;}; 1.3 排序拍平之后的数组比如最后的表格有三列需要合并单元格,每一行就要根据这三列综合来排序;最后的表格有n列需要合并单元格,那么每一行就要根据这n列综合来排序,这样做是为了保证合并的都是重复出现的单元格。12345678910111213141516171819202122232425262728293031323334353637383940// 获取单个条件的排序函数type BooleanFn<T> = (x: T, y: T) => boolean;function getSort<T>(fn: BooleanFn<T>) { return function(a: T, b: T) { let ret = 0; if (fn.call(this, a, b)) { ret = -1; } else if (fn.call(this, b, a)) { ret = 1; } return ret; };}// 获取多个条件的排序函数type NumberFn<T> = (x: T, y: T) => number;function getMutipSort<T>(arr: NumberFn<T>[]) { return function(a: T, b: T) { let tmp, i = 0; do { tmp = arr[i++](a, b); } while (tmp == 0 && i < arr.length); return tmp; };}// 根据多个条件排序数据function getSortableData(flatData: { [k: string]: ITreeDepsDataItem }[],colNameList: string[]) { const sortableData = _.cloneDeep(flatData); const sortArr = colNameList.map(item => getSort<{ [k: string]: ITreeDepsDataItem }>((a, b) => { if (a[item] && b[item]) { return a[item].name.toUpperCase() < b[item].name.toUpperCase(); } return false; }), ); sortableData.sort(getMutipSort(sortArr)); return sortableData;} 到这里,数据就已经处理好了。下面就是计算每一行下面每一个单元格出现的次数 2. 计算每一行里每个单元格出现的次数循环排好序的数据,计算一行里所有单元格的重复次数(以 ${name}_${id} 作为数据的唯一标识,只用 id 做唯一标识也可以),并把结果存在以 这一行里第一个单元格数据的唯一标识 作为key的对象中。这句话可能有点不好理解,所以我用下面的数据来解释下123456789101112131415161718192021// 假设这是已经排好序的数据const sortableData = [{ firstColName: { name: '项目一', id: 1, cost: 12, children: [] }, secondColName: { name: '迭代一', id: 2, rate: 0.75, cost: 9, children: [] }, thirdColName: { name: '需求一', id: 5, rate: 0.25, cost: 3, projectId: 1, projectName: '项目一', assigner: '张三', children: [], }},{ firstColName: { name: '项目一', id: 1, cost: 12, children: [] }, secondColName: { name: '迭代一', id: 2, rate: 0.75, cost: 9, children: [] }, thirdColName: { name: '需求二', id: 6, rate: 0.25, cost: 3, projectId: 1, projectName: '项目一', assigner: '李四', children: [], }}];// 计算之后的重复次数就是这样// 这个重复出现的次数就是后面 render 函数返回的 rowSpan 的值。const cellRepetitions = { 项目一_1: { 项目一_1: 2, 迭代一_2: 2, 需求一_5: 1, 需求二_6: 1, }} 下面是计算重复次数的函数123456789101112131415161718192021222324252627282930// 获取每一组数据里重复出现的数据的次数type IObjNumber = { [k: string]: number };function getCellRepetitions(sortableData:{ [k: string]: ITreeDepsDataItem }[], colNameList: string[]) { // 表格第一列的列名 const parentColName = colNameList[0]; const cellRepetitions: { [k: string]: IObjNumber | number; } = {}; // 循环排好序的数据,item代表每一行 _.forEach(sortableData, item => { const parentColVal = `${item[parentColName].name}_${item[parentColName].id}`; if (!cellRepetitions[parentColVal]) { cellRepetitions[parentColVal] = {}; } // 循环列名,item[col]代表这一行里每一个单元格 _.forEach(colNameList, col => { if (item[col]) { const colValue = `${item[col].name}_${item[col].id}`; cellRepetitions[`__dot_${col}`] = 0; //后面在render的时候会用到的标志位 if ((cellRepetitions[parentColVal] as IObjNumber)[colValue]) { (cellRepetitions[parentColVal] as IObjNumber)[colValue]++; } else { (cellRepetitions[parentColVal] as IObjNumber)[colValue] = 1; } } }); }); return cellRepetitions;} 3. render函数计算好了重复次数之后,下面就可以写列的 render 函数了。12345678910// 以下是使用自己封装的 renderContent 的例子// renderContent 函数接收三个参数// 第一个参数是 antd-table 原始render函数的所有参数// 第二个参数是列名// 第三个参数为可选参数,不传的话默认返回name字段的值,传了的话就可以自定义返回内容{ name: 'colName', dataIndex: 'colName', render: (...rest) => renderContent(rest, 'colName'),} 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647// 获取表格每项的render函数function getRenderContent(cellRepetitions: { [k: string]: IObjNumber | number; }, colNameList: string[]) { const parentColName = colNameList[0]; // 这里需要实时修改前面留下的 __dot标志位,所以先拷贝一份数据 const countObj = _.cloneDeep(cellRepetitions); const renderContent: IRenderContent = ( rest: any, name: string, userRenderText?: IRenderText, // 自定义表格返回什么格式的数据 ) => { const [v, row, index] = rest; const textForRender = userRenderText ? userRenderText(row) : v.name; const obj: { children: string; props: { rowSpan?: number }; } = { children: textForRender, props: {}, }; // 有 v 说明是需要合并的列 if (v) { const value = `${v.name}_${v.id}`; const parentColVal = `${row[parentColName].name}_${row[parentColName].id}`; if (countObj[parentColVal]) { /** * index表示第几行的数据 * 当 行数 等于 这一列的标志位 的时候,说明这一个单元格是需要被显示的 * 所以把这一个单元格的 rowspan 设置为它的重复次数 * * 并且这时更新标志位的数值,加上当前单元格的重复次数 * 后面当 行数 小于 这一列标志位 的时候,说明这一个单元格是需要被隐藏的, * 这时把 rowSpan 设置为 0 */ if (index === countObj[`__dot_${name}`]) { obj.props.rowSpan = (countObj[parentColVal] as IObjNumber)[value]; (countObj[`__dot_${name}`] as number) += (countObj[ parentColVal ] as IObjNumber)[value]; } else if (index < countObj[`__dot_${name}`]) { obj.props.rowSpan = 0; } } } return obj; }; return renderContent;} 以上,终于把表格合并单元格功能给完成了。下面就可以愉快的开始想怎么解决导出excel了。 二、 导出excel功能的实现导出excel功能使用的是 SheetJS 这个库。 并且参考了这位大佬的文章 http://blog.haoji.me/js-excel.html 遇到的坑点就是,js-xlsx 默认不支持设置样式。但是经过一番查找,发现了一个叫 xlsx-style 的库,使用这个库就可以给导出的excel设置样式。 通过dom节点导出具体的使用方式如下: 下载 js-xlsx/dist/xlsx.full.min.js 到项目中; 下载 xlsx-style/dist/xlsx.full.min.js 到项目中; 修改 xlsx-style/dist/xlsx.full.min.js 中的 XLSX 变量 为XLSX_STYLE,因为两个文件都是默认设置全局变量 XLSX,而我们只需要在最后导出的时候使用 xlsx-style 提供的方法,而其他工具方法还是使用 js-xlsx 中的; 在项目的index.html中引入这两个文件; js-xlsx 支持从dom节点导出excel,所以方便起见直接用dom节点导出。 12const dom = document.querySelector('.ant-table-body');const sheets = XLSX.utils.table_to_book(dom).Sheets.Sheet1; 给 sheet 设置样式,下面给出我的设置作为参考,更多设置请自行参考 xlsx-style#cell-styles 12345678910111213141516171819202122232425262728function setSheetStyle(sheet: ISheet, colNameList:string[]) { // 设置列宽 sheet['!cols'] = colNameList.map(item => ({ wpx: 200 })); Object.keys(sheet).forEach(key => { if (typeof sheet[key] === 'object') { // 第一行是标题行 if (/^[A-Z]+1$/.test(key)) { sheet[key].s = { font: { sz: 18, // 字体大小为18px bold: true,// 加粗 }, alignment: { vertical: 'center', // 垂直居中 wrapText: true, // 自动换行 }, }; } else { sheet[key].s = { alignment: { vertical: 'center', wrapText: true, }, }; } } });} 在 sheet2blob 这个方法中 调用 XLSX_STYLE.write 方法来创建 workbook,上面一步设置的样式才会生效。 12345678910111213141516171819202122232425262728function sheet2blob(sheet: ISheet, name?: string) { const sheetName = name || 'sheet1'; const workbook: { SheetNames: string[]; Sheets: { [k: string]: any }; //多个sheet } = { SheetNames: [sheetName], Sheets: {}, }; workbook.Sheets[sheetName] = sheet; // 生成excel的配置项 var wopts = { bookType: 'xlsx', // 要生成的文件类型 bookSST: false, // 是否生成Shared String Table,官方解释是,如果开启生成速度会下降,但在低版本IOS设备上有更好的兼容性 type: 'binary', }; // 这里调用 XLSX_STYLE 的 write 方法 const wbout = XLSX_STYLE.write(workbook, wopts); const blob = new Blob([s2ab(wbout)], { type: 'application/octet-stream' }); // 字符串转ArrayBuffer function s2ab(s: any) { const buf = new ArrayBuffer(s.length); const view = new Uint8Array(buf); for (let i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xff; return buf; } return blob;} 导出excel文件 1234567891011121314151617181920212223242526272829303132333435363738394041424344/** * 通用的打开下载对话框方法,没有测试过具体兼容性 * @param url 下载地址,也可以是一个blob对象,必选 * @param saveName 保存文件名,可选 */function openDownloadDialog(url: string | Blob, saveName?: string) { if (typeof url == 'object' && url instanceof Blob) { url = URL.createObjectURL(url); // 创建blob地址 } let aLink = document.createElement('a'); aLink.href = url; aLink.download = saveName || ''; // HTML5新增的属性,指定保存文件名,可以不要后缀,注意,file:///模式下不会生效 let event; if (window.MouseEvent) { event = new MouseEvent('click'); } else { event = document.createEvent('MouseEvents'); event.initMouseEvent( 'click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null, ); } aLink.dispatchEvent(event);}function downloadExcelWithDom(dom: HTMLTableElement, saveName?: string) { const fileName = `${saveName}.xlsx` || '导出.xlsx'; const sheet = XLSX.utils.table_to_book(dom).Sheets.Sheet1; setSheetStyle(sheet); openDownloadDialog(sheet2blob(sheet), fileName);} 以上就完成了导出excel功能。由于兼容性问题,有些浏览器上可能无法通过dom节点导出,这时候就只能自己设置数据来导出了。下面也讲讲怎么通过自己设置数据来导出。 通过数据导出 使用 XLSX.utils.aoa_to_sheet 方法来生成sheet,这个方法可以将一个二维数组转成sheet格式 12345678910111213141516171819function getSheetData(sortableData:{ [k: string]: ITreeDepsDataItem }[], colNameList: string[]) { let aoa: string[][] = []; aoa.push(colNameList); sortableData.forEach(item => { aoa.push( // 这里只是简单的把单元格的内容设置为name属性 // 如果需要自定义单元格内容的话,可以使用 table 组件里设置好的 columns colNameList.map(colName => { if (item[colName]) { return item[colName].name; } else { const lastColName = colNameList[Object.keys(item).length - 1]; return (item[lastColName] as any)[colName] || '-'; } }), ); }); return XLSX.utils.aoa_to_sheet(aoa);} 循环表格的列跟行,计算需要合并的单元格,核心逻辑和和前面说的 renderContent 方法是相同的。单元格的合并需要设置 sheet[!merges],格式如下 12345678910111213141516// 表示从 第0行第0列 到 第0行第2列 的单元格合并// 也就是第0行前三个单元格合并sheet[!merges] = [ { // 表示start= s:{ r: 0, // 表示row c: 0, // 表示col的 }, // 表示end e:{ r: 0, c: 2 } }] 下面是获取每一行需要合并的s和e 1234567891011121314151617181920_.forEach(colNameList, (item, colIndex) => { _.forEach(sortableData, (row, rowIndex) => { // getRepeatNum 方法同 renderContent,repeat就相当于rowSpan const repeat = getRepeatNum(row[item], row, rowIndex, item); // 只对 repeat 大于1 的单元格处理 // 并且这里列是固定的,只有行会发生合并,所以会简单一点 if (repeat > 1) { sheetMerges.push({ s: { r: rowIndex + 1, // 多了标题栏 所以要加 1 c: colIndex, }, e: { r: rowIndex + repeat - 1 + 1, // 多了标题栏 所以要加 1 c: colIndex, }, }); } });}); 最后 设置样式 和 导出 这两步和上面通过dom节点导出一样,不再赘述。","categories":[{"name":"web前端","slug":"web前端","permalink":"http://hzyhbk.github.io/categories/web前端/"}],"tags":[{"name":"antd-table","slug":"antd-table","permalink":"http://hzyhbk.github.io/tags/antd-table/"},{"name":"js-xlsx","slug":"js-xlsx","permalink":"http://hzyhbk.github.io/tags/js-xlsx/"},{"name":"react","slug":"react","permalink":"http://hzyhbk.github.io/tags/react/"}]},{"title":"2020年元旦快乐","slug":"start/happy-2020","date":"2020-01-01T04:52:12.000Z","updated":"2020-08-17T03:19:15.966Z","comments":true,"path":"start/happy-2020/","link":"","permalink":"http://hzyhbk.github.io/start/happy-2020/","excerpt":"2020年元旦快乐! 2020年第一天,给博客换了个logo,顺便分享一件早上发生的糗事。","text":"2020年元旦快乐! 2020年第一天,给博客换了个logo,顺便分享一件早上发生的糗事。 早上起床,看外面有太阳,于是就想拿被子去顶楼晒一晒。我住11楼,顶楼在14楼。起床,扛上被子,进电梯,按14楼没反应,猛地反应过来电梯从今天起启用梯控卡。可是我没带,只好尴尬的随电梯到了一楼。在一楼扛着被子等了一会,没等到人上楼,迫不得已,自己扛着被子爬上了11楼…","categories":[{"name":"生活","slug":"生活","permalink":"http://hzyhbk.github.io/categories/生活/"}],"tags":[]},{"title":"echarts-for-react 修改点击图例事件","slug":"start/echarts-for-react修改点击图例事件","date":"2019-12-28T09:37:48.000Z","updated":"2020-08-17T03:19:15.965Z","comments":true,"path":"start/echarts-for-react修改点击图例事件/","link":"","permalink":"http://hzyhbk.github.io/start/echarts-for-react修改点击图例事件/","excerpt":"需求:点击图例,则选中该项,将其他项置灰;再次点击,恢复选中所有选项 做法:监听下legendselectchanged事件,用一个变量来保存上次点击的图例的名字, 如果点击的图例名称和保存的一致,说明是再次点击,应该恢复所有选项,把所有图例都置为 true; 如果点击的图例名称和保存的不一致,说明点击了另外的图例,这是需要更新变量,并且把这次点击的图例的selected设置为true,其他的设置为false。 坑点:如果在legendselectchanged里调用 echarts 的 dispatchActions,会陷入循环调用,如果还是想调用 dispatchActions方法,可参考这篇文章。我就直接调用setOption了。 监听 legendselectchanged 事件代码如下:","text":"需求:点击图例,则选中该项,将其他项置灰;再次点击,恢复选中所有选项 做法:监听下legendselectchanged事件,用一个变量来保存上次点击的图例的名字, 如果点击的图例名称和保存的一致,说明是再次点击,应该恢复所有选项,把所有图例都置为 true; 如果点击的图例名称和保存的不一致,说明点击了另外的图例,这是需要更新变量,并且把这次点击的图例的selected设置为true,其他的设置为false。 坑点:如果在legendselectchanged里调用 echarts 的 dispatchActions,会陷入循环调用,如果还是想调用 dispatchActions方法,可参考这篇文章。我就直接调用setOption了。 监听 legendselectchanged 事件代码如下:12345678910111213141516171819202122232425262728293031onLegendSelectChanged = (e: { name: string; selected: { [k: string]: boolean }; }) => { let selected = Object.assign({}, e.selected); // 用一个变量来保存上次点击的图例的名字 // 如果新点击的图例名称和保存的一致,说明是再次点击,应该恢复所有选项, // 就是把所有图例都置为true if (this.clickedLegendName === e.name) { this.clickedLegendName = ''; Object.keys(selected).forEach(key => { selected[key] = true; }); } else { // 如果和保存的名称不一致,说明点击了另外的图例 // 更新clickedLegendName // 把这次点击的图例的selected设置为true,其他的设置为false this.clickedLegendName = e.name; Object.keys(selected).forEach(key => { if (key === e.name) { selected[key] = true; } else { selected[key] = false; } }); } // 重新调用 getEchartsOption const option = this.getEchartsOption(selected); // 重新调用 setOption this.echartRef.setOption(option); }; 组件完整代码如下:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123import React, { PureComponent } from 'react';import _ from 'lodash';import styled from 'styled-components';import ReactEcharts from 'echarts-for-react';import moment from 'moment';const Root = styled.div``;type ILineChartProps = { className?: string; style?: React.CSSProperties; rawData: { data: IRawChartDataItem[]; series: { name: string; data: number[]; type: 'line'; areaStyle: {}; }[]; }; loading: boolean;};type ILineChartState = {};class LineChart extends PureComponent<ILineChartProps, ILineChartState> { echartRef: any = null; clickedLegendName: string; getEchartsOption = (selected: { [k: string]: boolean } = {}) => { const { rawData: { data, series }, } = this.props; const option = { grid: { top: 24, bottom: 8, left: 24, right: 36, containLabel: true, }, legend: { data: series.map(item => item.name), selected }, tooltip: { trigger: 'axis', formatter(params: any) { let text = ''; if (_.isArray(params)) { text = `${moment(Number(params[0].axisValue)).format( 'YYYY-MM-DD HH:mm:ss', )}<br/>`; _.forEach(params, obj => { let value = obj.value; text = `${text}${obj.marker}${obj.seriesName}: ${value}<br/>`; }); } else { text = `${moment(Number(params.axisValue)).format( 'YYYY-MM-DD HH:mm:ss', )}<br/>`; text = `${text}${params}${params.seriesName}: ${params.value}`; } return text; }, }, xAxis: { data: data.map(o => o.timestamp), axisLabel: { formatter(value: string) { return moment(Number(value)).format('HH:mm'); }, }, }, yAxis: { type: 'value', }, series: series, }; return option; }; onLegendSelectChanged = (e: { name: string; selected: { [k: string]: boolean }; }) => { let selected = Object.assign({}, e.selected); if (this.clickedLegendName === e.name) { this.clickedLegendName = ''; Object.keys(selected).forEach(key => { selected[key] = true; }); } else { this.clickedLegendName = e.name; Object.keys(selected).forEach(key => { if (key === e.name) { selected[key] = true; } else { selected[key] = false; } }); } const option = this.getEchartsOption(selected); this.echartRef.setOption(option); }; render() { const { className, style, loading } = this.props; return ( <Root className={className} style={style} > <ReactEcharts ref={(ref:any) => (this.echartRef = ref && ref.getEchartsInstance())} option={this.getEchartsOption()} showLoading={loading} onEvents={{ legendselectchanged: this.onLegendSelectChanged, }} /> </Root> ); }}","categories":[{"name":"web前端","slug":"web前端","permalink":"http://hzyhbk.github.io/categories/web前端/"}],"tags":[{"name":"echarts-for-react","slug":"echarts-for-react","permalink":"http://hzyhbk.github.io/tags/echarts-for-react/"}]},{"title":"Hello World","slug":"start/hello-world","date":"2018-12-29T16:00:00.000Z","updated":"2020-08-17T03:19:15.967Z","comments":true,"path":"start/hello-world/","link":"","permalink":"http://hzyhbk.github.io/start/hello-world/","excerpt":"Hello World 中文意思是『你好,世界』。因为《The C Programming Language》中使用它做为第一个演示程序,非常著名,所以后来的程序员在学习编程或进行设备调试时延续了这一习惯。 ——百度百科 我自己开始学写代码的时候,写的第一段代码也是Hello World, 所以我把它作为博客的第一篇文章。 控制台版本1console.log('Hello World')","text":"Hello World 中文意思是『你好,世界』。因为《The C Programming Language》中使用它做为第一个演示程序,非常著名,所以后来的程序员在学习编程或进行设备调试时延续了这一习惯。 ——百度百科 我自己开始学写代码的时候,写的第一段代码也是Hello World, 所以我把它作为博客的第一篇文章。 控制台版本1console.log('Hello World') React函数式组件版本123function() { return <div>Hello World</div>} React类组件版本12345class Demo { render() { return <div>Hello World</div> }}","categories":[{"name":"web前端","slug":"web前端","permalink":"http://hzyhbk.github.io/categories/web前端/"}],"tags":[{"name":"hello-world","slug":"hello-world","permalink":"http://hzyhbk.github.io/tags/hello-world/"}]}]}