table.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694
  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: throttle(20, function() {
  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. bindEvents() {
  379. this.bodyWrapper.addEventListener('scroll', this.syncPostion, { passive: true });
  380. if (this.fit) {
  381. addResizeListener(this.$el, this.resizeListener);
  382. }
  383. },
  384. unbindEvents() {
  385. this.bodyWrapper.removeEventListener('scroll', this.syncPostion, { passive: true });
  386. if (this.fit) {
  387. removeResizeListener(this.$el, this.resizeListener);
  388. }
  389. },
  390. resizeListener() {
  391. if (!this.$ready) return;
  392. let shouldUpdateLayout = false;
  393. const el = this.$el;
  394. const { width: oldWidth, height: oldHeight } = this.resizeState;
  395. const width = el.offsetWidth;
  396. if (oldWidth !== width) {
  397. shouldUpdateLayout = true;
  398. }
  399. const height = el.offsetHeight;
  400. if ((this.height || this.shouldUpdateHeight) && oldHeight !== height) {
  401. shouldUpdateLayout = true;
  402. }
  403. if (shouldUpdateLayout) {
  404. this.resizeState.width = width;
  405. this.resizeState.height = height;
  406. this.doLayout();
  407. }
  408. },
  409. doLayout() {
  410. if (this.shouldUpdateHeight) {
  411. this.layout.updateElsHeight();
  412. }
  413. this.layout.updateColumnsWidth();
  414. },
  415. sort(prop, order) {
  416. this.store.commit('sort', { prop, order });
  417. },
  418. toggleAllSelection() {
  419. this.store.commit('toggleAllSelection');
  420. }
  421. },
  422. computed: {
  423. tableSize() {
  424. return this.size || (this.$ELEMENT || {}).size;
  425. },
  426. bodyWrapper() {
  427. return this.$refs.bodyWrapper;
  428. },
  429. shouldUpdateHeight() {
  430. return this.height ||
  431. this.maxHeight ||
  432. this.fixedColumns.length > 0 ||
  433. this.rightFixedColumns.length > 0;
  434. },
  435. bodyWidth() {
  436. const { bodyWidth, scrollY, gutterWidth } = this.layout;
  437. return bodyWidth ? bodyWidth - (scrollY ? gutterWidth : 0) + 'px' : '';
  438. },
  439. bodyHeight() {
  440. const { headerHeight = 0, bodyHeight, footerHeight = 0} = this.layout;
  441. if (this.height) {
  442. return {
  443. height: bodyHeight ? bodyHeight + 'px' : ''
  444. };
  445. } else if (this.maxHeight) {
  446. const maxHeight = parseHeight(this.maxHeight);
  447. if (typeof maxHeight === 'number') {
  448. return {
  449. 'max-height': (maxHeight - footerHeight - (this.showHeader ? headerHeight : 0)) + 'px'
  450. };
  451. }
  452. }
  453. return {};
  454. },
  455. fixedBodyHeight() {
  456. if (this.height) {
  457. return {
  458. height: this.layout.fixedBodyHeight ? this.layout.fixedBodyHeight + 'px' : ''
  459. };
  460. } else if (this.maxHeight) {
  461. let maxHeight = parseHeight(this.maxHeight);
  462. if (typeof maxHeight === 'number') {
  463. maxHeight = this.layout.scrollX ? maxHeight - this.layout.gutterWidth : maxHeight;
  464. if (this.showHeader) {
  465. maxHeight -= this.layout.headerHeight;
  466. }
  467. maxHeight -= this.layout.footerHeight;
  468. return {
  469. 'max-height': maxHeight + 'px'
  470. };
  471. }
  472. }
  473. return {};
  474. },
  475. fixedHeight() {
  476. if (this.maxHeight) {
  477. if (this.showSummary) {
  478. return {
  479. bottom: 0
  480. };
  481. }
  482. return {
  483. bottom: (this.layout.scrollX && this.data.length) ? this.layout.gutterWidth + 'px' : ''
  484. };
  485. } else {
  486. if (this.showSummary) {
  487. return {
  488. height: this.layout.tableHeight ? this.layout.tableHeight + 'px' : ''
  489. };
  490. }
  491. return {
  492. height: this.layout.viewportHeight ? this.layout.viewportHeight + 'px' : ''
  493. };
  494. }
  495. },
  496. emptyBlockStyle() {
  497. if (this.data && this.data.length) return null;
  498. let height = '100%';
  499. if (this.layout.appendHeight) {
  500. height = `calc(100% - ${this.layout.appendHeight}px)`;
  501. }
  502. return {
  503. width: this.bodyWidth,
  504. height
  505. };
  506. },
  507. ...mapStates({
  508. selection: 'selection',
  509. columns: 'columns',
  510. tableData: 'data',
  511. fixedColumns: 'fixedColumns',
  512. rightFixedColumns: 'rightFixedColumns'
  513. })
  514. },
  515. watch: {
  516. height: {
  517. immediate: true,
  518. handler(value) {
  519. this.layout.setHeight(value);
  520. }
  521. },
  522. maxHeight: {
  523. immediate: true,
  524. handler(value) {
  525. this.layout.setMaxHeight(value);
  526. }
  527. },
  528. currentRowKey: {
  529. immediate: true,
  530. handler(value) {
  531. if (!this.rowKey) return;
  532. this.store.setCurrentRowKey(value);
  533. }
  534. },
  535. data: {
  536. immediate: true,
  537. handler(value) {
  538. this.store.commit('setData', value);
  539. }
  540. },
  541. expandRowKeys: {
  542. immediate: true,
  543. handler(newVal) {
  544. if (newVal) {
  545. this.store.setExpandRowKeysAdapter(newVal);
  546. }
  547. }
  548. }
  549. },
  550. created() {
  551. this.tableId = 'el-table_' + tableIdSeed++;
  552. this.debouncedUpdateLayout = debounce(50, () => this.doLayout());
  553. },
  554. mounted() {
  555. this.bindEvents();
  556. this.store.updateColumns();
  557. this.doLayout();
  558. this.resizeState = {
  559. width: this.$el.offsetWidth,
  560. height: this.$el.offsetHeight
  561. };
  562. // init filters
  563. this.store.states.columns.forEach(column => {
  564. if (column.filteredValue && column.filteredValue.length) {
  565. this.store.commit('filterChange', {
  566. column,
  567. values: column.filteredValue,
  568. silent: true
  569. });
  570. }
  571. });
  572. this.$ready = true;
  573. },
  574. destroyed() {
  575. this.unbindEvents();
  576. },
  577. data() {
  578. const { hasChildren = 'hasChildren', children = 'children' } = this.treeProps;
  579. this.store = createStore(this, {
  580. rowKey: this.rowKey,
  581. defaultExpandAll: this.defaultExpandAll,
  582. selectOnIndeterminate: this.selectOnIndeterminate,
  583. // TreeTable 的相关配置
  584. indent: this.indent,
  585. lazy: this.lazy,
  586. lazyColumnIdentifier: hasChildren,
  587. childrenColumnName: children
  588. });
  589. const layout = new TableLayout({
  590. store: this.store,
  591. table: this,
  592. fit: this.fit,
  593. showHeader: this.showHeader
  594. });
  595. return {
  596. layout,
  597. isHidden: false,
  598. renderExpanded: null,
  599. resizeProxyVisible: false,
  600. resizeState: {
  601. width: null,
  602. height: null
  603. },
  604. // 是否拥有多级表头
  605. isGroup: false,
  606. scrollPosition: 'left'
  607. };
  608. }
  609. };
  610. </script>