table.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712
  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. highlightSelectionRow: {
  270. type: Boolean,
  271. default: false
  272. },
  273. currentRowKey: [String, Number],
  274. emptyText: String,
  275. expandRowKeys: Array,
  276. defaultExpandAll: Boolean,
  277. defaultSort: Object,
  278. tooltipEffect: String,
  279. spanMethod: Function,
  280. selectOnIndeterminate: {
  281. type: Boolean,
  282. default: true
  283. },
  284. indent: {
  285. type: Number,
  286. default: 16
  287. },
  288. treeProps: {
  289. type: Object,
  290. default() {
  291. return {
  292. hasChildren: 'hasChildren',
  293. children: 'children'
  294. };
  295. }
  296. },
  297. lazy: Boolean,
  298. load: Function
  299. },
  300. components: {
  301. TableHeader,
  302. TableFooter,
  303. TableBody,
  304. ElCheckbox
  305. },
  306. methods: {
  307. getMigratingConfig() {
  308. return {
  309. events: {
  310. expand: 'expand is renamed to expand-change'
  311. }
  312. };
  313. },
  314. setCurrentRow(row) {
  315. this.store.commit('setCurrentRow', row);
  316. },
  317. toggleRowSelection(row, selected) {
  318. this.store.toggleRowSelection(row, selected, false);
  319. this.store.updateAllSelected();
  320. },
  321. toggleRowExpansion(row, expanded) {
  322. this.store.toggleRowExpansionAdapter(row, expanded);
  323. },
  324. clearSelection() {
  325. this.store.clearSelection();
  326. },
  327. clearFilter(columnKeys) {
  328. this.store.clearFilter(columnKeys);
  329. },
  330. clearSort() {
  331. this.store.clearSort();
  332. },
  333. handleMouseLeave() {
  334. this.store.commit('setHoverRow', null);
  335. if (this.hoverState) this.hoverState = null;
  336. },
  337. updateScrollY() {
  338. const changed = this.layout.updateScrollY();
  339. if (changed) {
  340. this.layout.notifyObservers('scrollable');
  341. this.layout.updateColumnsWidth();
  342. }
  343. },
  344. handleFixedMousewheel(event, data) {
  345. const bodyWrapper = this.bodyWrapper;
  346. if (Math.abs(data.spinY) > 0) {
  347. const currentScrollTop = bodyWrapper.scrollTop;
  348. if (data.pixelY < 0 && currentScrollTop !== 0) {
  349. event.preventDefault();
  350. }
  351. if (data.pixelY > 0 && bodyWrapper.scrollHeight - bodyWrapper.clientHeight > currentScrollTop) {
  352. event.preventDefault();
  353. }
  354. bodyWrapper.scrollTop += Math.ceil(data.pixelY / 5);
  355. } else {
  356. bodyWrapper.scrollLeft += Math.ceil(data.pixelX / 5);
  357. }
  358. },
  359. handleHeaderFooterMousewheel(event, data) {
  360. const { pixelX, pixelY } = data;
  361. if (Math.abs(pixelX) >= Math.abs(pixelY)) {
  362. this.bodyWrapper.scrollLeft += data.pixelX / 5;
  363. }
  364. },
  365. // TODO 使用 CSS transform
  366. syncPostion() {
  367. const { scrollLeft, scrollTop, offsetWidth, scrollWidth } = this.bodyWrapper;
  368. const { headerWrapper, footerWrapper, fixedBodyWrapper, rightFixedBodyWrapper } = this.$refs;
  369. if (headerWrapper) headerWrapper.scrollLeft = scrollLeft;
  370. if (footerWrapper) footerWrapper.scrollLeft = scrollLeft;
  371. if (fixedBodyWrapper) fixedBodyWrapper.scrollTop = scrollTop;
  372. if (rightFixedBodyWrapper) rightFixedBodyWrapper.scrollTop = scrollTop;
  373. const maxScrollLeftPosition = scrollWidth - offsetWidth - 1;
  374. if (scrollLeft >= maxScrollLeftPosition) {
  375. this.scrollPosition = 'right';
  376. } else if (scrollLeft === 0) {
  377. this.scrollPosition = 'left';
  378. } else {
  379. this.scrollPosition = 'middle';
  380. }
  381. },
  382. throttleSyncPostion: throttle(16, function() {
  383. this.syncPostion();
  384. }),
  385. onScroll(evt) {
  386. let raf = window.requestAnimationFrame;
  387. if (!raf) {
  388. this.throttleSyncPostion();
  389. } else {
  390. raf(this.syncPostion);
  391. }
  392. },
  393. bindEvents() {
  394. this.bodyWrapper.addEventListener('scroll', this.onScroll, { passive: true });
  395. if (this.fit) {
  396. addResizeListener(this.$el, this.resizeListener);
  397. }
  398. },
  399. unbindEvents() {
  400. this.bodyWrapper.removeEventListener('scroll', this.onScroll, { passive: true });
  401. if (this.fit) {
  402. removeResizeListener(this.$el, this.resizeListener);
  403. }
  404. },
  405. resizeListener() {
  406. if (!this.$ready) return;
  407. let shouldUpdateLayout = false;
  408. const el = this.$el;
  409. const { width: oldWidth, height: oldHeight } = this.resizeState;
  410. const width = el.offsetWidth;
  411. if (oldWidth !== width) {
  412. shouldUpdateLayout = true;
  413. }
  414. const height = el.offsetHeight;
  415. if ((this.height || this.shouldUpdateHeight) && oldHeight !== height) {
  416. shouldUpdateLayout = true;
  417. }
  418. if (shouldUpdateLayout) {
  419. this.resizeState.width = width;
  420. this.resizeState.height = height;
  421. this.doLayout();
  422. }
  423. },
  424. doLayout() {
  425. if (this.shouldUpdateHeight) {
  426. this.layout.updateElsHeight();
  427. }
  428. this.layout.updateColumnsWidth();
  429. },
  430. sort(prop, order) {
  431. this.store.commit('sort', { prop, order });
  432. },
  433. toggleAllSelection() {
  434. this.store.commit('toggleAllSelection');
  435. }
  436. },
  437. computed: {
  438. tableSize() {
  439. return this.size || (this.$ELEMENT || {}).size;
  440. },
  441. bodyWrapper() {
  442. return this.$refs.bodyWrapper;
  443. },
  444. shouldUpdateHeight() {
  445. return this.height ||
  446. this.maxHeight ||
  447. this.fixedColumns.length > 0 ||
  448. this.rightFixedColumns.length > 0;
  449. },
  450. bodyWidth() {
  451. const { bodyWidth, scrollY, gutterWidth } = this.layout;
  452. return bodyWidth ? bodyWidth - (scrollY ? gutterWidth : 0) + 'px' : '';
  453. },
  454. bodyHeight() {
  455. const { headerHeight = 0, bodyHeight, footerHeight = 0} = this.layout;
  456. if (this.height) {
  457. return {
  458. height: bodyHeight ? bodyHeight + 'px' : ''
  459. };
  460. } else if (this.maxHeight) {
  461. const maxHeight = parseHeight(this.maxHeight);
  462. if (typeof maxHeight === 'number') {
  463. return {
  464. 'max-height': (maxHeight - footerHeight - (this.showHeader ? headerHeight : 0)) + 'px'
  465. };
  466. }
  467. }
  468. return {};
  469. },
  470. fixedBodyHeight() {
  471. if (this.height) {
  472. return {
  473. height: this.layout.fixedBodyHeight ? this.layout.fixedBodyHeight + 'px' : ''
  474. };
  475. } else if (this.maxHeight) {
  476. let maxHeight = parseHeight(this.maxHeight);
  477. if (typeof maxHeight === 'number') {
  478. maxHeight = this.layout.scrollX ? maxHeight - this.layout.gutterWidth : maxHeight;
  479. if (this.showHeader) {
  480. maxHeight -= this.layout.headerHeight;
  481. }
  482. maxHeight -= this.layout.footerHeight;
  483. return {
  484. 'max-height': maxHeight + 'px'
  485. };
  486. }
  487. }
  488. return {};
  489. },
  490. fixedHeight() {
  491. if (this.maxHeight) {
  492. if (this.showSummary) {
  493. return {
  494. bottom: 0
  495. };
  496. }
  497. return {
  498. bottom: (this.layout.scrollX && this.data.length) ? this.layout.gutterWidth + 'px' : ''
  499. };
  500. } else {
  501. if (this.showSummary) {
  502. return {
  503. height: this.layout.tableHeight ? this.layout.tableHeight + 'px' : ''
  504. };
  505. }
  506. return {
  507. height: this.layout.viewportHeight ? this.layout.viewportHeight + 'px' : ''
  508. };
  509. }
  510. },
  511. emptyBlockStyle() {
  512. if (this.data && this.data.length) return null;
  513. let height = '100%';
  514. if (this.layout.appendHeight) {
  515. height = `calc(100% - ${this.layout.appendHeight}px)`;
  516. }
  517. return {
  518. width: this.bodyWidth,
  519. height
  520. };
  521. },
  522. ...mapStates({
  523. selection: 'selection',
  524. columns: 'columns',
  525. tableData: 'data',
  526. fixedColumns: 'fixedColumns',
  527. rightFixedColumns: 'rightFixedColumns'
  528. })
  529. },
  530. watch: {
  531. height: {
  532. immediate: true,
  533. handler(value) {
  534. this.layout.setHeight(value);
  535. }
  536. },
  537. maxHeight: {
  538. immediate: true,
  539. handler(value) {
  540. this.layout.setMaxHeight(value);
  541. }
  542. },
  543. currentRowKey: {
  544. immediate: true,
  545. handler(value) {
  546. if (!this.rowKey) return;
  547. this.store.setCurrentRowKey(value);
  548. }
  549. },
  550. data: {
  551. immediate: true,
  552. handler(value) {
  553. this.store.commit('setData', value);
  554. }
  555. },
  556. expandRowKeys: {
  557. immediate: true,
  558. handler(newVal) {
  559. if (newVal) {
  560. this.store.setExpandRowKeysAdapter(newVal);
  561. }
  562. }
  563. }
  564. },
  565. created() {
  566. this.tableId = 'el-table_' + tableIdSeed++;
  567. this.debouncedUpdateLayout = debounce(50, () => this.doLayout());
  568. },
  569. mounted() {
  570. this.bindEvents();
  571. this.store.updateColumns();
  572. this.doLayout();
  573. this.resizeState = {
  574. width: this.$el.offsetWidth,
  575. height: this.$el.offsetHeight
  576. };
  577. // init filters
  578. this.store.states.columns.forEach(column => {
  579. if (column.filteredValue && column.filteredValue.length) {
  580. this.store.commit('filterChange', {
  581. column,
  582. values: column.filteredValue,
  583. silent: true
  584. });
  585. }
  586. });
  587. this.$ready = true;
  588. },
  589. destroyed() {
  590. this.unbindEvents();
  591. },
  592. data() {
  593. const { hasChildren = 'hasChildren', children = 'children' } = this.treeProps;
  594. this.store = createStore(this, {
  595. rowKey: this.rowKey,
  596. defaultExpandAll: this.defaultExpandAll,
  597. selectOnIndeterminate: this.selectOnIndeterminate,
  598. // TreeTable 的相关配置
  599. indent: this.indent,
  600. lazy: this.lazy,
  601. lazyColumnIdentifier: hasChildren,
  602. childrenColumnName: children
  603. });
  604. const layout = new TableLayout({
  605. store: this.store,
  606. table: this,
  607. fit: this.fit,
  608. showHeader: this.showHeader
  609. });
  610. return {
  611. layout,
  612. isHidden: false,
  613. renderExpanded: null,
  614. resizeProxyVisible: false,
  615. resizeState: {
  616. width: null,
  617. height: null
  618. },
  619. // 是否拥有多级表头
  620. isGroup: false,
  621. scrollPosition: 'left'
  622. };
  623. }
  624. };
  625. </script>