table.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707
  1. <template>
  2. <div class="el-table"
  3. :class="[{
  4. 'el-table--fit': fit,
  5. 'el-table--striped': stripe,
  6. 'el-table--border': border || isGroup,
  7. 'el-table--hidden': isHidden,
  8. 'el-table--group': isGroup,
  9. 'el-table--fluid-height': maxHeight,
  10. 'el-table--scrollable-x': layout.scrollX,
  11. 'el-table--scrollable-y': layout.scrollY,
  12. 'el-table--enable-row-hover': !store.states.isComplex,
  13. 'el-table--enable-row-transition': (store.states.data || []).length !== 0 && (store.states.data || []).length < 100
  14. }, tableSize ? `el-table--${ tableSize }` : '']"
  15. @mouseleave="handleMouseLeave($event)">
  16. <div class="hidden-columns" ref="hiddenColumns"><slot></slot></div>
  17. <div
  18. v-if="showHeader"
  19. v-mousewheel="handleHeaderFooterMousewheel"
  20. class="el-table__header-wrapper"
  21. ref="headerWrapper">
  22. <table-header
  23. ref="tableHeader"
  24. :store="store"
  25. :border="border"
  26. :default-sort="defaultSort"
  27. :style="{
  28. width: layout.bodyWidth ? layout.bodyWidth + 'px' : ''
  29. }">
  30. </table-header>
  31. </div>
  32. <div
  33. class="el-table__body-wrapper"
  34. ref="bodyWrapper"
  35. :class="[layout.scrollX ? `is-scrolling-${scrollPosition}` : 'is-scrolling-none']"
  36. :style="[bodyHeight]">
  37. <table-body
  38. :context="context"
  39. :store="store"
  40. :stripe="stripe"
  41. :row-class-name="rowClassName"
  42. :row-style="rowStyle"
  43. :highlight="highlightCurrentRow"
  44. :style="{
  45. width: bodyWidth
  46. }">
  47. </table-body>
  48. <div
  49. v-if="!data || data.length === 0"
  50. class="el-table__empty-block"
  51. ref="emptyBlock"
  52. :style="emptyBlockStyle">
  53. <span class="el-table__empty-text" >
  54. <slot name="empty">{{ emptyText || t('el.table.emptyText') }}</slot>
  55. </span>
  56. </div>
  57. <div
  58. v-if="$slots.append"
  59. class="el-table__append-wrapper"
  60. ref="appendWrapper">
  61. <slot name="append"></slot>
  62. </div>
  63. </div>
  64. <div
  65. v-if="showSummary"
  66. v-show="data && data.length > 0"
  67. v-mousewheel="handleHeaderFooterMousewheel"
  68. class="el-table__footer-wrapper"
  69. ref="footerWrapper">
  70. <table-footer
  71. :store="store"
  72. :border="border"
  73. :sum-text="sumText || t('el.table.sumText')"
  74. :summary-method="summaryMethod"
  75. :default-sort="defaultSort"
  76. :style="{
  77. width: layout.bodyWidth ? layout.bodyWidth + 'px' : ''
  78. }">
  79. </table-footer>
  80. </div>
  81. <div
  82. v-if="fixedColumns.length > 0"
  83. v-mousewheel="handleFixedMousewheel"
  84. class="el-table__fixed"
  85. ref="fixedWrapper"
  86. :style="[{
  87. width: layout.fixedWidth ? layout.fixedWidth + 'px' : ''
  88. },
  89. fixedHeight]">
  90. <div
  91. v-if="showHeader"
  92. class="el-table__fixed-header-wrapper"
  93. ref="fixedHeaderWrapper" >
  94. <table-header
  95. ref="fixedTableHeader"
  96. fixed="left"
  97. :border="border"
  98. :store="store"
  99. :style="{
  100. width: bodyWidth
  101. }"></table-header>
  102. </div>
  103. <div
  104. class="el-table__fixed-body-wrapper"
  105. ref="fixedBodyWrapper"
  106. :style="[{
  107. top: layout.headerHeight + 'px'
  108. },
  109. fixedBodyHeight]">
  110. <table-body
  111. fixed="left"
  112. :store="store"
  113. :stripe="stripe"
  114. :highlight="highlightCurrentRow"
  115. :row-class-name="rowClassName"
  116. :row-style="rowStyle"
  117. :style="{
  118. width: bodyWidth
  119. }">
  120. </table-body>
  121. <div
  122. v-if="$slots.append"
  123. class="el-table__append-gutter"
  124. :style="{ height: layout.appendHeight + 'px'}"></div>
  125. </div>
  126. <div
  127. v-if="showSummary"
  128. v-show="data && data.length > 0"
  129. class="el-table__fixed-footer-wrapper"
  130. ref="fixedFooterWrapper">
  131. <table-footer
  132. fixed="left"
  133. :border="border"
  134. :sum-text="sumText || t('el.table.sumText')"
  135. :summary-method="summaryMethod"
  136. :store="store"
  137. :style="{
  138. width: bodyWidth
  139. }"></table-footer>
  140. </div>
  141. </div>
  142. <div
  143. v-if="rightFixedColumns.length > 0"
  144. v-mousewheel="handleFixedMousewheel"
  145. class="el-table__fixed-right"
  146. ref="rightFixedWrapper"
  147. :style="[{
  148. width: layout.rightFixedWidth ? layout.rightFixedWidth + 'px' : '',
  149. right: layout.scrollY ? (border ? layout.gutterWidth : (layout.gutterWidth || 0)) + 'px' : ''
  150. },
  151. fixedHeight]">
  152. <div v-if="showHeader"
  153. class="el-table__fixed-header-wrapper"
  154. ref="rightFixedHeaderWrapper">
  155. <table-header
  156. ref="rightFixedTableHeader"
  157. fixed="right"
  158. :border="border"
  159. :store="store"
  160. :style="{
  161. width: bodyWidth
  162. }"></table-header>
  163. </div>
  164. <div
  165. class="el-table__fixed-body-wrapper"
  166. ref="rightFixedBodyWrapper"
  167. :style="[{
  168. top: layout.headerHeight + 'px'
  169. },
  170. fixedBodyHeight]">
  171. <table-body
  172. fixed="right"
  173. :store="store"
  174. :stripe="stripe"
  175. :row-class-name="rowClassName"
  176. :row-style="rowStyle"
  177. :highlight="highlightCurrentRow"
  178. :style="{
  179. width: bodyWidth
  180. }">
  181. </table-body>
  182. <div
  183. v-if="$slots.append"
  184. class="el-table__append-gutter"
  185. :style="{ height: layout.appendHeight + 'px' }"></div>
  186. </div>
  187. <div
  188. v-if="showSummary"
  189. v-show="data && data.length > 0"
  190. class="el-table__fixed-footer-wrapper"
  191. ref="rightFixedFooterWrapper">
  192. <table-footer
  193. fixed="right"
  194. :border="border"
  195. :sum-text="sumText || t('el.table.sumText')"
  196. :summary-method="summaryMethod"
  197. :store="store"
  198. :style="{
  199. width: bodyWidth
  200. }"></table-footer>
  201. </div>
  202. </div>
  203. <div
  204. v-if="rightFixedColumns.length > 0"
  205. class="el-table__fixed-right-patch"
  206. ref="rightFixedPatch"
  207. :style="{
  208. width: layout.scrollY ? layout.gutterWidth + 'px' : '0',
  209. height: layout.headerHeight + 'px'
  210. }"></div>
  211. <div class="el-table__column-resize-proxy" ref="resizeProxy" v-show="resizeProxyVisible"></div>
  212. </div>
  213. </template>
  214. <script type="text/babel">
  215. import ElCheckbox from 'element-ui/packages/checkbox';
  216. import { debounce, throttle } from 'throttle-debounce';
  217. import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event';
  218. import Mousewheel from 'element-ui/src/directives/mousewheel';
  219. import Locale from 'element-ui/src/mixins/locale';
  220. import Migrating from 'element-ui/src/mixins/migrating';
  221. import { createStore, mapStates } from './store/helper';
  222. import TableLayout from './table-layout';
  223. import TableBody from './table-body';
  224. import TableHeader from './table-header';
  225. import TableFooter from './table-footer';
  226. import { parseHeight } from './util';
  227. let tableIdSeed = 1;
  228. export default {
  229. name: 'ElTable',
  230. mixins: [Locale, Migrating],
  231. directives: {
  232. Mousewheel
  233. },
  234. props: {
  235. data: {
  236. type: Array,
  237. default: function() {
  238. return [];
  239. }
  240. },
  241. size: String,
  242. width: [String, Number],
  243. height: [String, Number],
  244. maxHeight: [String, Number],
  245. fit: {
  246. type: Boolean,
  247. default: true
  248. },
  249. stripe: Boolean,
  250. border: Boolean,
  251. rowKey: [String, Function],
  252. context: {},
  253. showHeader: {
  254. type: Boolean,
  255. default: true
  256. },
  257. showSummary: Boolean,
  258. sumText: String,
  259. summaryMethod: Function,
  260. rowClassName: [String, Function],
  261. rowStyle: [Object, Function],
  262. cellClassName: [String, Function],
  263. cellStyle: [Object, Function],
  264. headerRowClassName: [String, Function],
  265. headerRowStyle: [Object, Function],
  266. headerCellClassName: [String, Function],
  267. headerCellStyle: [Object, Function],
  268. highlightCurrentRow: Boolean,
  269. currentRowKey: [String, Number],
  270. emptyText: String,
  271. expandRowKeys: Array,
  272. defaultExpandAll: Boolean,
  273. defaultSort: Object,
  274. tooltipEffect: String,
  275. spanMethod: Function,
  276. selectOnIndeterminate: {
  277. type: Boolean,
  278. default: true
  279. },
  280. indent: {
  281. type: Number,
  282. default: 16
  283. },
  284. treeProps: {
  285. type: Object,
  286. default() {
  287. return {
  288. hasChildren: 'hasChildren',
  289. children: 'children'
  290. };
  291. }
  292. },
  293. lazy: Boolean,
  294. load: Function
  295. },
  296. components: {
  297. TableHeader,
  298. TableFooter,
  299. TableBody,
  300. ElCheckbox
  301. },
  302. methods: {
  303. getMigratingConfig() {
  304. return {
  305. events: {
  306. expand: 'expand is renamed to expand-change'
  307. }
  308. };
  309. },
  310. setCurrentRow(row) {
  311. this.store.commit('setCurrentRow', row);
  312. },
  313. toggleRowSelection(row, selected) {
  314. this.store.toggleRowSelection(row, selected, false);
  315. this.store.updateAllSelected();
  316. },
  317. toggleRowExpansion(row, expanded) {
  318. this.store.toggleRowExpansionAdapter(row, expanded);
  319. },
  320. clearSelection() {
  321. this.store.clearSelection();
  322. },
  323. clearFilter(columnKeys) {
  324. this.store.clearFilter(columnKeys);
  325. },
  326. clearSort() {
  327. this.store.clearSort();
  328. },
  329. handleMouseLeave() {
  330. this.store.commit('setHoverRow', null);
  331. if (this.hoverState) this.hoverState = null;
  332. },
  333. updateScrollY() {
  334. const changed = this.layout.updateScrollY();
  335. if (changed) {
  336. this.layout.notifyObservers('scrollable');
  337. this.layout.updateColumnsWidth();
  338. }
  339. },
  340. handleFixedMousewheel(event, data) {
  341. const bodyWrapper = this.bodyWrapper;
  342. if (Math.abs(data.spinY) > 0) {
  343. const currentScrollTop = bodyWrapper.scrollTop;
  344. if (data.pixelY < 0 && currentScrollTop !== 0) {
  345. event.preventDefault();
  346. }
  347. if (data.pixelY > 0 && bodyWrapper.scrollHeight - bodyWrapper.clientHeight > currentScrollTop) {
  348. event.preventDefault();
  349. }
  350. bodyWrapper.scrollTop += Math.ceil(data.pixelY / 5);
  351. } else {
  352. bodyWrapper.scrollLeft += Math.ceil(data.pixelX / 5);
  353. }
  354. },
  355. handleHeaderFooterMousewheel(event, data) {
  356. const { pixelX, pixelY } = data;
  357. if (Math.abs(pixelX) >= Math.abs(pixelY)) {
  358. this.bodyWrapper.scrollLeft += data.pixelX / 5;
  359. }
  360. },
  361. // TODO 使用 CSS transform
  362. syncPostion() {
  363. const { scrollLeft, scrollTop, offsetWidth, scrollWidth } = this.bodyWrapper;
  364. const { headerWrapper, footerWrapper, fixedBodyWrapper, rightFixedBodyWrapper } = this.$refs;
  365. if (headerWrapper) headerWrapper.scrollLeft = scrollLeft;
  366. if (footerWrapper) footerWrapper.scrollLeft = scrollLeft;
  367. if (fixedBodyWrapper) fixedBodyWrapper.scrollTop = scrollTop;
  368. if (rightFixedBodyWrapper) rightFixedBodyWrapper.scrollTop = scrollTop;
  369. const maxScrollLeftPosition = scrollWidth - offsetWidth - 1;
  370. if (scrollLeft >= maxScrollLeftPosition) {
  371. this.scrollPosition = 'right';
  372. } else if (scrollLeft === 0) {
  373. this.scrollPosition = 'left';
  374. } else {
  375. this.scrollPosition = 'middle';
  376. }
  377. },
  378. throttleSyncPostion: throttle(16, function() {
  379. this.syncPostion();
  380. }),
  381. onScroll(evt) {
  382. let raf = window.requestAnimationFrame;
  383. if (!raf) {
  384. this.throttleSyncPostion();
  385. } else {
  386. raf(this.syncPostion);
  387. }
  388. },
  389. bindEvents() {
  390. this.bodyWrapper.addEventListener('scroll', this.onScroll, { passive: true });
  391. if (this.fit) {
  392. addResizeListener(this.$el, this.resizeListener);
  393. }
  394. },
  395. unbindEvents() {
  396. this.bodyWrapper.removeEventListener('scroll', this.onScroll, { passive: true });
  397. if (this.fit) {
  398. removeResizeListener(this.$el, this.resizeListener);
  399. }
  400. },
  401. resizeListener() {
  402. if (!this.$ready) return;
  403. let shouldUpdateLayout = false;
  404. const el = this.$el;
  405. const { width: oldWidth, height: oldHeight } = this.resizeState;
  406. const width = el.offsetWidth;
  407. if (oldWidth !== width) {
  408. shouldUpdateLayout = true;
  409. }
  410. const height = el.offsetHeight;
  411. if ((this.height || this.shouldUpdateHeight) && oldHeight !== height) {
  412. shouldUpdateLayout = true;
  413. }
  414. if (shouldUpdateLayout) {
  415. this.resizeState.width = width;
  416. this.resizeState.height = height;
  417. this.doLayout();
  418. }
  419. },
  420. doLayout() {
  421. if (this.shouldUpdateHeight) {
  422. this.layout.updateElsHeight();
  423. }
  424. this.layout.updateColumnsWidth();
  425. },
  426. sort(prop, order) {
  427. this.store.commit('sort', { prop, order });
  428. },
  429. toggleAllSelection() {
  430. this.store.commit('toggleAllSelection');
  431. }
  432. },
  433. computed: {
  434. tableSize() {
  435. return this.size || (this.$ELEMENT || {}).size;
  436. },
  437. bodyWrapper() {
  438. return this.$refs.bodyWrapper;
  439. },
  440. shouldUpdateHeight() {
  441. return this.height ||
  442. this.maxHeight ||
  443. this.fixedColumns.length > 0 ||
  444. this.rightFixedColumns.length > 0;
  445. },
  446. bodyWidth() {
  447. const { bodyWidth, scrollY, gutterWidth } = this.layout;
  448. return bodyWidth ? bodyWidth - (scrollY ? gutterWidth : 0) + 'px' : '';
  449. },
  450. bodyHeight() {
  451. const { headerHeight = 0, bodyHeight, footerHeight = 0} = this.layout;
  452. if (this.height) {
  453. return {
  454. height: bodyHeight ? bodyHeight + 'px' : ''
  455. };
  456. } else if (this.maxHeight) {
  457. const maxHeight = parseHeight(this.maxHeight);
  458. if (typeof maxHeight === 'number') {
  459. return {
  460. 'max-height': (maxHeight - footerHeight - (this.showHeader ? headerHeight : 0)) + 'px'
  461. };
  462. }
  463. }
  464. return {};
  465. },
  466. fixedBodyHeight() {
  467. if (this.height) {
  468. return {
  469. height: this.layout.fixedBodyHeight ? this.layout.fixedBodyHeight + 'px' : ''
  470. };
  471. } else if (this.maxHeight) {
  472. let maxHeight = parseHeight(this.maxHeight);
  473. if (typeof maxHeight === 'number') {
  474. maxHeight = this.layout.scrollX ? maxHeight - this.layout.gutterWidth : maxHeight;
  475. if (this.showHeader) {
  476. maxHeight -= this.layout.headerHeight;
  477. }
  478. maxHeight -= this.layout.footerHeight;
  479. return {
  480. 'max-height': maxHeight + 'px'
  481. };
  482. }
  483. }
  484. return {};
  485. },
  486. fixedHeight() {
  487. if (this.maxHeight) {
  488. if (this.showSummary) {
  489. return {
  490. bottom: 0
  491. };
  492. }
  493. return {
  494. bottom: (this.layout.scrollX && this.data.length) ? this.layout.gutterWidth + 'px' : ''
  495. };
  496. } else {
  497. if (this.showSummary) {
  498. return {
  499. height: this.layout.tableHeight ? this.layout.tableHeight + 'px' : ''
  500. };
  501. }
  502. return {
  503. height: this.layout.viewportHeight ? this.layout.viewportHeight + 'px' : ''
  504. };
  505. }
  506. },
  507. emptyBlockStyle() {
  508. if (this.data && this.data.length) return null;
  509. let height = '100%';
  510. if (this.layout.appendHeight) {
  511. height = `calc(100% - ${this.layout.appendHeight}px)`;
  512. }
  513. return {
  514. width: this.bodyWidth,
  515. height
  516. };
  517. },
  518. ...mapStates({
  519. selection: 'selection',
  520. columns: 'columns',
  521. tableData: 'data',
  522. fixedColumns: 'fixedColumns',
  523. rightFixedColumns: 'rightFixedColumns'
  524. })
  525. },
  526. watch: {
  527. height: {
  528. immediate: true,
  529. handler(value) {
  530. this.layout.setHeight(value);
  531. }
  532. },
  533. maxHeight: {
  534. immediate: true,
  535. handler(value) {
  536. this.layout.setMaxHeight(value);
  537. }
  538. },
  539. currentRowKey: {
  540. immediate: true,
  541. handler(value) {
  542. if (!this.rowKey) return;
  543. this.store.setCurrentRowKey(value);
  544. }
  545. },
  546. data: {
  547. immediate: true,
  548. handler(value) {
  549. this.store.commit('setData', value);
  550. }
  551. },
  552. expandRowKeys: {
  553. immediate: true,
  554. handler(newVal) {
  555. if (newVal) {
  556. this.store.setExpandRowKeysAdapter(newVal);
  557. }
  558. }
  559. }
  560. },
  561. created() {
  562. this.tableId = 'el-table_' + tableIdSeed++;
  563. this.debouncedUpdateLayout = debounce(50, () => this.doLayout());
  564. },
  565. mounted() {
  566. this.bindEvents();
  567. this.store.updateColumns();
  568. this.doLayout();
  569. this.resizeState = {
  570. width: this.$el.offsetWidth,
  571. height: this.$el.offsetHeight
  572. };
  573. // init filters
  574. this.store.states.columns.forEach(column => {
  575. if (column.filteredValue && column.filteredValue.length) {
  576. this.store.commit('filterChange', {
  577. column,
  578. values: column.filteredValue,
  579. silent: true
  580. });
  581. }
  582. });
  583. this.$ready = true;
  584. },
  585. destroyed() {
  586. this.unbindEvents();
  587. },
  588. data() {
  589. const { hasChildren = 'hasChildren', children = 'children' } = this.treeProps;
  590. this.store = createStore(this, {
  591. rowKey: this.rowKey,
  592. defaultExpandAll: this.defaultExpandAll,
  593. selectOnIndeterminate: this.selectOnIndeterminate,
  594. // TreeTable 的相关配置
  595. indent: this.indent,
  596. lazy: this.lazy,
  597. lazyColumnIdentifier: hasChildren,
  598. childrenColumnName: children
  599. });
  600. const layout = new TableLayout({
  601. store: this.store,
  602. table: this,
  603. fit: this.fit,
  604. showHeader: this.showHeader
  605. });
  606. return {
  607. layout,
  608. isHidden: false,
  609. renderExpanded: null,
  610. resizeProxyVisible: false,
  611. resizeState: {
  612. width: null,
  613. height: null
  614. },
  615. // 是否拥有多级表头
  616. isGroup: false,
  617. scrollPosition: 'left'
  618. };
  619. }
  620. };
  621. </script>