vome-core 0.0.1

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 (209) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js +1 -0
  3. package/dist/server/index.d.ts +1 -0
  4. package/dist/server/index.js +1 -0
  5. package/dist/src/auth/define-macro.d.ts +8 -0
  6. package/dist/src/auth/index.d.ts +2 -0
  7. package/dist/src/comm/daily-log.d.ts +8 -0
  8. package/dist/src/comm/env.d.ts +4 -0
  9. package/dist/src/comm/format-time.d.ts +10 -0
  10. package/dist/src/comm/merge.d.ts +2 -0
  11. package/dist/src/comm/scan.d.ts +4 -0
  12. package/dist/src/comm/startBanner.d.ts +1 -0
  13. package/dist/src/core/app.d.ts +37 -0
  14. package/dist/src/core/config.d.ts +2 -0
  15. package/dist/src/core/context/index.d.ts +22 -0
  16. package/dist/src/core/host-infra.d.ts +20 -0
  17. package/dist/src/core/index.d.ts +20 -0
  18. package/dist/src/core/plugins/async-context.d.ts +3 -0
  19. package/dist/src/core/plugins/error.d.ts +5 -0
  20. package/dist/src/core/plugins/logging.d.ts +3 -0
  21. package/dist/src/core/plugins/openapi.d.ts +3 -0
  22. package/dist/src/core/vome-config.d.ts +2 -0
  23. package/dist/src/ext/base-plugin.d.ts +13 -0
  24. package/dist/src/ext/index.d.ts +15 -0
  25. package/dist/src/ext/load-plugin.d.ts +12 -0
  26. package/dist/src/ext/micro-apps.d.ts +60 -0
  27. package/dist/src/ext/module-gateway.d.ts +10 -0
  28. package/dist/src/ext/module-registry.d.ts +45 -0
  29. package/dist/src/ext/path.d.ts +8 -0
  30. package/dist/src/index.d.ts +10 -0
  31. package/dist/src/ioc/container.d.ts +29 -0
  32. package/dist/src/ioc/decorators.d.ts +35 -0
  33. package/dist/src/ioc/host.d.ts +2 -0
  34. package/dist/src/ioc/index.d.ts +6 -0
  35. package/dist/src/ioc/metadata.d.ts +12 -0
  36. package/dist/src/ioc/scope.d.ts +9 -0
  37. package/dist/src/logger/bootstrap.d.ts +2 -0
  38. package/dist/src/logger/exception.d.ts +22 -0
  39. package/dist/src/logger/index.d.ts +5 -0
  40. package/dist/src/logger/logger.d.ts +18 -0
  41. package/dist/src/logger/pino.d.ts +8 -0
  42. package/dist/src/orm/base-columns.d.ts +11 -0
  43. package/dist/src/orm/column-comments.d.ts +25 -0
  44. package/dist/src/orm/crud-config.d.ts +7 -0
  45. package/dist/src/orm/crud-register.d.ts +5 -0
  46. package/dist/src/orm/data-scope.d.ts +24 -0
  47. package/dist/src/orm/db-store.d.ts +12 -0
  48. package/dist/src/orm/entity-schema.d.ts +7 -0
  49. package/dist/src/orm/index.d.ts +22 -0
  50. package/dist/src/orm/query-builder.d.ts +12 -0
  51. package/dist/src/orm/query-op.d.ts +27 -0
  52. package/dist/src/orm/repository.d.ts +74 -0
  53. package/dist/src/orm/service.d.ts +71 -0
  54. package/dist/src/orm/tenant.d.ts +24 -0
  55. package/dist/src/routing/base.d.ts +27 -0
  56. package/dist/src/routing/context.d.ts +14 -0
  57. package/dist/src/routing/crud-summary.d.ts +2 -0
  58. package/dist/src/routing/decorators.d.ts +48 -0
  59. package/dist/src/routing/eps.d.ts +16 -0
  60. package/dist/src/routing/index.d.ts +8 -0
  61. package/dist/src/routing/metadata.d.ts +28 -0
  62. package/dist/src/routing/openapi-tags.d.ts +23 -0
  63. package/dist/src/routing/path-utils.d.ts +5 -0
  64. package/dist/src/routing/perm.d.ts +18 -0
  65. package/dist/src/routing/register.d.ts +7 -0
  66. package/dist/src/server/index.d.ts +10 -0
  67. package/dist/src/shared/index.d.ts +8 -0
  68. package/dist/src/shared/tree.d.ts +18 -0
  69. package/dist/typings/comm/crud.d.ts +24 -0
  70. package/dist/typings/comm/page.d.ts +16 -0
  71. package/dist/typings/comm/scan.d.ts +6 -0
  72. package/dist/typings/common.d.ts +6 -0
  73. package/dist/typings/context/data.d.ts +10 -0
  74. package/dist/typings/ext/module.d.ts +45 -0
  75. package/dist/typings/ext/plugin.d.ts +40 -0
  76. package/dist/typings/ioc/metadata.d.ts +31 -0
  77. package/dist/typings/logger/config.d.ts +6 -0
  78. package/dist/typings/routing/crud.d.ts +99 -0
  79. package/dist/typings/routing/eps.d.ts +53 -0
  80. package/dist/typings/routing/meta.d.ts +56 -0
  81. package/dist/typings/routing/register.d.ts +15 -0
  82. package/dist/typings/routing/scan-context.d.ts +6 -0
  83. package/package.json +96 -0
  84. package/src/admin/api/client.ts +184 -0
  85. package/src/admin/components/vm-aside.vue +227 -0
  86. package/src/admin/components/vm-auto-perm-dialog.vue +361 -0
  87. package/src/admin/components/vm-check-tree.vue +338 -0
  88. package/src/admin/components/vm-dept-tree.vue +549 -0
  89. package/src/admin/components/vm-empty.vue +62 -0
  90. package/src/admin/components/vm-eps-perm-picker.vue +114 -0
  91. package/src/admin/components/vm-group-cascader.vue +563 -0
  92. package/src/admin/components/vm-icon-picker.vue +616 -0
  93. package/src/admin/components/vm-markdown.vue +141 -0
  94. package/src/admin/components/vm-ri-icon.vue +21 -0
  95. package/src/admin/components/vm-role-picker.vue +314 -0
  96. package/src/admin/components/vm-view-path-picker.vue +56 -0
  97. package/src/admin/config/plugin-dev.ts +31 -0
  98. package/src/admin/crud/components/vm-column-custom.vue +196 -0
  99. package/src/admin/crud/components/vm-context-menu.vue +99 -0
  100. package/src/admin/crud/components/vm-date-picker.vue +74 -0
  101. package/src/admin/crud/components/vm-date-range.vue +291 -0
  102. package/src/admin/crud/components/vm-date-text.vue +41 -0
  103. package/src/admin/crud/components/vm-dict-tag.vue +47 -0
  104. package/src/admin/crud/components/vm-file-icon.vue +54 -0
  105. package/src/admin/crud/components/vm-multi-select.vue +181 -0
  106. package/src/admin/crud/components/vm-number-range.vue +200 -0
  107. package/src/admin/crud/components/vm-radio.vue +87 -0
  108. package/src/admin/crud/components/vm-select.vue +131 -0
  109. package/src/admin/crud/components/vm-switch.vue +128 -0
  110. package/src/admin/crud/components/vm-tree-select.vue +456 -0
  111. package/src/admin/crud/components/vm-upload-item.vue +215 -0
  112. package/src/admin/crud/components/vm-upload.vue +323 -0
  113. package/src/admin/crud/components/vm-user-select.vue +234 -0
  114. package/src/admin/crud/config.ts +38 -0
  115. package/src/admin/crud/confirm.ts +135 -0
  116. package/src/admin/crud/dict.ts +49 -0
  117. package/src/admin/crud/index.ts +79 -0
  118. package/src/admin/crud/key.ts +47 -0
  119. package/src/admin/crud/mitt.ts +27 -0
  120. package/src/admin/crud/plugins.ts +298 -0
  121. package/src/admin/crud/registry.ts +45 -0
  122. package/src/admin/crud/span.ts +48 -0
  123. package/src/admin/crud/style.ts +112 -0
  124. package/src/admin/crud/useCrud.ts +81 -0
  125. package/src/admin/crud/validate.ts +109 -0
  126. package/src/admin/crud/vm-add-btn.vue +68 -0
  127. package/src/admin/crud/vm-adv-search.vue +73 -0
  128. package/src/admin/crud/vm-crud.vue +517 -0
  129. package/src/admin/crud/vm-dialog.vue +234 -0
  130. package/src/admin/crud/vm-flex1.vue +14 -0
  131. package/src/admin/crud/vm-form.vue +239 -0
  132. package/src/admin/crud/vm-multi-delete-btn.vue +48 -0
  133. package/src/admin/crud/vm-pagination.vue +125 -0
  134. package/src/admin/crud/vm-refresh-btn.vue +34 -0
  135. package/src/admin/crud/vm-row.vue +42 -0
  136. package/src/admin/crud/vm-search-key.vue +76 -0
  137. package/src/admin/crud/vm-search.vue +367 -0
  138. package/src/admin/crud/vm-table.vue +852 -0
  139. package/src/admin/crud/vm-toolbar.vue +245 -0
  140. package/src/admin/crud/vm-upsert.vue +530 -0
  141. package/src/admin/data/remixicon-all.json +1 -0
  142. package/src/admin/directives/perm.ts +18 -0
  143. package/src/admin/env.d.ts +5 -0
  144. package/src/admin/hooks/useBrowser.ts +27 -0
  145. package/src/admin/hooks/useUpload.ts +82 -0
  146. package/src/admin/hooks/useVome.ts +13 -0
  147. package/src/admin/index.ts +59 -0
  148. package/src/admin/lib/browser.ts +28 -0
  149. package/src/admin/lib/cn.ts +7 -0
  150. package/src/admin/lib/dialog-float.ts +14 -0
  151. package/src/admin/lib/eps.ts +43 -0
  152. package/src/admin/lib/menu.ts +15 -0
  153. package/src/admin/lib/module.ts +112 -0
  154. package/src/admin/lib/tree.ts +2 -0
  155. package/src/admin/lib/upload.ts +102 -0
  156. package/src/admin/router/index.ts +61 -0
  157. package/src/admin/router/menu-routes.ts +126 -0
  158. package/src/admin/service/base.ts +96 -0
  159. package/src/admin/service/index.ts +281 -0
  160. package/src/admin/static/fileicon/demo.css +539 -0
  161. package/src/admin/static/fileicon/demo_index.html +901 -0
  162. package/src/admin/static/fileicon/iconfont.css +139 -0
  163. package/src/admin/static/fileicon/iconfont.js +1 -0
  164. package/src/admin/static/fileicon/iconfont.json +226 -0
  165. package/src/admin/static/fileicon/iconfont.ttf +0 -0
  166. package/src/admin/static/fileicon/iconfont.woff +0 -0
  167. package/src/admin/static/fileicon/iconfont.woff2 +0 -0
  168. package/src/admin/static/scarce//344/270/213/350/275/275/345/244/261/350/264/245.svg +1 -0
  169. package/src/admin/static/scarce//346/224/257/344/273/230/346/210/220/345/212/237.svg +1 -0
  170. package/src/admin/static/scarce//346/232/202/346/227/240/344/274/230/346/203/240/345/210/270.svg +1 -0
  171. package/src/admin/static/scarce//346/232/202/346/227/240/345/206/205/345/256/271.svg +1 -0
  172. package/src/admin/static/scarce//346/232/202/346/227/240/345/217/221/347/245/250.svg +1 -0
  173. package/src/admin/static/scarce//346/232/202/346/227/240/345/233/276/347/211/207.svg +1 -0
  174. package/src/admin/static/scarce//346/232/202/346/227/240/345/234/260/345/235/200.svg +1 -0
  175. package/src/admin/static/scarce//346/232/202/346/227/240/345/272/227/351/223/272.svg +1 -0
  176. package/src/admin/static/scarce//346/232/202/346/227/240/346/216/250/351/200/201.svg +1 -0
  177. package/src/admin/static/scarce//346/232/202/346/227/240/346/220/234/347/264/242/347/273/223/346/236/234.svg +1 -0
  178. package/src/admin/static/scarce//346/232/202/346/227/240/346/224/266/350/227/217.svg +1 -0
  179. package/src/admin/static/scarce//346/232/202/346/227/240/346/225/260/346/215/256.svg +1 -0
  180. package/src/admin/static/scarce//346/232/202/346/227/240/346/235/203/351/231/220.svg +1 -0
  181. package/src/admin/static/scarce//346/232/202/346/227/240/346/266/210/346/201/257.svg +1 -0
  182. package/src/admin/static/scarce//346/232/202/346/227/240/347/244/274/347/211/251.svg +1 -0
  183. package/src/admin/static/scarce//346/232/202/346/227/240/347/247/257/345/210/206.svg +1 -0
  184. package/src/admin/static/scarce//346/232/202/346/227/240/347/275/221/347/273/234.svg +1 -0
  185. package/src/admin/static/scarce//346/232/202/346/227/240/350/247/206/351/242/221.svg +1 -0
  186. package/src/admin/static/scarce//346/232/202/346/227/240/350/256/242/345/215/225.svg +1 -0
  187. package/src/admin/static/scarce//346/232/202/346/227/240/350/257/204/350/256/272.svg +1 -0
  188. package/src/admin/static/scarce//346/232/202/346/227/240/351/205/215/351/200/201.svg +1 -0
  189. package/src/admin/static/scarce//346/232/202/346/227/240/351/223/266/350/241/214/345/215/241.svg +1 -0
  190. package/src/admin/static/scarce//346/232/202/346/227/240/351/237/263/344/271/220.svg +1 -0
  191. package/src/admin/static/scarce//351/241/265/351/235/242/344/270/215/345/255/230/345/234/250.svg +1 -0
  192. package/src/admin/stores/app.ts +30 -0
  193. package/src/admin/stores/tags.ts +73 -0
  194. package/src/admin/stores/user.ts +52 -0
  195. package/src/admin/styles/base.css +137 -0
  196. package/src/admin/tsconfig.json +19 -0
  197. package/typings/admin/base/auth.ts +23 -0
  198. package/typings/admin/base/permission.ts +19 -0
  199. package/typings/admin/comm/cascader.ts +7 -0
  200. package/typings/admin/comm/check-tree.ts +7 -0
  201. package/typings/admin/comm/crud.ts +294 -0
  202. package/typings/admin/comm/eps.ts +52 -0
  203. package/typings/admin/comm/request.ts +12 -0
  204. package/typings/admin/comm/service.ts +35 -0
  205. package/typings/admin/comm/upload.ts +39 -0
  206. package/typings/admin/host.d.ts +61 -0
  207. package/typings/admin/vome/browser.ts +11 -0
  208. package/typings/admin/vome/module.ts +13 -0
  209. package/typings/admin/vome/tags.ts +9 -0
@@ -0,0 +1,852 @@
1
+ <template>
2
+ <div
3
+ class="vm-crud-table"
4
+ :class="{
5
+ 'is-border': border,
6
+ 'is-auto-height': autoHeight,
7
+ }"
8
+ >
9
+ <div v-if="crud.loading.value" class="vm-crud-table__loading">加载中…</div>
10
+ <div ref="scrollRef" class="vm-crud-table__scroll vm-scroll">
11
+ <Table>
12
+ <TableHeader>
13
+ <TableRow class="vm-crud-table__head-row">
14
+ <TableHead v-if="showSelection" class="vm-crud-table__check">
15
+ <Checkbox :model-value="allChecked" @update:model-value="toggleAll" />
16
+ </TableHead>
17
+ <TableHead
18
+ v-for="(col, i) in dataColumns"
19
+ :key="col.prop || col.type || i"
20
+ class="vm-crud-table__th"
21
+ :class="{
22
+ 'vm-crud-table__op': col.type === 'op',
23
+ 'is-sortable': isSortable(col),
24
+ }"
25
+ :style="colStyle(col)"
26
+ @click="onSortClick(col)"
27
+ >
28
+ <span class="vm-crud-table__th-inner">
29
+ {{ col.type === 'op' ? col.label || crud.dict.label.op : col.label }}
30
+ <i
31
+ v-if="isSortable(col) && col.prop"
32
+ class="vm-crud-table__sort"
33
+ :class="sortIconClass(col.prop)"
34
+ />
35
+ </span>
36
+ </TableHead>
37
+ <TableHead
38
+ v-if="showSlotOp"
39
+ class="vm-crud-table__th vm-crud-table__op"
40
+ :style="{ width: `${opWidth}px`, minWidth: `${opWidth}px` }"
41
+ >
42
+ {{ crud.dict.label.op }}
43
+ </TableHead>
44
+ </TableRow>
45
+ </TableHeader>
46
+ <TableBody>
47
+ <!-- empty-block:表体内 sticky,宽度=可视区,不盖表头、不跟总表宽跑 -->
48
+ <TableRow
49
+ v-if="!displayRows.length && !crud.loading.value"
50
+ class="vm-crud-table__empty-row"
51
+ >
52
+ <TableCell :colspan="colSpan" class="vm-crud-table__empty-cell">
53
+ <div class="vm-crud-table__empty-block" :style="emptyBlockStyle">
54
+ <vm-empty kind="暂无数据" />
55
+ </div>
56
+ </TableCell>
57
+ </TableRow>
58
+ <TableRow
59
+ v-for="(row, rowIndex) in displayRows"
60
+ :key="String(row[rowKey])"
61
+ class="vm-crud-table__row"
62
+ :class="{ 'is-current': highlight && String(row[rowKey]) === currentId }"
63
+ @click="onRowClick(row)"
64
+ @contextmenu="(e: MouseEvent) => onContextMenu(e, row)"
65
+ >
66
+ <TableCell v-if="showSelection" class="vm-crud-table__check">
67
+ <Checkbox
68
+ :model-value="selectedIds.has(String(row[rowKey]))"
69
+ @update:model-value="(v) => toggleRow(row, v)"
70
+ />
71
+ </TableCell>
72
+ <TableCell
73
+ v-for="(col, i) in dataColumns"
74
+ :key="col.prop || col.type || i"
75
+ class="vm-crud-table__td"
76
+ :class="{ 'vm-crud-table__op': col.type === 'op' }"
77
+ :style="colStyle(col)"
78
+ >
79
+ <template v-if="col.type === 'index'">
80
+ {{ displayIndex(rowIndex) }}
81
+ </template>
82
+ <template v-else-if="col.type === 'op'">
83
+ <div
84
+ v-if="crud.trashMode.value === 'onlyTrashed'"
85
+ class="vm-crud-table__ops"
86
+ >
87
+ <Button size="xs" variant="softPrimary" @click="crud.rowRestore([row])">
88
+ 恢复
89
+ </Button>
90
+ <Button
91
+ size="xs"
92
+ variant="softDestructive"
93
+ @click="crud.rowDelete([row], true)"
94
+ >
95
+ 彻底删除
96
+ </Button>
97
+ </div>
98
+ <div v-else class="vm-crud-table__ops">
99
+ <template v-for="(btn, bi) in resolveButtons(col, row)" :key="bi">
100
+ <Button
101
+ v-if="btn === 'edit' && crud.getPermission('update')"
102
+ size="xs"
103
+ variant="softPrimary"
104
+ @click="crud.rowEdit(row)"
105
+ >
106
+ {{ crud.dict.label.update }}
107
+ </Button>
108
+ <Button
109
+ v-else-if="btn === 'delete' && crud.getPermission('delete')"
110
+ size="xs"
111
+ variant="softDestructive"
112
+ @click="crud.rowDelete([row])"
113
+ >
114
+ {{ crud.dict.label.delete }}
115
+ </Button>
116
+ <Button
117
+ v-else-if="btn === 'info' && crud.getPermission('info')"
118
+ size="xs"
119
+ variant="ghost"
120
+ @click="crud.rowInfo(row)"
121
+ >
122
+ {{ crud.dict.label.info }}
123
+ </Button>
124
+ <Button
125
+ v-else-if="typeof btn === 'object' && !btn.hidden"
126
+ size="xs"
127
+ :variant="btn.variant || 'softSuccess'"
128
+ @click="btn.onClick(row)"
129
+ >
130
+ {{ btn.label }}
131
+ </Button>
132
+ </template>
133
+ </div>
134
+ </template>
135
+ <div
136
+ v-else-if="isTree && i === 0"
137
+ class="vm-crud-table__tree-cell"
138
+ :style="{ paddingLeft: `${(row.depth || 0) * 18}px` }"
139
+ >
140
+ <button
141
+ v-if="row.hasChildren"
142
+ type="button"
143
+ class="vm-crud-table__fold"
144
+ @click="toggleExpand(row[rowKey] as string | number)"
145
+ >
146
+ <i
147
+ :class="
148
+ expanded.has(row[rowKey] as string | number)
149
+ ? 'ri-arrow-down-s-line'
150
+ : 'ri-arrow-right-s-line'
151
+ "
152
+ />
153
+ </button>
154
+ <span v-else class="vm-crud-table__fold is-spacer" />
155
+ <slot
156
+ :name="col.slot || `cell-${col.prop}`"
157
+ :row="row"
158
+ :value="col.prop ? row[col.prop] : undefined"
159
+ >
160
+ <component
161
+ :is="cellComp(col)"
162
+ v-if="cellComp(col)"
163
+ :model-value="col.prop ? row[col.prop] : undefined"
164
+ :scope="row"
165
+ :column="{ property: col.prop, prop: col.prop }"
166
+ :options="resolveDict(col)"
167
+ v-bind="col.component?.props || {}"
168
+ />
169
+ <vm-dict-tag
170
+ v-else-if="resolveDict(col).length"
171
+ :model-value="col.prop ? (row[col.prop] as string | number) : null"
172
+ :options="resolveDict(col)"
173
+ />
174
+ <template v-else>{{ cellText(col, row) }}</template>
175
+ </slot>
176
+ </div>
177
+ <slot
178
+ v-else
179
+ :name="col.slot || `cell-${col.prop}`"
180
+ :row="row"
181
+ :value="col.prop ? row[col.prop] : undefined"
182
+ >
183
+ <component
184
+ :is="cellComp(col)"
185
+ v-if="cellComp(col)"
186
+ :model-value="col.prop ? row[col.prop] : undefined"
187
+ :scope="row"
188
+ :column="{ property: col.prop, prop: col.prop }"
189
+ :options="resolveDict(col)"
190
+ v-bind="col.component?.props || {}"
191
+ />
192
+ <vm-dict-tag
193
+ v-else-if="resolveDict(col).length"
194
+ :model-value="col.prop ? (row[col.prop] as string | number) : null"
195
+ :options="resolveDict(col)"
196
+ />
197
+ <template v-else>{{ cellText(col, row) }}</template>
198
+ </slot>
199
+ </TableCell>
200
+ <TableCell v-if="showSlotOp" class="vm-crud-table__td">
201
+ <div
202
+ v-if="crud.trashMode.value === 'onlyTrashed'"
203
+ class="vm-crud-table__ops"
204
+ >
205
+ <Button size="xs" variant="softPrimary" @click="crud.rowRestore([row])">
206
+ 恢复
207
+ </Button>
208
+ <Button
209
+ size="xs"
210
+ variant="softDestructive"
211
+ @click="crud.rowDelete([row], true)"
212
+ >
213
+ 彻底删除
214
+ </Button>
215
+ </div>
216
+ <slot v-else name="op" :row="row" />
217
+ </TableCell>
218
+ </TableRow>
219
+ </TableBody>
220
+ </Table>
221
+ </div>
222
+ <vm-context-menu ref="ctxRef" :items="ctxItems" @select="onCtxSelect" />
223
+ </div>
224
+ </template>
225
+
226
+ <script setup lang="ts">
227
+ import { useCrud } from './useCrud'
228
+ import { injectTableOptions, TABLE_API_KEY } from './key'
229
+ import { flattenTree } from '../lib/tree'
230
+ import { resolveCrudComponent } from './registry'
231
+ import { getCrudStyle } from './style'
232
+ defineOptions({ name: 'vm-table' })
233
+
234
+ const props = withDefaults(
235
+ defineProps<{
236
+ columns?: CrudColumn[]
237
+ selection?: boolean
238
+ rowKey?: string
239
+ }>(),
240
+ {
241
+ selection: undefined,
242
+ rowKey: 'id',
243
+ },
244
+ )
245
+
246
+ const slots = useSlots()
247
+ const crud = useCrud()
248
+ const tableOpts = injectTableOptions()
249
+ const style = getCrudStyle().table
250
+
251
+ const scrollRef = ref<HTMLElement | null>(null)
252
+ /** empty-block:宽度取滚动容器可视宽,不跟表内容总宽 */
253
+ const emptyWidth = ref(0)
254
+
255
+ function syncEmptyWidth() {
256
+ const el = scrollRef.value
257
+ if (!el) return
258
+ emptyWidth.value = el.clientWidth
259
+ }
260
+
261
+ const emptyBlockStyle = computed(() => ({
262
+ width: emptyWidth.value ? `${emptyWidth.value}px` : '100%',
263
+ }))
264
+
265
+ let emptyRo: ResizeObserver | null = null
266
+
267
+ const overrideColumns = ref<CrudColumn[] | null>(null)
268
+
269
+ const columns = computed(() => {
270
+ const raw =
271
+ overrideColumns.value ||
272
+ (props.columns?.length ? props.columns : tableOpts?.columns || [])
273
+ return raw.filter((c) => !c.hidden)
274
+ })
275
+
276
+ const hasSelectionCol = computed(() => columns.value.some((c) => c.type === 'selection'))
277
+ const hasOpCol = computed(() => columns.value.some((c) => c.type === 'op'))
278
+ const showSelection = computed(() =>
279
+ props.selection !== undefined ? props.selection : hasSelectionCol.value,
280
+ )
281
+ const dataColumns = computed(() =>
282
+ columns.value.filter((c) => c.type !== 'selection'),
283
+ )
284
+ const showSlotOp = computed(() => Boolean(slots.op) && !hasOpCol.value)
285
+
286
+ const colSpan = computed(
287
+ () =>
288
+ dataColumns.value.length +
289
+ (showSelection.value ? 1 : 0) +
290
+ (showSlotOp.value ? 1 : 0),
291
+ )
292
+
293
+ const rowKey = computed(() => tableOpts?.rowKey || props.rowKey)
294
+ const isTree = computed(() => Boolean(tableOpts?.tree))
295
+ const border = computed(() => tableOpts?.border ?? style.border)
296
+ const autoHeight = computed(() => tableOpts?.autoHeight ?? style.autoHeight)
297
+ const highlight = computed(() => style.highlightCurrentRow)
298
+ const opWidth = computed(() => style.column.opWidth)
299
+ const defaultAlign = computed(() => style.column.align)
300
+ const sortRefresh = computed(() => tableOpts?.sortRefresh !== false)
301
+
302
+ function normalizeSortDir(
303
+ order?: string,
304
+ ): 'asc' | 'desc' | undefined {
305
+ if (order === 'asc' || order === 'ascending') return 'asc'
306
+ if (order === 'desc' || order === 'descending') return 'desc'
307
+ return undefined
308
+ }
309
+
310
+ function initSortState() {
311
+ const ds = tableOpts?.defaultSort
312
+ if (!ds?.prop) return null
313
+ return {
314
+ prop: ds.prop,
315
+ order: normalizeSortDir(ds.order) || ('asc' as const),
316
+ }
317
+ }
318
+
319
+ const sortState = ref<{ prop: string; order: 'asc' | 'desc' } | null>(
320
+ initSortState(),
321
+ )
322
+
323
+ /** dict.sort → 本项目为 { order: 字段, sort: 方向 } */
324
+ function sortPayload(prop: string, order: 'asc' | 'desc') {
325
+ return {
326
+ [crud.dict.sort.prop]: prop,
327
+ [crud.dict.sort.order]: order,
328
+ }
329
+ }
330
+
331
+ function isSortable(col: CrudColumn) {
332
+ if (!col.prop || col.type === 'op' || col.type === 'selection' || col.type === 'index') {
333
+ return false
334
+ }
335
+ return Boolean(col.sortable)
336
+ }
337
+
338
+ function sortIconClass(prop: string) {
339
+ if (sortState.value?.prop !== prop) return 'ri-arrow-up-down-line is-muted'
340
+ return sortState.value.order === 'asc'
341
+ ? 'ri-arrow-up-s-fill is-active'
342
+ : 'ri-arrow-down-s-fill is-active'
343
+ }
344
+
345
+ function displayIndex(idx: string | number) {
346
+ return Number(idx) + 1
347
+ }
348
+
349
+ function onSortClick(col: CrudColumn) {
350
+ if (!isSortable(col) || !col.prop) return
351
+ const prop = col.prop
352
+ let next: 'asc' | 'desc' = 'asc'
353
+ const current = sortState.value
354
+ if (current?.prop === prop) {
355
+ next = current.order === 'asc' ? 'desc' : 'asc'
356
+ } else {
357
+ const hint = normalizeSortDir(
358
+ typeof col.sortable === 'string' ? col.sortable : undefined,
359
+ )
360
+ next = hint || 'asc'
361
+ }
362
+ sortState.value = { prop, order: next }
363
+ if (sortRefresh.value) void crud.refresh(sortPayload(prop, next))
364
+ }
365
+
366
+ /** col.width / col.minWidth 优先;op 列未设 width 时回退全局 opWidth */
367
+ function toPx(v: string | number) {
368
+ return typeof v === 'number' ? `${v}px` : v
369
+ }
370
+
371
+ function colStyle(col: CrudColumn) {
372
+ if (col.type === 'op') {
373
+ const w = col.width ?? opWidth.value
374
+ return {
375
+ // 固定操作列宽,剩余空间留给内容列
376
+ width: toPx(w),
377
+ minWidth: toPx(w),
378
+ whiteSpace: 'nowrap' as const,
379
+ textAlign: (col.align || defaultAlign.value) as string | undefined,
380
+ }
381
+ }
382
+ const w = col.width
383
+ return {
384
+ width: w != null ? toPx(w) : undefined,
385
+ minWidth: col.minWidth != null ? toPx(col.minWidth) : undefined,
386
+ textAlign: (col.align || defaultAlign.value) as string | undefined,
387
+ }
388
+ }
389
+ const ctxEnabled = computed(() => {
390
+ const c = tableOpts?.contextMenu ?? style.contextMenu
391
+ return c !== false && !(Array.isArray(c) && c.length === 0)
392
+ })
393
+
394
+ const expanded = ref(new Set<string | number>())
395
+ const currentId = ref('')
396
+ const ctxRef = ref<{ open: (e: MouseEvent) => void } | null>(null)
397
+ const ctxRow = ref<Record<string, unknown> | null>(null)
398
+
399
+ const ctxItems = computed<CtxMenuItem[]>(() => {
400
+ const conf = tableOpts?.contextMenu ?? style.contextMenu
401
+ const keys = Array.isArray(conf)
402
+ ? conf
403
+ : ['refresh', 'check', 'edit', 'delete', 'order-asc', 'order-desc']
404
+ const map: Record<string, string> = {
405
+ refresh: crud.dict.label.refresh,
406
+ check: crud.dict.label.select,
407
+ edit: crud.dict.label.update,
408
+ delete: crud.dict.label.delete,
409
+ 'order-asc': crud.dict.label.asc,
410
+ 'order-desc': crud.dict.label.desc,
411
+ }
412
+ return keys.map((k) => ({
413
+ key: k,
414
+ label: map[k] || k,
415
+ disabled:
416
+ (k === 'edit' && !crud.getPermission('update')) ||
417
+ (k === 'delete' && !crud.getPermission('delete')),
418
+ }))
419
+ })
420
+
421
+ watch(
422
+ () => crud.list.value,
423
+ (rows) => {
424
+ if (!isTree.value || expanded.value.size) return
425
+ const ids = rows
426
+ .filter((r) => Array.isArray(r.children) && (r.children as unknown[]).length)
427
+ .map((r) => r[rowKey.value] as string | number)
428
+ if (ids.length) expanded.value = new Set(ids)
429
+ },
430
+ { immediate: true },
431
+ )
432
+
433
+ type FlatRow = Record<string, unknown> & {
434
+ depth?: number
435
+ hasChildren?: boolean
436
+ }
437
+
438
+ const displayRows = computed<FlatRow[]>(() => {
439
+ const rows = crud.list.value as FlatRow[]
440
+ if (!isTree.value) return rows
441
+ return flattenTree(rows as never, expanded.value) as FlatRow[]
442
+ })
443
+
444
+ const selectedIds = computed(
445
+ () => new Set(crud.selection.value.map((r) => String(r[rowKey.value]))),
446
+ )
447
+
448
+ const allChecked = computed(() => {
449
+ const rows = displayRows.value
450
+ return (
451
+ rows.length > 0 &&
452
+ rows.every((r) => selectedIds.value.has(String(r[rowKey.value])))
453
+ )
454
+ })
455
+
456
+ /** 自身 + 全部子孙(树勾选级联) */
457
+ function collectSubtree(row: Record<string, unknown>): Record<string, unknown>[] {
458
+ const out: Record<string, unknown>[] = [row]
459
+ const kids = row.children as Record<string, unknown>[] | undefined
460
+ if (!kids?.length) return out
461
+ for (const c of kids) out.push(...collectSubtree(c))
462
+ return out
463
+ }
464
+
465
+ function toggleAll(checked: boolean | 'indeterminate') {
466
+ if (checked === true) {
467
+ // 树表:含折叠未展示的子孙
468
+ if (isTree.value) {
469
+ const all: Record<string, unknown>[] = []
470
+ for (const r of crud.list.value) all.push(...collectSubtree(r))
471
+ crud.setSelection(all)
472
+ return
473
+ }
474
+ crud.setSelection([...displayRows.value])
475
+ } else {
476
+ crud.setSelection([])
477
+ }
478
+ }
479
+
480
+ function toggleRow(row: Record<string, unknown>, checked: boolean | 'indeterminate') {
481
+ const key = rowKey.value
482
+ if (isTree.value) {
483
+ const subtree = collectSubtree(row)
484
+ const ids = new Set(subtree.map((r) => String(r[key])))
485
+ if (checked === true) {
486
+ const map = new Map(
487
+ crud.selection.value.map((r) => [String(r[key]), r] as const),
488
+ )
489
+ for (const r of subtree) map.set(String(r[key]), r)
490
+ crud.setSelection([...map.values()])
491
+ } else {
492
+ crud.setSelection(
493
+ crud.selection.value.filter((r) => !ids.has(String(r[key]))),
494
+ )
495
+ }
496
+ return
497
+ }
498
+ const id = String(row[key])
499
+ if (checked === true) {
500
+ if (!selectedIds.value.has(id)) crud.setSelection([...crud.selection.value, row])
501
+ } else {
502
+ crud.setSelection(
503
+ crud.selection.value.filter((r) => String(r[key]) !== id),
504
+ )
505
+ }
506
+ }
507
+
508
+ function toggleExpand(id: string | number) {
509
+ const next = new Set(expanded.value)
510
+ if (next.has(id)) next.delete(id)
511
+ else next.add(id)
512
+ expanded.value = next
513
+ }
514
+
515
+ function resolveButtons(col: CrudColumn, row: Record<string, unknown>): CrudOpButton[] {
516
+ const raw = col.buttons
517
+ if (typeof raw === 'function') return raw(row)
518
+ return raw?.length ? raw : (['edit', 'delete'] as CrudOpButton[])
519
+ }
520
+
521
+ function cellComp(col: CrudColumn) {
522
+ return resolveCrudComponent(col.component?.name)
523
+ }
524
+
525
+ /** dict: dict.get('x') → ComputedRef */
526
+ function resolveDict(col: CrudColumn) {
527
+ const list = col.dict == null ? [] : toValue(col.dict)
528
+ return Array.isArray(list) ? list : []
529
+ }
530
+
531
+ function cellText(col: CrudColumn, row: Record<string, unknown>) {
532
+ if (!col.prop) return ''
533
+ const val = row[col.prop]
534
+ if (col.formatter) return col.formatter(row, val)
535
+ if (val == null) return ''
536
+ return String(val)
537
+ }
538
+
539
+ function onRowClick(row: Record<string, unknown>) {
540
+ if (highlight.value) currentId.value = String(row[rowKey.value])
541
+ }
542
+
543
+ function onContextMenu(e: MouseEvent, row: Record<string, unknown>) {
544
+ if (!ctxEnabled.value) return
545
+ ctxRow.value = row
546
+ ctxRef.value?.open(e)
547
+ }
548
+
549
+ function onCtxSelect(key: string) {
550
+ const row = ctxRow.value
551
+ if (!row) return
552
+ if (key === 'refresh') void crud.refresh()
553
+ if (key === 'check') toggleRow(row, true)
554
+ if (key === 'edit') void crud.rowEdit(row)
555
+ if (key === 'delete') void crud.rowDelete([row])
556
+ if (key === 'order-asc') {
557
+ sortState.value = { prop: 'orderNum', order: 'asc' }
558
+ void crud.refresh(sortPayload('orderNum', 'asc'))
559
+ }
560
+ if (key === 'order-desc') {
561
+ sortState.value = { prop: 'orderNum', order: 'desc' }
562
+ void crud.refresh(sortPayload('orderNum', 'desc'))
563
+ }
564
+ }
565
+
566
+ function baseColumns(): CrudColumn[] {
567
+ return (
568
+ overrideColumns.value ||
569
+ (props.columns?.length ? props.columns : tableOpts?.columns || [])
570
+ )
571
+ }
572
+
573
+ function setColumns(cols: CrudColumn[]) {
574
+ overrideColumns.value = cols
575
+ }
576
+
577
+ function showColumn(prop: string) {
578
+ setColumns(
579
+ baseColumns().map((c) =>
580
+ c.prop === prop ? { ...c, hidden: false } : c,
581
+ ),
582
+ )
583
+ }
584
+
585
+ function hideColumn(prop: string) {
586
+ setColumns(
587
+ baseColumns().map((c) =>
588
+ c.prop === prop ? { ...c, hidden: true } : c,
589
+ ),
590
+ )
591
+ }
592
+
593
+ function getColumns() {
594
+ return baseColumns()
595
+ }
596
+
597
+ function columnCustomKey() {
598
+ return `column-custom__${typeof location !== 'undefined' ? location.pathname : 'default'}`
599
+ }
600
+
601
+ function applyColumnCustomMap(
602
+ cols: CrudColumn[],
603
+ map: Record<string, { checked: boolean; order?: number }>,
604
+ ) {
605
+ const ordered = [...cols].sort((a, b) => {
606
+ const ao = a.prop != null ? map[a.prop]?.order ?? 999 : 999
607
+ const bo = b.prop != null ? map[b.prop]?.order ?? 999 : 999
608
+ return ao - bo
609
+ })
610
+ return ordered.map((c) => {
611
+ if (!c.prop || c.type === 'selection' || c.type === 'op') return c
612
+ return { ...c, hidden: !(map[c.prop]?.checked ?? true) }
613
+ })
614
+ }
615
+
616
+ onMounted(() => {
617
+ syncEmptyWidth()
618
+ if (typeof ResizeObserver !== 'undefined' && scrollRef.value) {
619
+ emptyRo = new ResizeObserver(() => syncEmptyWidth())
620
+ emptyRo.observe(scrollRef.value)
621
+ }
622
+
623
+ try {
624
+ const raw = localStorage.getItem(columnCustomKey())
625
+ if (!raw) return
626
+ const map = JSON.parse(raw) as Record<
627
+ string,
628
+ { checked: boolean; order?: number }
629
+ >
630
+ const base = props.columns?.length
631
+ ? props.columns
632
+ : tableOpts?.columns || []
633
+ if (!base.length) return
634
+ overrideColumns.value = applyColumnCustomMap(base, map)
635
+ } catch {
636
+ /* ignore */
637
+ }
638
+ })
639
+
640
+ onBeforeUnmount(() => {
641
+ emptyRo?.disconnect()
642
+ emptyRo = null
643
+ })
644
+
645
+ watch(
646
+ () => displayRows.value.length,
647
+ () => {
648
+ requestAnimationFrame(syncEmptyWidth)
649
+ },
650
+ )
651
+
652
+ provide(TABLE_API_KEY, {
653
+ setColumns,
654
+ showColumn,
655
+ hideColumn,
656
+ getColumns,
657
+ })
658
+
659
+ defineExpose({
660
+ showColumn,
661
+ hideColumn,
662
+ setColumns,
663
+ data: displayRows,
664
+ })
665
+ </script>
666
+
667
+ <style lang="scss" scoped>
668
+ .vm-crud-table {
669
+ position: relative;
670
+ display: flex;
671
+ width: 100%;
672
+ min-width: 0;
673
+ min-height: 0;
674
+ flex: 1 1 auto;
675
+ flex-direction: column;
676
+ overflow: hidden;
677
+ border-radius: 16px;
678
+
679
+ &.is-border {
680
+ border: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
681
+ }
682
+
683
+ &.is-auto-height {
684
+ min-height: 0;
685
+ }
686
+ }
687
+
688
+ .vm-crud-table__scroll {
689
+ width: 100%;
690
+ min-width: 0;
691
+ min-height: 0;
692
+ flex: 1 1 auto;
693
+ overflow: auto;
694
+ overscroll-behavior: contain;
695
+ }
696
+
697
+ /* 表铺满容器;列 minWidth 过大时由 __scroll 横向滚动 */
698
+ :deep([data-slot='table-container']) {
699
+ width: 100%;
700
+ min-width: 100%;
701
+ overflow: visible !important;
702
+ }
703
+
704
+ :deep([data-slot='table']) {
705
+ width: 100%;
706
+ }
707
+
708
+ .vm-crud-table__loading {
709
+ position: absolute;
710
+ inset: 0;
711
+ z-index: 10;
712
+ display: flex;
713
+ align-items: center;
714
+ justify-content: center;
715
+ background: color-mix(in srgb, var(--card) 70%, transparent);
716
+ color: var(--muted-foreground);
717
+ font-size: 12px;
718
+ backdrop-filter: blur(1px);
719
+ }
720
+
721
+ /* empty-block:在表体内,sticky 贴可视左缘,不盖表头 */
722
+ :deep(.vm-crud-table__empty-row) {
723
+ &:hover {
724
+ background: transparent !important;
725
+ }
726
+ }
727
+
728
+ :deep(.vm-crud-table__empty-cell) {
729
+ height: 240px;
730
+ padding: 0 !important;
731
+ border-bottom: none !important;
732
+ }
733
+
734
+ .vm-crud-table__empty-block {
735
+ position: sticky;
736
+ left: 0;
737
+ display: flex;
738
+ height: 240px;
739
+ align-items: center;
740
+ justify-content: center;
741
+ overflow: hidden;
742
+ }
743
+
744
+ :deep(.vm-crud-table__head-row) {
745
+ border-color: color-mix(in srgb, var(--border) 70%, transparent);
746
+ background: transparent;
747
+
748
+ &:hover {
749
+ background: transparent !important;
750
+ }
751
+ }
752
+
753
+ :deep(.vm-crud-table__th) {
754
+ height: 42px;
755
+ color: var(--muted-foreground);
756
+ font-size: 12px;
757
+ font-weight: 600;
758
+ white-space: nowrap;
759
+
760
+ &.is-sortable {
761
+ cursor: pointer;
762
+ user-select: none;
763
+
764
+ &:hover {
765
+ color: var(--foreground);
766
+ }
767
+ }
768
+ }
769
+
770
+ .vm-crud-table__th-inner {
771
+ display: inline-flex;
772
+ align-items: center;
773
+ gap: 4px;
774
+ }
775
+
776
+ .vm-crud-table__sort {
777
+ font-size: 14px;
778
+ line-height: 1;
779
+ opacity: 0.45;
780
+
781
+ &.is-active {
782
+ color: var(--brand);
783
+ opacity: 1;
784
+ }
785
+
786
+ &.is-muted {
787
+ opacity: 0.35;
788
+ }
789
+ }
790
+
791
+ .vm-crud-table__ops {
792
+ display: inline-flex;
793
+ flex-wrap: nowrap;
794
+ gap: 8px;
795
+ white-space: nowrap;
796
+ }
797
+
798
+ :deep(.vm-crud-table__op) {
799
+ white-space: nowrap;
800
+ }
801
+
802
+ :deep(.vm-crud-table__check) {
803
+ width: 40px;
804
+ padding-left: 4px;
805
+ }
806
+
807
+ :deep(.vm-crud-table__row) {
808
+ height: 48px;
809
+ border-color: color-mix(in srgb, var(--border) 55%, transparent);
810
+ transition: background 0.15s ease;
811
+
812
+ &:hover,
813
+ &.is-current {
814
+ background: var(--brand-soft) !important;
815
+ }
816
+ }
817
+
818
+ :deep(.vm-crud-table__td) {
819
+ padding-top: 10px;
820
+ padding-bottom: 10px;
821
+ font-size: 13px;
822
+ }
823
+
824
+ .vm-crud-table__tree-cell {
825
+ display: flex;
826
+ align-items: center;
827
+ gap: 4px;
828
+ }
829
+
830
+ .vm-crud-table__fold {
831
+ display: inline-flex;
832
+ width: 22px;
833
+ height: 22px;
834
+ flex-shrink: 0;
835
+ align-items: center;
836
+ justify-content: center;
837
+ border: none;
838
+ border-radius: 8px;
839
+ background: transparent;
840
+ color: var(--muted-foreground);
841
+ cursor: pointer;
842
+
843
+ &.is-spacer {
844
+ cursor: default;
845
+ }
846
+
847
+ &:not(.is-spacer):hover {
848
+ background: var(--muted);
849
+ color: var(--foreground);
850
+ }
851
+ }
852
+ </style>