u-table-column.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. <template>
  2. <view v-if="show" class="u-table__column" :style="[columnStyle]">
  3. <block v-for="(row, index) in tableData" :key="index">
  4. <view class="u-table__cell" :style="[cellStyles(row, index)]" :class="[cellClasses(row, index)]"
  5. @click="handleCellClick(row, index)" v-if="!isCellHidden(row, index)">
  6. <view class="u-table__value" :class="{ 'is-ellipsis': isEllipsis }">
  7. <slot :row="row" :column="columnConfig" :index="index" :value="row">
  8. <template v-if="type === 'index'">
  9. {{ index + 1 }}
  10. </template>
  11. <template v-else-if="type === 'selection'">
  12. <u-checkbox :checked="isSelectionSelected(row)"
  13. @change="(checked) => handleSelectionChange(row, checked)"></u-checkbox>
  14. </template>
  15. <template v-else>
  16. {{ getCellValue(row) }}
  17. </template>
  18. </slot>
  19. </view>
  20. </view>
  21. </block>
  22. </view>
  23. </template>
  24. <script>
  25. import props from './props.js';
  26. import mixin from '../../libs/mixin/mixin'
  27. import mpMixin from '../../libs/mixin/mpMixin';
  28. /**
  29. * table-col 表格列
  30. * @description 表格列组件,需要与u-table组件配合使用
  31. * @tutorial https://www.uviewui.com/components/table.html
  32. * @property {String} prop 字段名称,对应列内容的字段名 (默认 '' )
  33. * @property {String} label 显示的标题 (默认 '' )
  34. * @property {String|Number} width 列宽度 (默认 '' )
  35. * @property {String|Number} minWidth 列最小宽度 (默认 '' )
  36. * @property {Boolean} sortable 是否可排序 (默认 false )
  37. * @property {Boolean} fixed 是否固定列 (默认 false )
  38. * @property {String} align 对齐方式 (默认 'left' )
  39. * @property {String} type 列类型:selection/index (默认 '' )
  40. * @property {Function} formatter 格式化函数 (默认 null )
  41. * @property {Boolean} show 是否显示 (默认 true )
  42. * @property {String} headerAlign 表头对齐方式 (默认 'left' )
  43. * @property {Boolean} headerEllipsis 表头超出1行隐藏 (默认 false )
  44. * @property {String} columnKey 列的key (默认 '' )
  45. * @property {String} className 列的类名 (默认 '' )
  46. * @property {Object} customStyle 定义需要用到的外部样式
  47. * @example <u-table-column prop="name" label="姓名" width="100"></u-table-column>
  48. */
  49. export default {
  50. name: 'u-table-column',
  51. mixins: [mpMixin, mixin, props],
  52. data() {
  53. return {
  54. parentData: {
  55. data: [],
  56. border: false,
  57. stripe: false,
  58. rowHeight: null,
  59. ellipsis: true,
  60. cellStyle: {},
  61. selectedRowKeys: [],
  62. toggleRowSelection: null,
  63. mergeInfo: {}
  64. },
  65. sortDirection: 0,
  66. innerFormatter: (value) => value
  67. };
  68. },
  69. computed: {
  70. columnConfig() {
  71. return {
  72. prop: this.prop,
  73. type: this.type,
  74. label: this.label,
  75. width: this.width,
  76. minWidth: this.minWidth,
  77. sortable: this.sortable,
  78. align: this.align,
  79. type: this.type,
  80. formatter: this.formatter,
  81. sortDirection: this.sortDirection
  82. };
  83. },
  84. tableData() {
  85. return this.parentData.data || [];
  86. },
  87. isEllipsis() {
  88. return this.parentData.ellipsis;
  89. },
  90. columnStyle() {
  91. const style = {};
  92. if (this.width) {
  93. style.width = this.$u.addUnit(this.width);
  94. } else if (this.minWidth) {
  95. style.minWidth = this.$u.addUnit(this.minWidth);
  96. } else {
  97. style.flex = 1;
  98. }
  99. return style;
  100. },
  101. // 响应式的行选中状态映射
  102. isSelectionSelected() {
  103. return (row) => {
  104. if (this.parentData.selectedRowKeys && this.parentData.selectedRowKeys.length > 0) {
  105. const rowKey = this.getRowKey(row);
  106. return this.parentData.selectedRowKeys.includes(rowKey);
  107. }
  108. return false;
  109. }
  110. },
  111. cellStyles(row, index) {
  112. return (row, index) => {
  113. const style = {};
  114. if (this.parentData.rowHeight) {
  115. style.height = this.$u.addUnit(this.parentData.rowHeight) ;
  116. }
  117. // 处理合并单元格样式
  118. if (this.parentData.mergeInfo && this.prop) {
  119. const mergeKey = `${index}-${this.prop}`;
  120. const mergeData = this.parentData.mergeInfo[mergeKey];
  121. if (mergeData) {
  122. // 设置合并单元格的高度(考虑边框和像素比例)
  123. if (mergeData.rowspan > 1) {
  124. let rowHeight = parseInt(this.parentData.rowHeight);
  125. let pixelRatio = uni.$u.window().pixelRatio;
  126. // 基础高度:行高 × 合并行数
  127. let totalHeight = rowHeight * mergeData.rowspan;
  128. // 如果开启了边框,需要加上被合并行之间的边框高度
  129. if (this.parentData.border) {
  130. // 边框宽度需要考虑像素比例,避免出现小数像素
  131. const borderWidth = Math.round(1 * pixelRatio) / pixelRatio;
  132. //保留1位小数
  133. totalHeight += ((mergeData.rowspan - 1) * borderWidth);
  134. }
  135. style.height = this.$u.addUnit(totalHeight);
  136. }
  137. // 设置显示状态
  138. if (mergeData.display === 'none') {
  139. style.display = 'none';
  140. }
  141. }
  142. }
  143. return uni.$u.deepMerge(style, uni.$u.addStyle(this.parentData.cellStyle));
  144. }
  145. },
  146. cellClasses(row, index) {
  147. return (row, index) => {
  148. let classes = [
  149. this.className,
  150. `is-${this.align || 'left'}`,
  151. ];
  152. if (this.parentData.border) {
  153. classes.push('is-border');
  154. }
  155. if (this.parentData.fixed) {
  156. classes.push('is-fixed');
  157. }
  158. if (this.parentData.stripe && index % 2 === 1) {
  159. classes.push('is-stripe');
  160. }
  161. // 添加合并单元格的样式类
  162. if (this.parentData.mergeInfo && this.prop) {
  163. const mergeKey = `${index}-${this.prop}`;
  164. const mergeData = this.parentData.mergeInfo[mergeKey];
  165. if (mergeData && mergeData.rowspan > 1) {
  166. classes.push('is-merged');
  167. }
  168. }
  169. // #ifdef MP-ALIPAY || MP-TOUTIAO
  170. classes = classes.join(' ')
  171. // #endif
  172. return classes;
  173. }
  174. },
  175. },
  176. mounted() {
  177. this.init()
  178. },
  179. // #ifdef VUE3
  180. emits: ['cellClick'],
  181. // #endif
  182. methods: {
  183. init() {
  184. this.updateParentData();
  185. if (!this.parent) {
  186. uni.$u.error('u-table-column必须搭配u-table组件使用');
  187. return;
  188. }
  189. },
  190. updateParentData() {
  191. this.getParentData('u-table');
  192. },
  193. // 获取行的唯一标识(与父组件保持一致)
  194. getRowKey(row) {
  195. if (!row) return '';
  196. // 这里需要与父组件的rowKey属性保持一致
  197. const rowKeyProp = this.parent.rowKey || 'id';
  198. return this.getValueByPath(row, rowKeyProp) || JSON.stringify(row);
  199. },
  200. // 获取嵌套属性值
  201. getValueByPath(obj, path) {
  202. if (!path || !obj) return '';
  203. const keys = path.split('.');
  204. let value = obj;
  205. for (const key of keys) {
  206. if (value && typeof value === 'object') {
  207. value = value[key];
  208. } else {
  209. value = '';
  210. break;
  211. }
  212. }
  213. return value;
  214. },
  215. // 处理选择状态变化
  216. handleSelectionChange(row, checked) {
  217. if (typeof this.parentData.toggleRowSelection === 'function') {
  218. this.parentData.toggleRowSelection(row, checked);
  219. }
  220. },
  221. // 处理展开点击
  222. handleExpandClick(row) {
  223. if (typeof this.parentData.toggleRowExpansion === 'function') {
  224. this.parentData.toggleRowExpansion(row);
  225. }
  226. },
  227. handleCellClick(row, rowIndex) {
  228. // 如果是selection或expand类型,不触发cell-click事件
  229. if (this.type === 'selection' || this.type === 'expand') {
  230. return;
  231. }
  232. const cellData = {
  233. row,
  234. column: this.columnConfig,
  235. rowIndex,
  236. columnIndex: this.getColumnIndex(),
  237. value: this.getCellValue(row)
  238. };
  239. this.$emit('cellClick', cellData);
  240. if (this.parent) {
  241. this.parent.$emit('cellClick', cellData);
  242. }
  243. },
  244. getCellValue(row) {
  245. if (!this.prop) return '';
  246. const keys = this.prop.split('.');
  247. let value = row;
  248. for (const key of keys) {
  249. if (value && typeof value === 'object') {
  250. value = value[key];
  251. } else {
  252. value = '';
  253. break;
  254. }
  255. }
  256. const formatter = this.formatter || this.innerFormatter;
  257. return formatter(value);
  258. },
  259. getColumnIndex() {
  260. if (!this.parent || !this.parent.children) return 0;
  261. return this.parent.children.findIndex(col => col.prop === this.prop);
  262. },
  263. isCellHidden(row, index) {
  264. // 检查合并信息,如果该单元格被合并到其他单元格中,则隐藏
  265. if (this.parentData.mergeInfo && this.prop) {
  266. const mergeKey = `${index}-${this.prop}`;
  267. const mergeData = this.parentData.mergeInfo[mergeKey];
  268. if (mergeData && mergeData.display === 'none') {
  269. return true;
  270. }
  271. }
  272. return false;
  273. }
  274. }
  275. };
  276. </script>
  277. <style lang="scss" scoped>
  278. @import '../../libs/css/components.scss';
  279. .u-table {
  280. &__column {
  281. @include flex(column);
  282. flex-shrink: 0;
  283. &:last-child .is-border {
  284. border-right: none;
  285. }
  286. }
  287. &__cell {
  288. @include flex(column);
  289. padding: 0px 10px;
  290. align-items: center;
  291. justify-content: center;
  292. position: relative;
  293. &.is-left {
  294. align-items: flex-start;
  295. }
  296. &.is-center {
  297. align-items: center;
  298. }
  299. &.is-right {
  300. align-items: flex-end;
  301. }
  302. &.is-border {
  303. border-right: 1px solid $u-border-color;
  304. border-bottom: 1px solid $u-border-color;
  305. &:last-child {
  306. border-bottom: none;
  307. }
  308. }
  309. &.is-stripe {
  310. background-color: $u-bg-color;
  311. }
  312. &.is-merged {
  313. position: relative;
  314. z-index: 1;
  315. background-color: inherit;
  316. }
  317. }
  318. &__value {
  319. font-size: 13px;
  320. line-height: 15px;
  321. &.is-ellipsis {
  322. word-break: break-all;
  323. display: -webkit-box;
  324. -webkit-box-orient: vertical;
  325. -webkit-line-clamp: 2;
  326. overflow: hidden;
  327. }
  328. }
  329. &__expand-icon {
  330. @include flex(row);
  331. align-items: center;
  332. justify-content: center;
  333. width: 20px;
  334. height: 20px;
  335. cursor: pointer;
  336. transition: transform 0.2s ease;
  337. &.is-expanded {
  338. transform: rotate(90deg);
  339. }
  340. &:hover {
  341. background-color: rgba(0, 0, 0, 0.05);
  342. border-radius: 2px;
  343. }
  344. }
  345. }
  346. </style>