djm-ui 0.0.0-stage → 0.67.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/CHANGELOG.md +715 -0
  2. package/LICENSE +21 -0
  3. package/README.md +183 -2
  4. package/dist/djm-ui.cjs +15521 -0
  5. package/dist/djm-ui.cjs.map +7 -0
  6. package/dist/djm-ui.css +11817 -0
  7. package/dist/djm-ui.esm.js +15371 -0
  8. package/dist/djm-ui.esm.js.map +7 -0
  9. package/dist/djm-ui.esm.min.js +4059 -0
  10. package/dist/djm-ui.min.cjs +4059 -0
  11. package/dist/djm-ui.min.css +2 -0
  12. package/dist/djm-ui.umd.js +15331 -0
  13. package/dist/djm-ui.umd.min.js +4059 -0
  14. package/package.json +90 -4
  15. package/src/components/affix/index.js +98 -0
  16. package/src/components/alert/index.js +43 -0
  17. package/src/components/anchor/index.js +107 -0
  18. package/src/components/approval-flow/index.js +96 -0
  19. package/src/components/attachment-list/index.js +155 -0
  20. package/src/components/autocomplete/index.js +187 -0
  21. package/src/components/avatar/group.js +22 -0
  22. package/src/components/avatar/index.js +47 -0
  23. package/src/components/backtop/index.js +82 -0
  24. package/src/components/badge/index.js +43 -0
  25. package/src/components/batch-bar/index.js +52 -0
  26. package/src/components/breadcrumb/index.js +45 -0
  27. package/src/components/button/index.js +45 -0
  28. package/src/components/calendar/index.js +180 -0
  29. package/src/components/card/index.js +24 -0
  30. package/src/components/carousel/index.js +136 -0
  31. package/src/components/cascader/index.js +157 -0
  32. package/src/components/chat/index.js +157 -0
  33. package/src/components/checkbox/index.js +109 -0
  34. package/src/components/collapse/index.js +55 -0
  35. package/src/components/color-picker/index.js +133 -0
  36. package/src/components/command-palette/index.js +182 -0
  37. package/src/components/comment/index.js +149 -0
  38. package/src/components/config-provider/index.js +41 -0
  39. package/src/components/confirm-dialog/index.js +66 -0
  40. package/src/components/context-menu/index.js +148 -0
  41. package/src/components/date-picker/index.js +448 -0
  42. package/src/components/date-time-picker/index.js +376 -0
  43. package/src/components/descriptions/index.js +47 -0
  44. package/src/components/diff/index.js +172 -0
  45. package/src/components/divider/index.js +25 -0
  46. package/src/components/drawer/index.js +131 -0
  47. package/src/components/dropdown/index.js +236 -0
  48. package/src/components/editable-table/index.js +264 -0
  49. package/src/components/empty/index.js +31 -0
  50. package/src/components/field-list/index.js +221 -0
  51. package/src/components/filter-tags/index.js +33 -0
  52. package/src/components/float-button/index.js +97 -0
  53. package/src/components/floating-window/index.js +171 -0
  54. package/src/components/form/index.js +130 -0
  55. package/src/components/form-item/index.js +71 -0
  56. package/src/components/gantt/index.js +192 -0
  57. package/src/components/grid/col.js +21 -0
  58. package/src/components/grid/row.js +16 -0
  59. package/src/components/icon/index.js +87 -0
  60. package/src/components/image/index.js +110 -0
  61. package/src/components/import-wizard/index.js +306 -0
  62. package/src/components/input/index.js +117 -0
  63. package/src/components/input-number/index.js +106 -0
  64. package/src/components/input-tag/index.js +117 -0
  65. package/src/components/kanban/index.js +223 -0
  66. package/src/components/layout/content.js +5 -0
  67. package/src/components/layout/footer.js +5 -0
  68. package/src/components/layout/header.js +5 -0
  69. package/src/components/layout/index.js +15 -0
  70. package/src/components/layout/sider.js +13 -0
  71. package/src/components/list/index.js +31 -0
  72. package/src/components/media-picker/index.js +206 -0
  73. package/src/components/mentions/index.js +193 -0
  74. package/src/components/menu/index.js +248 -0
  75. package/src/components/message/index.js +56 -0
  76. package/src/components/modal/index.js +132 -0
  77. package/src/components/notification/index.js +140 -0
  78. package/src/components/org-chart/index.js +190 -0
  79. package/src/components/org-picker/index.js +414 -0
  80. package/src/components/page-header/index.js +24 -0
  81. package/src/components/page-tabs/index.js +80 -0
  82. package/src/components/pagination/index.js +124 -0
  83. package/src/components/password/index.js +92 -0
  84. package/src/components/popconfirm/index.js +161 -0
  85. package/src/components/popover/index.js +142 -0
  86. package/src/components/print/index.js +101 -0
  87. package/src/components/progress/index.js +37 -0
  88. package/src/components/qrcode/index.js +75 -0
  89. package/src/components/query-filter/index.js +171 -0
  90. package/src/components/radio/index.js +83 -0
  91. package/src/components/rate/index.js +84 -0
  92. package/src/components/result/index.js +52 -0
  93. package/src/components/rich-editor/index.js +235 -0
  94. package/src/components/schedule/index.js +287 -0
  95. package/src/components/schema-form/index.js +222 -0
  96. package/src/components/segmented/index.js +40 -0
  97. package/src/components/select/index.js +614 -0
  98. package/src/components/signature/index.js +243 -0
  99. package/src/components/skeleton/index.js +35 -0
  100. package/src/components/slider/index.js +166 -0
  101. package/src/components/space/index.js +32 -0
  102. package/src/components/spin/index.js +42 -0
  103. package/src/components/splitter/index.js +110 -0
  104. package/src/components/statistic/index.js +41 -0
  105. package/src/components/status/index.js +31 -0
  106. package/src/components/steps/index.js +49 -0
  107. package/src/components/switch/index.js +40 -0
  108. package/src/components/table/index.js +847 -0
  109. package/src/components/tabs/index.js +136 -0
  110. package/src/components/tag/index.js +56 -0
  111. package/src/components/time-picker/index.js +165 -0
  112. package/src/components/timeline/index.js +24 -0
  113. package/src/components/tooltip/index.js +129 -0
  114. package/src/components/tour/index.js +144 -0
  115. package/src/components/transfer/index.js +145 -0
  116. package/src/components/tree/index.js +485 -0
  117. package/src/components/tree-select/index.js +406 -0
  118. package/src/components/tree-table/index.js +318 -0
  119. package/src/components/upload/index.js +407 -0
  120. package/src/components/virtual-list/index.js +178 -0
  121. package/src/components/watermark/index.js +82 -0
  122. package/src/index.js +356 -0
  123. package/src/locale/en-US.js +254 -0
  124. package/src/locale/index.js +32 -0
  125. package/src/locale/zh-CN.js +254 -0
  126. package/src/styles/base.css +101 -0
  127. package/src/styles/components/account.css +230 -0
  128. package/src/styles/components/affix.css +11 -0
  129. package/src/styles/components/anchor.css +56 -0
  130. package/src/styles/components/approval-flow.css +196 -0
  131. package/src/styles/components/attachment-list.css +172 -0
  132. package/src/styles/components/autocomplete.css +42 -0
  133. package/src/styles/components/avatar.css +42 -0
  134. package/src/styles/components/backtop.css +33 -0
  135. package/src/styles/components/badge.css +72 -0
  136. package/src/styles/components/batch-bar.css +51 -0
  137. package/src/styles/components/breadcrumb.css +74 -0
  138. package/src/styles/components/button.css +237 -0
  139. package/src/styles/components/calendar.css +99 -0
  140. package/src/styles/components/card.css +74 -0
  141. package/src/styles/components/carousel.css +119 -0
  142. package/src/styles/components/cascader.css +118 -0
  143. package/src/styles/components/charts.css +173 -0
  144. package/src/styles/components/chat.css +162 -0
  145. package/src/styles/components/color-picker.css +93 -0
  146. package/src/styles/components/command-palette.css +97 -0
  147. package/src/styles/components/comment.css +164 -0
  148. package/src/styles/components/composite.css +353 -0
  149. package/src/styles/components/console.css +288 -0
  150. package/src/styles/components/context-menu.css +98 -0
  151. package/src/styles/components/dashboard.css +245 -0
  152. package/src/styles/components/date-picker.css +258 -0
  153. package/src/styles/components/date-time-picker.css +239 -0
  154. package/src/styles/components/demo.css +432 -0
  155. package/src/styles/components/descriptions.css +45 -0
  156. package/src/styles/components/diff.css +178 -0
  157. package/src/styles/components/divider.css +32 -0
  158. package/src/styles/components/drawer.css +82 -0
  159. package/src/styles/components/dropdown.css +97 -0
  160. package/src/styles/components/feedback.css +270 -0
  161. package/src/styles/components/field-list.css +132 -0
  162. package/src/styles/components/filter-tags.css +60 -0
  163. package/src/styles/components/float-button.css +80 -0
  164. package/src/styles/components/form.css +688 -0
  165. package/src/styles/components/frame.css +193 -0
  166. package/src/styles/components/gantt.css +241 -0
  167. package/src/styles/components/grid.css +161 -0
  168. package/src/styles/components/image.css +107 -0
  169. package/src/styles/components/import-wizard.css +262 -0
  170. package/src/styles/components/input-number.css +58 -0
  171. package/src/styles/components/input-tag.css +73 -0
  172. package/src/styles/components/kanban.css +181 -0
  173. package/src/styles/components/layout-container.css +59 -0
  174. package/src/styles/components/layout.css +765 -0
  175. package/src/styles/components/list.css +73 -0
  176. package/src/styles/components/lookup.css +142 -0
  177. package/src/styles/components/media-picker.css +154 -0
  178. package/src/styles/components/mentions.css +50 -0
  179. package/src/styles/components/menu.css +200 -0
  180. package/src/styles/components/modal.css +97 -0
  181. package/src/styles/components/motion.css +62 -0
  182. package/src/styles/components/notification.css +179 -0
  183. package/src/styles/components/org-chart.css +131 -0
  184. package/src/styles/components/org-picker.css +341 -0
  185. package/src/styles/components/overlay.css +234 -0
  186. package/src/styles/components/page-tabs.css +101 -0
  187. package/src/styles/components/pagination.css +113 -0
  188. package/src/styles/components/panel.css +99 -0
  189. package/src/styles/components/password.css +74 -0
  190. package/src/styles/components/permission.css +118 -0
  191. package/src/styles/components/placeholder.css +90 -0
  192. package/src/styles/components/popconfirm.css +51 -0
  193. package/src/styles/components/popover.css +134 -0
  194. package/src/styles/components/print.css +20 -0
  195. package/src/styles/components/qrcode.css +23 -0
  196. package/src/styles/components/query-filter.css +82 -0
  197. package/src/styles/components/rate.css +41 -0
  198. package/src/styles/components/responsive.css +599 -0
  199. package/src/styles/components/result.css +62 -0
  200. package/src/styles/components/rich-editor.css +216 -0
  201. package/src/styles/components/schedule.css +243 -0
  202. package/src/styles/components/schema-form.css +18 -0
  203. package/src/styles/components/signature.css +46 -0
  204. package/src/styles/components/skin.css +108 -0
  205. package/src/styles/components/slider.css +77 -0
  206. package/src/styles/components/sort.css +93 -0
  207. package/src/styles/components/space.css +35 -0
  208. package/src/styles/components/spin.css +44 -0
  209. package/src/styles/components/splitter.css +66 -0
  210. package/src/styles/components/statistic.css +53 -0
  211. package/src/styles/components/steps.css +123 -0
  212. package/src/styles/components/table.css +852 -0
  213. package/src/styles/components/tabs.css +192 -0
  214. package/src/styles/components/tag.css +74 -0
  215. package/src/styles/components/template.css +409 -0
  216. package/src/styles/components/time-picker.css +100 -0
  217. package/src/styles/components/timeline.css +78 -0
  218. package/src/styles/components/toast.css +52 -0
  219. package/src/styles/components/tooltip.css +60 -0
  220. package/src/styles/components/tour.css +86 -0
  221. package/src/styles/components/transfer.css +104 -0
  222. package/src/styles/components/tree-table.css +78 -0
  223. package/src/styles/components/tree.css +568 -0
  224. package/src/styles/components/upload.css +274 -0
  225. package/src/styles/components/virtual-list.css +56 -0
  226. package/src/styles/components/watermark.css +13 -0
  227. package/src/styles/index.css +109 -0
  228. package/src/styles/theme-dark.css +78 -0
  229. package/src/styles/tokens.css +110 -0
  230. package/src/theme/index.js +186 -0
  231. package/src/utils/export.js +14 -0
  232. package/src/utils/focus-trap.js +45 -0
  233. package/src/utils/panel.js +73 -0
  234. package/src/utils/permission.js +52 -0
  235. package/src/utils/row-move.js +26 -0
  236. package/src/utils/sanitize-html.js +92 -0
  237. package/src/utils/scroll-lock.js +39 -0
  238. package/src/utils/text-diff.js +62 -0
  239. package/src/utils/tree-move.js +51 -0
  240. package/types/index.d.ts +1714 -0
  241. package/types/usage-check.ts +50 -0
  242. package/vendor/qrcode.esm.js +8 -0
@@ -0,0 +1,847 @@
1
+ import Empty from '../empty/index.js'
2
+ import Icon from '../icon/index.js'
3
+ import { t } from '../../locale/index.js'
4
+
5
+ /**
6
+ * 数据表格:列配置 + 具名插槽,替代各页面手写的表头/空状态/加载遮罩
7
+ *
8
+ * columns: [{ key, label, width, align, class, headerClass, cellClass, sortable, sorter, fixed, children }]
9
+ * children 用于多级表头,分组本身不参与排序与单元格渲染
10
+ * 单元格插槽:<template #cell-user="{ row, value, index }">...</template>
11
+ * 选择:v-model:selected="selected"(值为 row[rowKey])
12
+ */
13
+
14
+ // 是否配置了子列(空数组按普通列处理)
15
+ function hasChildren (column) {
16
+ return Array.isArray(column.children) && column.children.length > 0
17
+ }
18
+
19
+ // 一个节点下有多少叶子列,用于分组表头的 colspan
20
+ function leafCount (node) {
21
+ if (!hasChildren(node)) return 1
22
+ return node.children.reduce((sum, child) => sum + leafCount(child), 0)
23
+ }
24
+
25
+ export default {
26
+ name: 'DjmTable',
27
+ components: { DjmEmpty: Empty, DjmIcon: Icon },
28
+ props: {
29
+ columns: { type: Array, required: true },
30
+ rows: { type: Array, default: () => [] },
31
+ rowKey: { type: String, default: 'id' },
32
+ selected: { type: Array, default: () => [] },
33
+ selectable: { type: Boolean, default: false },
34
+ showIndex: { type: Boolean, default: false },
35
+ // 分页场景下用于让序号跨页连续
36
+ indexOffset: { type: Number, default: 0 },
37
+ loading: { type: Boolean, default: false },
38
+ loadingText: { type: String, default: '' },
39
+ striped: { type: Boolean, default: false },
40
+ compact: { type: Boolean, default: false },
41
+ emptyText: { type: String, default: '' },
42
+ emptyHint: { type: String, default: '' },
43
+ emptyIcon: { type: String, default: 'clipboard' },
44
+ // 排序受控值:列设置 sortable 后由组件回传,业务侧可 v-model 绑定
45
+ sortKey: { type: String, default: '' },
46
+ sortDir: { type: String, default: '' },
47
+ // 展开行:true 或 (row) => boolean
48
+ expandable: { type: [Boolean, Function], default: false },
49
+ expanded: { type: Array, default: () => [] },
50
+ // 行样式:字符串或 (row, index) => string
51
+ rowClass: { type: [String, Function], default: '' },
52
+ // 滚动区域高度:设置后表头吸顶,配合列 fixed 使用
53
+ maxHeight: { type: [Number, String], default: '' },
54
+ // 显示列设置按钮,可勾选要展示的列
55
+ columnSetting: { type: Boolean, default: false },
56
+ // 显示导出按钮,导出当前排序后的全部行
57
+ exportable: { type: Boolean, default: false },
58
+ // 导出文件名前缀,最终为 前缀-YYYY-MM-DD.csv
59
+ exportName: { type: String, default: 'export' },
60
+ // 表头右侧拖拽调整列宽,双击分割线恢复配置宽度
61
+ resizable: { type: Boolean, default: false },
62
+ // 拖拽时的最小列宽
63
+ minColumnWidth: { type: Number, default: 60 },
64
+ // 表格最小宽度,默认走样式里的 900px;列少或容器窄时可调小避免横向滚动
65
+ minWidth: { type: [Number, String], default: '' },
66
+ // 行拖拽排序:行首出现手柄,拖放后抛 row-drop 由业务重排数据
67
+ rowDraggable: { type: Boolean, default: false },
68
+ // 单行是否允许拖动,(row, index) => boolean
69
+ allowRowDrag: { type: Function, default: null },
70
+ // 虚拟滚动:只渲染可视区行,需要同时设置 max-height;展开行模式不支持
71
+ virtual: { type: Boolean, default: false },
72
+ // 虚拟滚动的行高,需与实际渲染高度一致,否则滚动位置会漂移
73
+ rowHeight: { type: Number, default: 44 },
74
+ // 可视区上下多渲染几行,避免快速滚动出现空白
75
+ overscan: { type: Number, default: 6 }
76
+ },
77
+ emits: [
78
+ 'update:selected',
79
+ 'row-click',
80
+ 'row-dblclick',
81
+ 'update:sortKey',
82
+ 'update:sortDir',
83
+ 'sort-change',
84
+ 'update:expanded',
85
+ 'export',
86
+ 'column-resize',
87
+ 'column-resize-end',
88
+ 'row-drop'
89
+ ],
90
+ data () {
91
+ return {
92
+ hiddenColumns: [],
93
+ settingOpen: false,
94
+ // 拖拽后的列宽:key -> px,未拖拽的列沿用 column.width
95
+ colWidths: {},
96
+ // 拖拽中的列,用于高亮分割线
97
+ resizeState: null,
98
+ // 行拖拽:起始行与当前落点
99
+ rowDragState: null,
100
+ rowDropState: null,
101
+ // 虚拟滚动状态
102
+ scrollTop: 0,
103
+ viewportHeight: 0
104
+ }
105
+ },
106
+ computed: {
107
+ // 去掉分组层级,按钮叶子列顺序铺平;单元格、排序、导出一律按叶子处理
108
+ leafColumns () {
109
+ const flat = []
110
+ const walk = (list, parent) => {
111
+ list.forEach((column) => {
112
+ if (hasChildren(column)) {
113
+ walk(column.children, column)
114
+ return
115
+ }
116
+ // 带上分组信息,列设置里能看出归属
117
+ flat.push(parent ? { ...column, groupKey: parent.key, groupLabel: parent.label } : column)
118
+ })
119
+ }
120
+ walk(this.columns, null)
121
+ return flat
122
+ },
123
+ // 列设置隐藏后的可见列(叶子)
124
+ visibleColumns () {
125
+ return this.leafColumns.filter((column) => !this.hiddenColumns.includes(column.key))
126
+ },
127
+ // 表头树:保留分组结构,隐藏的叶子连同空分组一起去掉
128
+ headerTree () {
129
+ const strip = (list) =>
130
+ list
131
+ .map((column) => (hasChildren(column) ? { ...column, children: strip(column.children) } : { ...column }))
132
+ .filter((column) => (hasChildren(column) ? column.children.length > 0 : !this.hiddenColumns.includes(column.key)))
133
+ return strip(this.columns)
134
+ },
135
+ // 表头按层铺开,分组用 colspan,无子节点的列用 rowspan 补齐剩余行
136
+ headerRows () {
137
+ const rows = []
138
+ const depthOf = (node) => (hasChildren(node) ? 1 + Math.max(...node.children.map(depthOf)) : 1)
139
+ const maxDepth = this.headerTree.reduce((max, node) => Math.max(max, depthOf(node)), 1)
140
+
141
+ const collect = (nodes, depth) => {
142
+ if (!rows[depth]) rows[depth] = []
143
+ nodes.forEach((node) => {
144
+ if (hasChildren(node)) {
145
+ rows[depth].push({ node, colspan: leafCount(node), rowspan: 1, group: true })
146
+ collect(node.children, depth + 1)
147
+ } else {
148
+ rows[depth].push({ node, colspan: 1, rowspan: maxDepth - depth, group: false })
149
+ }
150
+ })
151
+ }
152
+
153
+ collect(this.headerTree, 0)
154
+ return rows
155
+ },
156
+ colSpan () {
157
+ return this.visibleColumns.length +
158
+ (this.selectable ? 1 : 0) +
159
+ (this.showIndex ? 1 : 0) +
160
+ (this.expandable ? 1 : 0) +
161
+ (this.rowDraggable ? 1 : 0)
162
+ },
163
+ // 是否存在左右固定列
164
+ hasFixed () {
165
+ return this.visibleColumns.some((column) => column.fixed === 'left' || column.fixed === 'right')
166
+ },
167
+ // 前置列(拖拽手柄 / 展开 / 选择 / 序号)的左侧偏移:有固定列时一并吸边
168
+ prependOffsets () {
169
+ if (!this.hasFixed) return null
170
+ const offsets = { drag: null, expand: null, select: null, index: null, base: 0 }
171
+ let left = 0
172
+ if (this.rowDraggable) {
173
+ offsets.drag = left
174
+ left += 36
175
+ }
176
+ if (this.expandable) {
177
+ offsets.expand = left
178
+ left += 38
179
+ }
180
+ if (this.selectable) {
181
+ offsets.select = left
182
+ left += 42
183
+ }
184
+ if (this.showIndex) {
185
+ offsets.index = left
186
+ left += 52
187
+ }
188
+ offsets.base = left
189
+ return offsets
190
+ },
191
+ // 固定列偏移表:key -> { side, offset, edge }
192
+ stickyMap () {
193
+ if (!this.hasFixed) return {}
194
+ const columns = this.visibleColumns
195
+ const map = {}
196
+ let left = this.prependOffsets ? this.prependOffsets.base : 0
197
+ columns.forEach((column) => {
198
+ if (column.fixed !== 'left') return
199
+ map[column.key] = { side: 'left', offset: left, edge: false }
200
+ left += this.columnWidth(column)
201
+ })
202
+ let right = 0
203
+ for (let i = columns.length - 1; i >= 0; i--) {
204
+ const column = columns[i]
205
+ if (column.fixed !== 'right') continue
206
+ map[column.key] = { side: 'right', offset: right, edge: false }
207
+ right += this.columnWidth(column)
208
+ }
209
+ // 标记贴边列,用于投影分隔
210
+ const leftKeys = columns.filter((column) => column.fixed === 'left').map((column) => column.key)
211
+ const rightKeys = columns.filter((column) => column.fixed === 'right').map((column) => column.key)
212
+ if (leftKeys.length) map[leftKeys[leftKeys.length - 1]].edge = true
213
+ if (rightKeys.length) map[rightKeys[0]].edge = true
214
+ return map
215
+ },
216
+ scrollStyle () {
217
+ if (!this.maxHeight) return {}
218
+ const value = this.maxHeight
219
+ // 数字与纯数字字符串都按 px 处理,其余(如 60vh)原样透传
220
+ if (typeof value === 'number' || /^\d+(\.\d+)?$/.test(value)) {
221
+ return { maxHeight: value + 'px' }
222
+ }
223
+ return { maxHeight: value }
224
+ },
225
+ // 最小宽度:覆盖样式里的 900px,列少的窄容器不再被迫横向滚动
226
+ tableStyle () {
227
+ const value = this.minWidth
228
+ if (value === '' || value === null || value === undefined) return {}
229
+ if (typeof value === 'number' || /^\d+(\.\d+)?$/.test(value)) {
230
+ return { minWidth: value + 'px' }
231
+ }
232
+ return { minWidth: value }
233
+ },
234
+ loadingLabel () {
235
+ return this.loadingText || t('table.loading')
236
+ },
237
+ emptyLabel () {
238
+ return this.emptyText || t('table.empty')
239
+ },
240
+ selectAllLabel () {
241
+ return t('table.selectAll')
242
+ },
243
+ selectRowLabel () {
244
+ return t('table.selectRow')
245
+ },
246
+ scrollRegionLabel () {
247
+ return t('table.scrollRegion')
248
+ },
249
+ allChecked () {
250
+ return this.rows.length > 0 && this.rows.every((row) => this.isSelected(row))
251
+ },
252
+ someChecked () {
253
+ return !this.allChecked && this.rows.some((row) => this.isSelected(row))
254
+ },
255
+ // 排序只影响展示顺序,不改变原始 rows
256
+ displayRows () {
257
+ if (!this.sortKey || !this.sortDir) return this.rows
258
+ // 多级表头的子列也能排序,按叶子列查找
259
+ const column = this.leafColumns.find((item) => item.key === this.sortKey)
260
+ if (!column) return this.rows
261
+ const dir = this.sortDir === 'asc' ? 1 : -1
262
+ const list = this.rows.slice()
263
+ if (typeof column.sorter === 'function') {
264
+ return list.sort((a, b) => column.sorter(a, b) * dir)
265
+ }
266
+ return list.sort((a, b) => {
267
+ const left = a[this.sortKey]
268
+ const right = b[this.sortKey]
269
+ if (typeof left === 'number' && typeof right === 'number') return (left - right) * dir
270
+ return String(left === undefined || left === null ? '' : left)
271
+ .localeCompare(String(right === undefined || right === null ? '' : right), 'zh-Hans-CN') * dir
272
+ })
273
+ },
274
+
275
+ /* ---------------- 虚拟滚动 ---------------- */
276
+
277
+ // 需要 maxHeight 提供滚动容器;展开行高度不固定,暂不参与虚拟滚动
278
+ virtualEnabled () {
279
+ return Boolean(this.virtual && this.maxHeight && !this.expandable)
280
+ },
281
+ // 兜底行高,避免配置过小导致计算出负数区间
282
+ effectiveRowHeight () {
283
+ return Math.max(16, this.rowHeight)
284
+ },
285
+ // 可视区起止行(含 overscan)
286
+ visibleRange () {
287
+ const total = this.displayRows.length
288
+ if (!this.virtualEnabled || !total) return { start: 0, end: total }
289
+ const rowHeight = this.effectiveRowHeight
290
+ const viewHeight = this.viewportHeight || rowHeight * 10
291
+ const start = Math.max(0, Math.floor(this.scrollTop / rowHeight) - this.overscan)
292
+ const end = Math.min(total, Math.ceil((this.scrollTop + viewHeight) / rowHeight) + this.overscan)
293
+ return { start, end }
294
+ },
295
+ // 实际渲染的行,带原始下标(序号、插槽都用绝对位置)
296
+ bodyRows () {
297
+ const { start, end } = this.visibleRange
298
+ return this.displayRows.slice(start, end).map((row, offset) => ({ row, index: start + offset }))
299
+ },
300
+ topPad () {
301
+ return this.virtualEnabled ? this.visibleRange.start * this.effectiveRowHeight : 0
302
+ },
303
+ bottomPad () {
304
+ if (!this.virtualEnabled) return 0
305
+ const total = this.displayRows.length
306
+ return Math.max(0, (total - this.visibleRange.end) * this.effectiveRowHeight)
307
+ }
308
+ },
309
+ mounted () {
310
+ document.addEventListener('mousedown', this.onDocumentMousedown, true)
311
+ this.measureViewport()
312
+ },
313
+ beforeUnmount () {
314
+ document.removeEventListener('mousedown', this.onDocumentMousedown, true)
315
+ document.removeEventListener('mousemove', this.onResizeMove)
316
+ document.removeEventListener('mouseup', this.onResizeEnd)
317
+ },
318
+ methods: {
319
+ // 记录可视区高度:虚拟滚动用它算需要渲染多少行
320
+ measureViewport () {
321
+ const el = this.$refs.wrap
322
+ if (!el || !this.virtualEnabled) return
323
+ this.viewportHeight = el.clientHeight
324
+ this.scrollTop = el.scrollTop
325
+ },
326
+ onScroll (event) {
327
+ if (!this.virtualEnabled) return
328
+ const el = event.target
329
+ this.scrollTop = el.scrollTop
330
+ this.viewportHeight = el.clientHeight
331
+ },
332
+ // 列设置:至少保留一列
333
+ toggleColumn (key) {
334
+ if (this.hiddenColumns.includes(key)) {
335
+ this.hiddenColumns = this.hiddenColumns.filter((item) => item !== key)
336
+ return
337
+ }
338
+ if (this.visibleColumns.length <= 1) return
339
+ this.hiddenColumns = [...this.hiddenColumns, key]
340
+ },
341
+ resetColumns () {
342
+ this.hiddenColumns = []
343
+ },
344
+ onDocumentMousedown (event) {
345
+ if (!this.$refs.root || this.$refs.root.contains(event.target)) return
346
+ this.settingOpen = false
347
+ },
348
+ // 导出当前排序后的全部行,操作列可通过 exportable: false 排除
349
+ exportCsv () {
350
+ const columns = this.visibleColumns.filter((column) => column.exportable !== false)
351
+ if (!columns.length) return
352
+ const header = columns.map((column) => column.label || column.key)
353
+ const lines = this.displayRows.map((row) =>
354
+ columns.map((column) => {
355
+ const raw = typeof column.exportValue === 'function' ? column.exportValue(row) : row[column.key]
356
+ return String(raw === undefined || raw === null ? '' : raw).replace(/"/g, '""')
357
+ })
358
+ )
359
+ const csv = '\uFEFF' + [header, ...lines]
360
+ .map((cols) => cols.map((cell) => `"${cell}"`).join(','))
361
+ .join('\n')
362
+
363
+ this.$emit('export', { rows: this.displayRows.slice(), csv })
364
+ this.downloadCsv(csv)
365
+ },
366
+ downloadCsv (csv) {
367
+ const now = new Date()
368
+ // 文件名用本地日期,避免 toISOString 的 UTC 跨天
369
+ const day = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
370
+ const url = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' }))
371
+ const link = document.createElement('a')
372
+ link.href = url
373
+ link.download = `${this.exportName}-${day}.csv`
374
+ link.click()
375
+ URL.revokeObjectURL(url)
376
+ },
377
+ isSelected (row) {
378
+ return this.selected.includes(row[this.rowKey])
379
+ },
380
+ canExpand (row) {
381
+ if (typeof this.expandable === 'function') return Boolean(this.expandable(row))
382
+ return Boolean(this.expandable)
383
+ },
384
+ isExpanded (row) {
385
+ return this.expanded.includes(row[this.rowKey])
386
+ },
387
+ toggleExpand (row) {
388
+ const id = row[this.rowKey]
389
+ const next = this.expanded.includes(id)
390
+ ? this.expanded.filter((item) => item !== id)
391
+ : [...this.expanded, id]
392
+ this.$emit('update:expanded', next)
393
+ },
394
+ rowClassOf (row, index) {
395
+ return typeof this.rowClass === 'function' ? this.rowClass(row, index) : this.rowClass
396
+ },
397
+ // 固定列宽度:拖拽值优先,未配置宽度时给一个兜底值,保证相邻列偏移不重叠
398
+ columnWidth (column) {
399
+ const custom = this.colWidths[column.key]
400
+ if (typeof custom === 'number') return custom
401
+ return typeof column.width === 'number' ? column.width : 120
402
+ },
403
+ stickyClassOf (column) {
404
+ const item = this.stickyMap[column.key]
405
+ if (!item) return ''
406
+ return [
407
+ item.side === 'left' ? 'djm-sticky-left' : 'djm-sticky-right',
408
+ item.edge ? 'djm-sticky-edge' : ''
409
+ ]
410
+ },
411
+ stickyStyleOf (column) {
412
+ const item = this.stickyMap[column.key]
413
+ if (!item) return {}
414
+ return item.side === 'left' ? { left: item.offset + 'px' } : { right: item.offset + 'px' }
415
+ },
416
+ // 多行表头吸顶时逐行下移,避免第二行盖住第一行
417
+ stickyTop (rowIndex) {
418
+ if (this.headerRows.length <= 1) return {}
419
+ return { top: rowIndex * (this.compact ? 32 : 38) + 'px' }
420
+ },
421
+ // 前置列的吸边类名与偏移
422
+ prependClass (key) {
423
+ if (!this.prependOffsets) return ''
424
+ const offset = this.prependOffsets[key]
425
+ if (offset === null || offset === undefined) return ''
426
+ const order = ['drag', 'expand', 'select', 'index'].filter((name) => this.prependOffsets[name] !== null)
427
+ // 后面还有左固定数据列时,投影由数据列承担
428
+ const last = order[order.length - 1] === key
429
+ const hasLeftFixed = this.visibleColumns.some((column) => column.fixed === 'left')
430
+ return ['djm-sticky-left', last && !hasLeftFixed ? 'djm-sticky-edge' : '']
431
+ },
432
+ prependStyle (key) {
433
+ if (!this.prependOffsets) return {}
434
+ const offset = this.prependOffsets[key]
435
+ if (offset === null || offset === undefined) return {}
436
+ return { left: offset + 'px' }
437
+ },
438
+ ariaSort (column) {
439
+ if (!column.sortable) return undefined
440
+ if (this.sortKey !== column.key) return 'none'
441
+ return this.sortDir === 'asc' ? 'ascending' : 'descending'
442
+ },
443
+ toggleSort (column) {
444
+ if (!column.sortable) return
445
+ let key = column.key
446
+ let dir = 'asc'
447
+ if (this.sortKey === column.key && this.sortDir === 'asc') {
448
+ dir = 'desc'
449
+ } else if (this.sortKey === column.key && this.sortDir === 'desc') {
450
+ key = ''
451
+ dir = ''
452
+ }
453
+ this.$emit('update:sortKey', key)
454
+ this.$emit('update:sortDir', dir)
455
+ this.$emit('sort-change', { key, dir })
456
+ },
457
+ toggleAll (event) {
458
+ const ids = this.rows.map((row) => row[this.rowKey])
459
+ if (event.target.checked) {
460
+ const merged = new Set(this.selected)
461
+ ids.forEach((id) => merged.add(id))
462
+ this.$emit('update:selected', [...merged])
463
+ } else {
464
+ const current = new Set(ids)
465
+ this.$emit('update:selected', this.selected.filter((id) => !current.has(id)))
466
+ }
467
+ },
468
+ toggleRow (row) {
469
+ const id = row[this.rowKey]
470
+ const next = this.selected.includes(id)
471
+ ? this.selected.filter((item) => item !== id)
472
+ : [...this.selected, id]
473
+ this.$emit('update:selected', next)
474
+ },
475
+ cellStyle (column) {
476
+ // 拖拽过的列固定 px,其余保持列配置(支持数字与 '20%' 这类字符串)
477
+ const custom = this.colWidths[column.key]
478
+ let width = ''
479
+ if (typeof custom === 'number') width = custom + 'px'
480
+ else if (typeof column.width === 'number') width = column.width + 'px'
481
+ else if (column.width) width = String(column.width)
482
+ return {
483
+ width,
484
+ minWidth: width,
485
+ // 固定列锁死宽度,否则自动布局撑开会让偏移量错位
486
+ maxWidth: column.fixed ? width : '',
487
+ textAlign: column.align || ''
488
+ }
489
+ },
490
+
491
+ /* ---------------- 列宽拖拽 ---------------- */
492
+
493
+ // 开始拖拽:记录起点与当前实际宽度
494
+ startResize (event, column) {
495
+ if (!this.resizable || column.resizable === false) return
496
+ const th = event.currentTarget.parentNode
497
+ this.resizeState = {
498
+ key: column.key,
499
+ startX: event.clientX,
500
+ startWidth: th.getBoundingClientRect().width
501
+ }
502
+ document.addEventListener('mousemove', this.onResizeMove)
503
+ document.addEventListener('mouseup', this.onResizeEnd)
504
+ },
505
+ onResizeMove (event) {
506
+ const state = this.resizeState
507
+ if (!state) return
508
+ const min = Math.max(40, this.minColumnWidth)
509
+ const width = Math.round(Math.max(min, state.startWidth + event.clientX - state.startX))
510
+ this.colWidths = { ...this.colWidths, [state.key]: width }
511
+ this.$emit('column-resize', { key: state.key, width })
512
+ },
513
+ onResizeEnd () {
514
+ if (!this.resizeState) return
515
+ const { key } = this.resizeState
516
+ this.resizeState = null
517
+ document.removeEventListener('mousemove', this.onResizeMove)
518
+ document.removeEventListener('mouseup', this.onResizeEnd)
519
+ this.$emit('column-resize-end', { key, width: this.colWidths[key] })
520
+ },
521
+ // 双击分割线恢复该列的配置宽度
522
+ resetColumnWidth (key) {
523
+ if (!this.colWidths[key]) return
524
+ const next = { ...this.colWidths }
525
+ delete next[key]
526
+ this.colWidths = next
527
+ },
528
+
529
+ /* ---------------- 行拖拽排序 ---------------- */
530
+
531
+ canDragRow (row, index) {
532
+ if (!this.rowDraggable) return false
533
+ if (typeof this.allowRowDrag === 'function') return this.allowRowDrag(row, index) !== false
534
+ return true
535
+ },
536
+ isRowDragging (item) {
537
+ return Boolean(this.rowDragState && this.rowDragState.key === item.row[this.rowKey])
538
+ },
539
+ rowDropClassOf (item) {
540
+ const state = this.rowDropState
541
+ if (!state || state.key !== item.row[this.rowKey]) return ''
542
+ return 'djm-is-row-drop-' + state.position
543
+ },
544
+ onRowDragStart (item, event) {
545
+ if (!this.canDragRow(item.row, item.index)) {
546
+ event.preventDefault()
547
+ return
548
+ }
549
+ this.rowDragState = { key: item.row[this.rowKey], index: item.index }
550
+ if (event.dataTransfer) {
551
+ event.dataTransfer.effectAllowed = 'move'
552
+ event.dataTransfer.setData('text/plain', String(item.index))
553
+ }
554
+ },
555
+ // 落点按行高对半分:上半插到前面,下半插到后面
556
+ dropPositionOf (event) {
557
+ const rect = event.currentTarget.getBoundingClientRect()
558
+ const ratio = rect.height ? (event.clientY - rect.top) / rect.height : 0.5
559
+ return ratio < 0.5 ? 'before' : 'after'
560
+ },
561
+ onRowDragOver (item, event) {
562
+ if (!this.rowDragState) return
563
+ event.preventDefault()
564
+ if (event.dataTransfer) event.dataTransfer.dropEffect = 'move'
565
+ this.rowDropState = {
566
+ key: item.row[this.rowKey],
567
+ index: item.index,
568
+ position: this.dropPositionOf(event)
569
+ }
570
+ },
571
+ onRowDragLeave (item, event) {
572
+ const current = event.currentTarget
573
+ // 行内单元格之间移动也会触发 dragleave,指针仍在行内时不清落点
574
+ if (current && event.relatedTarget && current.contains(event.relatedTarget)) return
575
+ if (this.rowDropState && this.rowDropState.key === item.row[this.rowKey]) this.rowDropState = null
576
+ },
577
+ onRowDrop (item, event) {
578
+ const drag = this.rowDragState
579
+ if (!drag) return
580
+ event.preventDefault()
581
+ const state = this.rowDropState
582
+ const position = state && state.key === item.row[this.rowKey] ? state.position : this.dropPositionOf(event)
583
+ this.resetRowDrag()
584
+ if (this.isNoopDrop(drag.index, item.index, position)) return
585
+ this.$emit('row-drop', {
586
+ dragIndex: drag.index,
587
+ dropIndex: item.index,
588
+ position,
589
+ dragKey: drag.key,
590
+ dropKey: item.row[this.rowKey]
591
+ })
592
+ },
593
+ // 拖到自身、或紧邻位置(结果不变)时不抛事件
594
+ isNoopDrop (from, to, position) {
595
+ if (from === to) return true
596
+ if (position === 'after' && to === from - 1) return true
597
+ if (position === 'before' && to === from + 1) return true
598
+ return false
599
+ },
600
+ onRowDragEnd () {
601
+ this.resetRowDrag()
602
+ },
603
+ resetRowDrag () {
604
+ this.rowDragState = null
605
+ this.rowDropState = null
606
+ }
607
+ },
608
+ template: `
609
+ <div class="djm-table-root" :class="{ 'djm-is-resizing': !!resizeState }" ref="root">
610
+ <div
611
+ v-if="columnSetting || exportable || $slots.toolbar || $slots['toolbar-extra']"
612
+ class="djm-table-tools"
613
+ >
614
+ <div class="djm-table-tools-left">
615
+ <slot name="toolbar" :rows="displayRows"></slot>
616
+ </div>
617
+ <div class="djm-table-tools-right">
618
+ <slot name="toolbar-extra"></slot>
619
+ <button v-if="exportable" class="djm-btn djm-btn-sm" type="button" @click="exportCsv">
620
+ <DjmIcon name="download" :size="13" />导出
621
+ </button>
622
+ <div v-if="columnSetting" class="djm-table-col-setting">
623
+ <button class="djm-btn djm-btn-sm" type="button" @click.stop="settingOpen = !settingOpen">
624
+ <DjmIcon name="sliders" :size="13" />列设置
625
+ </button>
626
+ <div v-if="settingOpen" class="djm-table-col-panel">
627
+ <div class="djm-table-col-head">
628
+ <span>显示列</span>
629
+ <button class="djm-link-btn" type="button" @click="resetColumns">重置</button>
630
+ </div>
631
+ <!-- 多级表头按叶子列勾选,带上分组名便于识别 -->
632
+ <label v-for="column in leafColumns" :key="column.key" class="djm-table-col-item">
633
+ <input
634
+ type="checkbox"
635
+ :checked="!hiddenColumns.includes(column.key)"
636
+ :disabled="visibleColumns.length <= 1 && !hiddenColumns.includes(column.key)"
637
+ @change="toggleColumn(column.key)"
638
+ />
639
+ <span>{{ column.groupLabel ? column.groupLabel + ' / ' + (column.label || column.key) : (column.label || column.key) }}</span>
640
+ </label>
641
+ </div>
642
+ </div>
643
+ </div>
644
+ </div>
645
+
646
+ <div
647
+ class="djm-table-wrap"
648
+ ref="wrap"
649
+ :class="{ 'djm-is-scrollable': !!maxHeight, 'djm-has-fixed': hasFixed, 'djm-is-virtual': virtualEnabled }"
650
+ :style="scrollStyle"
651
+ role="region"
652
+ :aria-label="scrollRegionLabel"
653
+ tabindex="0"
654
+ @scroll="onScroll"
655
+ >
656
+ <table class="djm-data-table" :class="{ 'djm-is-striped': striped, 'djm-is-compact': compact, 'djm-is-resizable': resizable }" :style="tableStyle">
657
+ <thead>
658
+ <tr v-for="(row, rowIndex) in headerRows" :key="rowIndex">
659
+ <!-- 前置列只在第一行渲染,并跨全部表头行 -->
660
+ <th
661
+ v-if="rowDraggable && rowIndex === 0"
662
+ :class="['djm-col-drag', prependClass('drag')]"
663
+ :style="[prependStyle('drag'), stickyTop(rowIndex)]"
664
+ :rowspan="headerRows.length > 1 ? headerRows.length : undefined"
665
+ scope="col"
666
+ aria-label="拖动排序"
667
+ ></th>
668
+ <th
669
+ v-if="expandable && rowIndex === 0"
670
+ :class="['djm-col-expand', prependClass('expand')]"
671
+ :style="[prependStyle('expand'), stickyTop(rowIndex)]"
672
+ :rowspan="headerRows.length > 1 ? headerRows.length : undefined"
673
+ scope="col"
674
+ ></th>
675
+ <th
676
+ v-if="selectable && rowIndex === 0"
677
+ :class="['djm-col-check', prependClass('select')]"
678
+ :style="[prependStyle('select'), stickyTop(rowIndex)]"
679
+ :rowspan="headerRows.length > 1 ? headerRows.length : undefined"
680
+ scope="col"
681
+ >
682
+ <input type="checkbox" :checked="allChecked" :indeterminate="someChecked" :aria-label="selectAllLabel" @change="toggleAll" />
683
+ </th>
684
+ <th
685
+ v-if="showIndex && rowIndex === 0"
686
+ :class="['djm-col-index', prependClass('index')]"
687
+ :style="[prependStyle('index'), stickyTop(rowIndex)]"
688
+ :rowspan="headerRows.length > 1 ? headerRows.length : undefined"
689
+ scope="col"
690
+ >#</th>
691
+
692
+ <th
693
+ v-for="cell in row"
694
+ :key="cell.node.key"
695
+ :colspan="cell.colspan > 1 ? cell.colspan : undefined"
696
+ :rowspan="cell.rowspan > 1 ? cell.rowspan : undefined"
697
+ :class="[
698
+ cell.node.class,
699
+ cell.node.headerClass,
700
+ cell.group ? 'djm-col-group' : stickyClassOf(cell.node),
701
+ {
702
+ 'djm-is-sortable': !cell.group && cell.node.sortable,
703
+ 'djm-has-resizer': !cell.group && resizable && cell.node.resizable !== false
704
+ }
705
+ ]"
706
+ :style="[cell.group ? {} : cellStyle(cell.node), cell.group ? {} : stickyStyleOf(cell.node), stickyTop(rowIndex)]"
707
+ :aria-sort="cell.group ? undefined : ariaSort(cell.node)"
708
+ :scope="cell.group ? 'colgroup' : 'col'"
709
+ :tabindex="!cell.group && cell.node.sortable ? 0 : undefined"
710
+ @click="!cell.group && toggleSort(cell.node)"
711
+ @keydown.enter.prevent="!cell.group && toggleSort(cell.node)"
712
+ @keydown.space.prevent="!cell.group && toggleSort(cell.node)"
713
+ >
714
+ <span class="djm-table-sort">
715
+ <slot :name="'header-' + cell.node.key" :column="cell.node">{{ cell.node.label }}</slot>
716
+ <span v-if="!cell.group && cell.node.sortable" class="djm-table-sorter">
717
+ <i class="djm-table-sort-up" :class="{ 'djm-is-on': sortKey === cell.node.key && sortDir === 'asc' }"></i>
718
+ <i class="djm-table-sort-down" :class="{ 'djm-is-on': sortKey === cell.node.key && sortDir === 'desc' }"></i>
719
+ </span>
720
+ </span>
721
+ <!-- 列宽拖拽分割线:只挂在叶子列,阻止冒泡避免触发排序 -->
722
+ <span
723
+ v-if="!cell.group && resizable && cell.node.resizable !== false"
724
+ class="djm-table-resizer"
725
+ :class="{ 'djm-is-active': resizeState && resizeState.key === cell.node.key }"
726
+ aria-hidden="true"
727
+ @mousedown.prevent.stop="startResize($event, cell.node)"
728
+ @click.stop
729
+ @dblclick.stop="resetColumnWidth(cell.node.key)"
730
+ ></span>
731
+ </th>
732
+ </tr>
733
+ </thead>
734
+ <tbody>
735
+ <!-- 虚拟滚动:上方占位行,保证滚动条长度与真实行数一致 -->
736
+ <tr v-if="topPad > 0" class="djm-table-spacer" aria-hidden="true">
737
+ <td :colspan="colSpan" :style="{ height: topPad + 'px' }"></td>
738
+ </tr>
739
+
740
+ <template v-for="item in bodyRows" :key="item.row[rowKey]">
741
+ <tr
742
+ :class="[
743
+ rowClassOf(item.row, item.index),
744
+ rowDropClassOf(item),
745
+ { 'djm-is-selected': isSelected(item.row), 'djm-is-row-dragging': isRowDragging(item) }
746
+ ]"
747
+ :style="virtualEnabled ? { height: effectiveRowHeight + 'px' } : null"
748
+ @click="$emit('row-click', item.row, item.index)"
749
+ @dblclick="$emit('row-dblclick', item.row, item.index)"
750
+ @dragover="onRowDragOver(item, $event)"
751
+ @dragleave="onRowDragLeave(item, $event)"
752
+ @drop="onRowDrop(item, $event)"
753
+ >
754
+ <!-- 拖拽手柄:只有手柄可拖,避免和选中文本、行点击冲突 -->
755
+ <td
756
+ v-if="rowDraggable"
757
+ :class="['djm-col-drag', prependClass('drag')]"
758
+ :style="prependStyle('drag')"
759
+ >
760
+ <span
761
+ class="djm-row-handle"
762
+ :class="{ 'djm-is-disabled': !canDragRow(item.row, item.index) }"
763
+ :draggable="canDragRow(item.row, item.index) ? 'true' : 'false'"
764
+ :title="canDragRow(item.row, item.index) ? '拖动排序' : '该行不可拖动'"
765
+ aria-hidden="true"
766
+ @dragstart.stop="onRowDragStart(item, $event)"
767
+ @dragend.stop="onRowDragEnd"
768
+ @click.stop
769
+ >
770
+ <DjmIcon name="grip" :size="13" />
771
+ </span>
772
+ </td>
773
+ <td
774
+ v-if="expandable"
775
+ :class="['djm-col-expand', prependClass('expand')]"
776
+ :style="prependStyle('expand')"
777
+ >
778
+ <button
779
+ v-if="canExpand(item.row)"
780
+ class="djm-table-expand-btn"
781
+ type="button"
782
+ :aria-expanded="isExpanded(item.row)"
783
+ :aria-label="isExpanded(item.row) ? '收起' : '展开'"
784
+ @click.stop="toggleExpand(item.row)"
785
+ >
786
+ <DjmIcon :name="isExpanded(item.row) ? 'chevron-down' : 'chevron-right'" :size="12" />
787
+ </button>
788
+ </td>
789
+ <td
790
+ v-if="selectable"
791
+ :class="['djm-col-check', prependClass('select')]"
792
+ :style="prependStyle('select')"
793
+ >
794
+ <input type="checkbox" :checked="isSelected(item.row)" :aria-label="selectRowLabel" @change="toggleRow(item.row)" @click.stop />
795
+ </td>
796
+ <td
797
+ v-if="showIndex"
798
+ :class="['djm-col-index', prependClass('index')]"
799
+ :style="prependStyle('index')"
800
+ >{{ indexOffset + item.index + 1 }}</td>
801
+ <td
802
+ v-for="column in visibleColumns"
803
+ :key="column.key"
804
+ :class="[column.class, column.cellClass, stickyClassOf(column)]"
805
+ :style="[cellStyle(column), stickyStyleOf(column)]"
806
+ >
807
+ <slot
808
+ :name="'cell-' + column.key"
809
+ :row="item.row"
810
+ :value="item.row[column.key]"
811
+ :index="item.index"
812
+ :column="column"
813
+ >{{ item.row[column.key] }}</slot>
814
+ </td>
815
+ </tr>
816
+
817
+ <tr v-if="isExpanded(item.row)" class="djm-table-expand-row">
818
+ <td :colspan="colSpan">
819
+ <div class="djm-table-expand-body">
820
+ <slot name="expand" :row="item.row" :index="item.index"></slot>
821
+ </div>
822
+ </td>
823
+ </tr>
824
+ </template>
825
+
826
+ <!-- 虚拟滚动:下方占位行 -->
827
+ <tr v-if="bottomPad > 0" class="djm-table-spacer" aria-hidden="true">
828
+ <td :colspan="colSpan" :style="{ height: bottomPad + 'px' }"></td>
829
+ </tr>
830
+
831
+ <tr v-if="!displayRows.length">
832
+ <td :colspan="colSpan">
833
+ <slot name="empty">
834
+ <DjmEmpty :icon="emptyIcon" :title="emptyLabel" :description="emptyHint" />
835
+ </slot>
836
+ </td>
837
+ </tr>
838
+ </tbody>
839
+ </table>
840
+
841
+ <div v-if="loading" class="djm-table-loading">
842
+ <span class="djm-spinner"></span>{{ loadingLabel }}
843
+ </div>
844
+ </div>
845
+ </div>
846
+ `
847
+ }