u-table2.vue 15 KB

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