u-table2.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. <template>
  2. <view scroll-x class="u-table2" :style="{ height: height ? height + 'px' : 'auto' }">
  3. <!-- 表头 -->
  4. <view v-if="showHeader" class="u-table-header" :class="{ 'u-table-sticky': fixedHeader }" :style="{minWidth: scrollWidth}">
  5. <view class="u-table-row">
  6. <view v-for="(col, colIndex) in columns" :key="col.key" class="u-table-cell"
  7. :style="headerColStyle(col)"
  8. :class="[
  9. col.align ? 'u-text-' + col.align : '',
  10. headerCellClassName ? headerCellClassName(col) : '',
  11. col.fixed === 'left' ? 'u-table-fixed-left' : '',
  12. col.fixed === 'right' ? 'u-table-fixed-right' : ''
  13. ]" @click="handleHeaderClick(col)">
  14. <slot name="header" :column="col" :columnIndex="colIndex" :level="1">
  15. {{ col.title }}
  16. </slot>
  17. <view v-if="col.sortable">
  18. {{ getSortIcon(col.key) }}
  19. </view>
  20. </view>
  21. </view>
  22. </view>
  23. <!-- 表体 -->
  24. <view class="u-table-body" :style="{ minWidth: scrollWidth, maxHeight: maxHeight ? maxHeight + 'px' : 'none' }">
  25. <template v-if="data && data.length > 0">
  26. <template v-for="(row, index) in sortedData" :key="row[rowKey] || index">
  27. <view class="u-table-row" :class="[
  28. highlightCurrentRow && currentRow === row ? 'u-table-row-highlight' : '',
  29. rowClassName ? rowClassName(row, index) : '',
  30. stripe && index % 2 === 1 ? 'u-table-row-zebra' : ''
  31. ]" @click="handleRowClick(row)">
  32. <view v-for="(col, colIndex) in columns" :key="col.key"
  33. class="u-table-cell" :class="[
  34. col.align ? 'u-text-' + col.align : '',
  35. cellClassName ? cellClassName(row, col) : '',
  36. col.fixed === 'left' ? 'u-table-fixed-left' : '',
  37. col.fixed === 'right' ? 'u-table-fixed-right' : ''
  38. ]" :style="cellStyleInner({row: row, column: col,
  39. rowIndex: index, columnIndex: colIndex, level: 0})">
  40. <!-- 复选框列 -->
  41. <view v-if="col.type === 'selection'">
  42. <checkbox :checked="isSelected(row)"
  43. @click.stop="toggleSelect(row)" />
  44. </view>
  45. <!-- 树形结构展开图标 -->
  46. <view v-else-if="col.type === 'expand'"
  47. @click.stop="toggleExpand(row)">
  48. {{ isExpanded(row) ? '▼' : '▶' }}
  49. </view>
  50. <!-- 默认插槽或文本 -->
  51. <slot name="cell" :row="row" :column="col"
  52. :rowIndex="index" :columnIndex="colIndex">
  53. <view class="u-table-cell_content">
  54. {{ row[col.key] }}
  55. </view>
  56. </slot>
  57. </view>
  58. </view>
  59. <!-- 子级渲染 -->
  60. <template v-if="isExpanded(row) && row[treeProps.children] && row[treeProps.children].length">
  61. <view v-for="childRow in row[treeProps.children]" :key="childRow[rowKey]"
  62. class="u-table-row u-table-row-child">
  63. <view v-for="(col2, col2Index) in columns" :key="col2.key" class="u-table-cell"
  64. :style="cellStyleInner({row: childRow, column: col2,
  65. rowIndex: index, columnIndex: col2Index, level: 1})">
  66. <slot name="cell" :row="childRow" :column="col2" :prow="row"
  67. :rowIndex="index" :columnIndex="col2Index" :level="1">
  68. <view class="u-table-cell_content">
  69. {{ childRow[col2.key] }}
  70. </view>
  71. </slot>
  72. </view>
  73. </view>
  74. </template>
  75. </template>
  76. </template>
  77. <template v-else>
  78. <slot name="empty">
  79. <view class="u-table-empty">{{ emptyText }}</view>
  80. </slot>
  81. </template>
  82. </view>
  83. </view>
  84. </template>
  85. <script>
  86. import { ref, watch, computed } from 'vue'
  87. import { addUnit, sleep } from '../../libs/function/index';
  88. export default {
  89. name: 'u-table2',
  90. props: {
  91. data: {
  92. type: Array,
  93. required: true,
  94. default: () => {
  95. return []
  96. }
  97. },
  98. columns: {
  99. type: Array,
  100. required: true,
  101. default: () => {
  102. return []
  103. },
  104. validator: cols =>
  105. cols.every(col =>
  106. ['default', 'selection', 'expand'].includes(col.type || 'default')
  107. )
  108. },
  109. stripe: {
  110. type: Boolean,
  111. default: false
  112. },
  113. border: {
  114. type: Boolean,
  115. default: false
  116. },
  117. height: {
  118. type: [String, Number],
  119. default: null
  120. },
  121. maxHeight: {
  122. type: [String, Number],
  123. default: null
  124. },
  125. showHeader: {
  126. type: Boolean,
  127. default: true
  128. },
  129. highlightCurrentRow: {
  130. type: Boolean,
  131. default: false
  132. },
  133. rowKey: {
  134. type: String,
  135. default: 'id'
  136. },
  137. currentRowKey: {
  138. type: [String, Number],
  139. default: null
  140. },
  141. rowStyle: {
  142. type: Object,
  143. default: () => ({})
  144. },
  145. cellClassName: {
  146. type: Function,
  147. default: null
  148. },
  149. cellStyle: {
  150. type: Function,
  151. default: null
  152. },
  153. headerCellClassName: {
  154. type: Function,
  155. default: null
  156. },
  157. rowClassName: {
  158. type: Function,
  159. default: null
  160. },
  161. context: {
  162. type: Object,
  163. default: null
  164. },
  165. showOverflowTooltip: {
  166. type: Boolean,
  167. default: false
  168. },
  169. lazy: {
  170. type: Boolean,
  171. default: false
  172. },
  173. load: {
  174. type: Function,
  175. default: null
  176. },
  177. treeProps: {
  178. type: Object,
  179. default: () => ({
  180. children: 'children',
  181. hasChildren: 'hasChildren'
  182. })
  183. },
  184. defaultExpandAll: {
  185. type: Boolean,
  186. default: false
  187. },
  188. expandRowKeys: {
  189. type: Array,
  190. default: () => []
  191. },
  192. sortOrders: {
  193. type: Array,
  194. default: () => ['ascending', 'descending']
  195. },
  196. sortable: {
  197. type: [Boolean, String],
  198. default: false
  199. },
  200. multiSort: {
  201. type: Boolean,
  202. default: false
  203. },
  204. sortBy: {
  205. type: String,
  206. default: null
  207. },
  208. sortMethod: {
  209. type: Function,
  210. default: null
  211. },
  212. filters: {
  213. type: Object,
  214. default: () => ({})
  215. },
  216. fixedHeader: {
  217. type: Boolean,
  218. default: true
  219. },
  220. emptyText: {
  221. type: String,
  222. default: '暂无数据'
  223. },
  224. },
  225. emits: [
  226. 'select', 'select-all', 'selection-change',
  227. 'cell-click', 'row-click', 'row-dblclick',
  228. 'header-click', 'sort-change', 'filter-change',
  229. 'current-change', 'expand-change'
  230. ],
  231. data() {
  232. return {
  233. scrollWidth: 'auto'
  234. }
  235. },
  236. mounted() {
  237. this.getComponentWidth()
  238. },
  239. computed: {
  240. },
  241. methods: {
  242. addUnit,
  243. headerColStyle(col) {
  244. let style = {
  245. width: col.width ? addUnit(col.width) : 'auto',
  246. flex: col.width ? 'none' : 1
  247. };
  248. if (col?.style) {
  249. style = {...style, ...col?.style};
  250. }
  251. return style;
  252. },
  253. setCellStyle(e) {
  254. this.cellStyle = e
  255. },
  256. cellStyleInner(scope) {
  257. let style = {
  258. width: scope.column?.width ? addUnit(scope.column.width) : 'auto',
  259. flex: scope.column?.width ? 'none' : 1,
  260. paddingLeft: (24 * scope.level) + 'px'
  261. };
  262. if (this.cellStyle != null) {
  263. let styleCalc = this.cellStyle(scope)
  264. if (styleCalc != null) {
  265. style = {...style, ...styleCalc}
  266. }
  267. }
  268. return style;
  269. },
  270. // 获取组件的宽度
  271. async getComponentWidth() {
  272. // 延时一定时间,以获取dom尺寸
  273. await sleep(30)
  274. this.$uGetRect('.u-table-row').then(size => {
  275. this.scrollWidth = size.width + 'px'
  276. })
  277. },
  278. },
  279. setup(props, { emit }) {
  280. const expandedKeys = ref([...props.expandRowKeys]);
  281. const selectedRows = ref([]);
  282. const sortConditions = ref([]);
  283. // 当前高亮行
  284. const currentRow = ref(null);
  285. watch(
  286. () => props.expandRowKeys,
  287. newVal => {
  288. expandedKeys.value = [...newVal];
  289. }
  290. );
  291. watch(
  292. () => props.currentRowKey,
  293. newVal => {
  294. const found = props.data.find(item => item[props.rowKey] === newVal);
  295. if (found) {
  296. currentRow.value = found;
  297. }
  298. }
  299. );
  300. // 过滤后的数据
  301. const filteredData = computed(() => {
  302. return props.data.filter(row => {
  303. return Object.keys(props.filters).every(key => {
  304. const filter = props.filters[key];
  305. if (!filter) return true;
  306. return row[key]?.toString().includes(filter.toString());
  307. });
  308. });
  309. });
  310. // 排序后的数据
  311. const sortedData = computed(() => {
  312. if (!sortConditions.value.length) return filteredData.value;
  313. const data = [...filteredData.value];
  314. return data.sort((a, b) => {
  315. for (const condition of sortConditions.value) {
  316. const { field, order } = condition;
  317. let valA = a[field];
  318. let valB = b[field];
  319. if (props.sortMethod) {
  320. const result = props.sortMethod(a, b, field);
  321. if (result !== 0) return result * (order === 'ascending' ? 1 : -1);
  322. }
  323. if (valA < valB) return order === 'ascending' ? -1 : 1;
  324. if (valA > valB) return order === 'ascending' ? 1 : -1;
  325. }
  326. return 0;
  327. });
  328. });
  329. function handleRowClick(row) {
  330. if (props.highlightCurrentRow) {
  331. const oldRow = currentRow.value;
  332. currentRow.value = row;
  333. emit('current-change', row, oldRow);
  334. }
  335. emit('row-click', row);
  336. }
  337. function handleHeaderClick(column) {
  338. if (!column.sortable) return;
  339. const index = sortConditions.value.findIndex(c => c.field === column.key);
  340. let newOrder = 'ascending';
  341. if (index >= 0) {
  342. if (sortConditions.value[index].order === 'ascending') {
  343. newOrder = 'descending';
  344. } else {
  345. sortConditions.value.splice(index, 1);
  346. emit('sort-change', sortConditions.value);
  347. return;
  348. }
  349. }
  350. if (!props.multiSort) {
  351. sortConditions.value = [{ field: column.key, order: newOrder }];
  352. } else {
  353. if (index >= 0) {
  354. sortConditions.value[index].order = newOrder;
  355. } else {
  356. sortConditions.value.push({ field: column.key, order: newOrder });
  357. }
  358. }
  359. emit('sort-change', sortConditions.value);
  360. }
  361. function getSortIcon(field) {
  362. const cond = sortConditions.value.find(c => c.field === field);
  363. if (!cond) return '';
  364. return cond.order === 'ascending' ? '↑' : '↓';
  365. }
  366. function toggleSelect(row) {
  367. const index = selectedRows.value.findIndex(r => r[props.rowKey] === row[props.rowKey]);
  368. if (index >= 0) {
  369. selectedRows.value.splice(index, 1);
  370. } else {
  371. selectedRows.value.push(row);
  372. }
  373. emit('selection-change', selectedRows.value);
  374. emit('select', row);
  375. }
  376. function isSelected(row) {
  377. return selectedRows.value.some(r => r[props.rowKey] === row[props.rowKey]);
  378. }
  379. function toggleExpand(row) {
  380. const key = row[props.rowKey];
  381. const index = expandedKeys.value.indexOf(key);
  382. if (index === -1) {
  383. expandedKeys.value.push(key);
  384. } else {
  385. expandedKeys.value.splice(index, 1);
  386. }
  387. emit('expand-change', expandedKeys.value);
  388. }
  389. function isExpanded(row) {
  390. return expandedKeys.value.includes(row[props.rowKey]);
  391. }
  392. return {
  393. currentRow,
  394. sortedData,
  395. expandedKeys,
  396. selectedRows,
  397. sortConditions,
  398. handleRowClick,
  399. handleHeaderClick,
  400. getSortIcon,
  401. toggleSelect,
  402. isSelected,
  403. toggleExpand,
  404. isExpanded
  405. };
  406. }
  407. };
  408. </script>
  409. <style lang="scss" scoped>
  410. .u-table2 {
  411. width: auto;
  412. overflow: auto;
  413. white-space: nowrap;
  414. .u-table-header {
  415. min-width: 100% !important;
  416. width: fit-content;
  417. background-color: #f5f7fa;
  418. }
  419. .u-table-body {
  420. min-width: 100% !important;
  421. width: fit-content;
  422. }
  423. .u-table-sticky {
  424. position: sticky;
  425. top: 0;
  426. z-index: 10;
  427. }
  428. .u-table-row {
  429. display: flex;
  430. flex-direction: row;
  431. align-items: center;
  432. border-bottom: 1rpx solid #ebeef5;
  433. overflow: hidden;
  434. }
  435. .u-table-cell {
  436. flex: 1;
  437. display: flex;
  438. flex-direction: row;
  439. padding: 5px 4px;
  440. font-size: 14px;
  441. white-space: nowrap;
  442. overflow: hidden;
  443. text-overflow: ellipsis;
  444. }
  445. .u-table-fixed-left {
  446. position: sticky;
  447. left: 0;
  448. z-index: 9;
  449. }
  450. .u-table-fixed-right {
  451. position: sticky;
  452. right: 0;
  453. z-index: 9;
  454. }
  455. .u-table-row-zebra {
  456. background-color: #fafafa;
  457. }
  458. .u-table-row-highlight {
  459. background-color: #f5f7fa;
  460. }
  461. .u-table-empty {
  462. text-align: center;
  463. padding: 20px;
  464. color: #999;
  465. }
  466. .u-text-left {
  467. text-align: left;
  468. }
  469. .u-text-center {
  470. text-align: center;
  471. }
  472. .u-text-right {
  473. text-align: right;
  474. }
  475. }
  476. </style>