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,68 @@
1
+ <template>
2
+ <button
3
+ v-if="visible"
4
+ type="button"
5
+ class="vm-crud-toolbar__btn is-primary"
6
+ @click="onClick"
7
+ >
8
+ <i class="ri-add-line" />
9
+ {{ crud.dict.label.add }}
10
+ </button>
11
+ </template>
12
+
13
+ <script setup lang="ts">
14
+ import { useCrud } from './useCrud'
15
+
16
+ defineOptions({ name: 'vm-add-btn' })
17
+
18
+ const props = withDefaults(
19
+ defineProps<{
20
+ /** 打开新增表单时写入的默认值 */
21
+ data?: Record<string, unknown> | (() => Record<string, unknown>)
22
+ /** 回收站中隐藏(默认 true) */
23
+ hideInTrash?: boolean
24
+ }>(),
25
+ {
26
+ hideInTrash: true,
27
+ },
28
+ )
29
+
30
+ const crud = useCrud()
31
+
32
+ /** 权限来自 service._permission / useCrud.permission */
33
+ const visible = computed(() => {
34
+ if (!crud.getPermission('add')) return false
35
+ if (props.hideInTrash && crud.trashMode.value === 'onlyTrashed') return false
36
+ return true
37
+ })
38
+
39
+ function onClick() {
40
+ const raw = props.data
41
+ const defaults = typeof raw === 'function' ? raw() : raw
42
+ crud.rowAdd(defaults && Object.keys(defaults).length ? defaults : {})
43
+ }
44
+ </script>
45
+
46
+ <style lang="scss" scoped>
47
+ .vm-crud-toolbar__btn {
48
+ display: inline-flex;
49
+ height: 36px;
50
+ align-items: center;
51
+ gap: 4px;
52
+ padding: 0 14px;
53
+ border: none;
54
+ border-radius: 12px;
55
+ font-size: 13px;
56
+ font-weight: 600;
57
+ cursor: pointer;
58
+
59
+ &.is-primary {
60
+ background: var(--brand);
61
+ color: #fff;
62
+
63
+ &:hover {
64
+ background: var(--brand-deep);
65
+ }
66
+ }
67
+ }
68
+ </style>
@@ -0,0 +1,73 @@
1
+ <template>
2
+ <div class="vm-adv-search">
3
+ <Button size="sm" variant="outline" class="h-8" @click="open = true">
4
+ <i class="ri-filter-3-line" />
5
+ {{ crud.dict.label.advSearch }}
6
+ </Button>
7
+ <vm-dialog v-model:open="open" :title="crud.dict.label.advSearch" width="640px">
8
+ <vm-form v-model="form" :items="items" />
9
+ <template #footer>
10
+ <button type="button" class="vm-adv-search__btn is-ghost" @click="reset">
11
+ {{ crud.dict.label.reset }}
12
+ </button>
13
+ <button type="button" class="vm-adv-search__btn is-primary" @click="search">
14
+ {{ crud.dict.label.search }}
15
+ </button>
16
+ </template>
17
+ </vm-dialog>
18
+ </div>
19
+ </template>
20
+
21
+ <script setup lang="ts">
22
+ import { useCrud } from './useCrud'
23
+ defineOptions({ name: 'vm-adv-search' })
24
+
25
+ const props = withDefaults(
26
+ defineProps<{
27
+ items?: CrudFormItem[]
28
+ }>(),
29
+ { items: () => [] },
30
+ )
31
+
32
+ const crud = useCrud()
33
+ const open = ref(false)
34
+ const form = ref<Record<string, unknown>>({})
35
+
36
+ function search() {
37
+ const payload: Record<string, unknown> = {}
38
+ for (const it of props.items) {
39
+ const v = form.value[it.prop]
40
+ if (v !== '' && v != null) payload[it.prop] = v
41
+ }
42
+ crud.setParams(payload, true)
43
+ open.value = false
44
+ }
45
+
46
+ function reset() {
47
+ const next: Record<string, unknown> = {}
48
+ for (const it of props.items) next[it.prop] = ''
49
+ form.value = next
50
+ crud.resetParams()
51
+ }
52
+ </script>
53
+
54
+ <style lang="scss" scoped>
55
+ .vm-adv-search__btn {
56
+ height: 36px;
57
+ padding: 0 14px;
58
+ border: none;
59
+ border-radius: 12px;
60
+ font-size: 13px;
61
+ font-weight: 600;
62
+ cursor: pointer;
63
+
64
+ &.is-ghost {
65
+ background: var(--muted);
66
+ }
67
+
68
+ &.is-primary {
69
+ background: var(--brand);
70
+ color: #fff;
71
+ }
72
+ }
73
+ </style>
@@ -0,0 +1,517 @@
1
+ <template>
2
+ <div class="vm-crud">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { toast } from 'vue-sonner'
9
+ import { resolveService } from '../service'
10
+ import { deepTree } from '../lib/tree'
11
+ import { mergeDict, mergePermission } from './config'
12
+ import { vmConfirm } from './confirm'
13
+ import { CRUD_KEY, CRUD_MITT_KEY, injectCrudOptions, injectTableOptions, injectUpsertOptions } from './key'
14
+ import { createMitt } from './mitt'
15
+ defineOptions({ name: 'vm-crud' })
16
+
17
+ const props = withDefaults(
18
+ defineProps<{
19
+ /** 直接传入 service;字符串如 'base.user';也可 useCrud({ service }) */
20
+ service?: CrudService | string
21
+ params?: Record<string, unknown>
22
+ immediate?: boolean
23
+ }>(),
24
+ {
25
+ params: () => ({}),
26
+ immediate: false,
27
+ },
28
+ )
29
+
30
+ const hookOptions = injectCrudOptions()
31
+ const tableOpts = injectTableOptions()
32
+ const upsertOpts = injectUpsertOptions()
33
+ const mitt = createMitt()
34
+ const searchActions = shallowRef<{ search: () => void; reset: () => void } | null>(null)
35
+ const hasSearchFilters = ref(false)
36
+
37
+ function pickService(): CrudService {
38
+ const s = props.service ?? hookOptions?.service
39
+ if (!s) return {} as CrudService
40
+ if (typeof s === 'string') {
41
+ return (resolveService(s) || {}) as CrudService
42
+ }
43
+ return s
44
+ }
45
+
46
+ function permFromService(s: CrudService): Partial<CrudPermission> | undefined {
47
+ if (s._permission && typeof s._permission === 'object') {
48
+ return s._permission as Partial<CrudPermission>
49
+ }
50
+ return undefined
51
+ }
52
+
53
+ const service = computed<CrudService>(() => pickService())
54
+
55
+ const dict = mergeDict(hookOptions?.dict)
56
+ const permission = ref(
57
+ mergePermission(hookOptions?.permission, permFromService(service.value)),
58
+ )
59
+
60
+ const loading = ref(false)
61
+ const list = ref<Record<string, unknown>[]>([])
62
+ const selection = ref<Record<string, unknown>[]>([])
63
+ const pagination = ref({ page: 1, size: 20, total: 0 })
64
+ const params = ref<Record<string, unknown>>({ ...props.params })
65
+ const trashMode = ref<CrudTrashMode>('default')
66
+
67
+ const upsertVisible = ref(false)
68
+ const upsertMode = ref<'add' | 'update' | 'info'>('add')
69
+ const upsertForm = ref<Record<string, unknown>>({})
70
+
71
+ function getPermission(key?: keyof typeof permission.value) {
72
+ if (!key) return { ...permission.value }
73
+ return Boolean(permission.value[key])
74
+ }
75
+
76
+ function getParams() {
77
+ return { ...params.value }
78
+ }
79
+
80
+ async function refresh(extra: Record<string, unknown> = {}) {
81
+ const svc = service.value
82
+ if (!svc.page && !svc.list) {
83
+ toast.error('CRUD service 缺少 page/list')
84
+ return
85
+ }
86
+
87
+ const body: Record<string, unknown> = {
88
+ ...params.value,
89
+ ...extra,
90
+ [dict.pagination.page]: pagination.value.page,
91
+ [dict.pagination.size]: pagination.value.size,
92
+ }
93
+ // defaultSort → 未带排序参数时注入(dict.sort → order/sort)
94
+ const ds = tableOpts?.defaultSort
95
+ if (ds?.prop && body[dict.sort.prop] == null) {
96
+ body[dict.sort.prop] = ds.prop
97
+ const o = ds.order
98
+ body[dict.sort.order] =
99
+ o === 'asc' || o === 'ascending' ? 'asc' : 'desc'
100
+ }
101
+ if (trashMode.value === 'onlyTrashed') body.onlyTrashed = true
102
+ if (trashMode.value === 'withTrashed') body.withTrashed = true
103
+
104
+ loading.value = true
105
+
106
+ const done = () => {
107
+ loading.value = false
108
+ }
109
+
110
+ const render = (
111
+ data:
112
+ | Record<string, unknown>[]
113
+ | { list: Record<string, unknown>[]; pagination?: unknown },
114
+ pag?: { page?: number; size?: number; total?: number },
115
+ ) => {
116
+ let rows: Record<string, unknown>[]
117
+ if (Array.isArray(data)) {
118
+ rows = data
119
+ if (pag) {
120
+ pagination.value = {
121
+ page: pag.page ?? pagination.value.page,
122
+ size: pag.size ?? pagination.value.size,
123
+ total: pag.total ?? data.length,
124
+ }
125
+ } else {
126
+ pagination.value = { ...pagination.value, total: data.length }
127
+ }
128
+ } else {
129
+ rows = data.list || []
130
+ const p = (pag || data.pagination || {}) as {
131
+ page?: number
132
+ size?: number
133
+ total?: number
134
+ }
135
+ pagination.value = {
136
+ page: p.page ?? pagination.value.page,
137
+ size: p.size ?? pagination.value.size,
138
+ total: p.total ?? 0,
139
+ }
140
+ }
141
+
142
+ // toTree:扁平 parentId → 树
143
+ if (tableOpts?.tree) {
144
+ const nested = rows.some((r) => Array.isArray(r.children) && r.children.length)
145
+ list.value = nested ? rows : (deepTree(rows as never) as Record<string, unknown>[])
146
+ } else {
147
+ list.value = rows
148
+ }
149
+
150
+ selection.value = []
151
+ done()
152
+ mitt.emit('crud.refresh', { list: list.value, pagination: pagination.value })
153
+ }
154
+
155
+ const next = async (req: Record<string, unknown>) => {
156
+ try {
157
+ if (svc.page) {
158
+ const data = await svc.page(req)
159
+ render(data)
160
+ return data
161
+ }
162
+ const rows = (await svc.list!(req)) || []
163
+ render(rows)
164
+ return { list: rows, pagination: pagination.value }
165
+ } catch (e) {
166
+ console.error(e)
167
+ done()
168
+ throw e
169
+ }
170
+ }
171
+
172
+ try {
173
+ if (hookOptions?.onRefresh) {
174
+ await hookOptions.onRefresh(body, { next, done, render })
175
+ } else {
176
+ await next(body)
177
+ }
178
+ } catch {
179
+ done()
180
+ }
181
+ }
182
+
183
+ function setParams(p: Record<string, unknown>, doRefresh = true) {
184
+ params.value = { ...params.value, ...p }
185
+ if (doRefresh) {
186
+ pagination.value = { ...pagination.value, page: 1 }
187
+ void refresh()
188
+ }
189
+ }
190
+
191
+ function resetParams() {
192
+ params.value = { ...props.params }
193
+ pagination.value = { ...pagination.value, page: 1 }
194
+ void refresh()
195
+ }
196
+
197
+ function setSelection(rows: Record<string, unknown>[]) {
198
+ selection.value = rows
199
+ }
200
+
201
+ function rowAdd(defaults: Record<string, unknown> = {}) {
202
+ if (!permission.value.add) return
203
+ upsertMode.value = 'add'
204
+ upsertForm.value = { ...defaults }
205
+ upsertVisible.value = true
206
+ mitt.emit('crud.proxy', { name: 'add', data: [defaults] })
207
+ }
208
+
209
+ function rowAppend(data: Record<string, unknown> = {}) {
210
+ rowAdd(data)
211
+ }
212
+
213
+ async function loadRowForm(row: Record<string, unknown>, mode: 'update' | 'info') {
214
+ upsertMode.value = mode
215
+ const svc = service.value
216
+
217
+ const next = async (data?: Record<string, unknown>) => {
218
+ if (svc.info && row[dict.primaryId] != null) {
219
+ return await svc.info({
220
+ id: row[dict.primaryId] as string | number,
221
+ ...data,
222
+ })
223
+ }
224
+ return { ...row, ...data }
225
+ }
226
+
227
+ const done = (data: Record<string, unknown>) => {
228
+ upsertForm.value = data
229
+ upsertVisible.value = true
230
+ }
231
+
232
+ const close = () => upsertClose()
233
+
234
+ // 自定义拉详情
235
+ if (upsertOpts?.onInfo) {
236
+ await upsertOpts.onInfo({ ...row }, { next, done, close })
237
+ return
238
+ }
239
+
240
+ // sync:直接用行数据打开,不调 info
241
+ if (upsertOpts?.sync) {
242
+ done({ ...row })
243
+ return
244
+ }
245
+
246
+ try {
247
+ done(await next())
248
+ } catch {
249
+ done({ ...row })
250
+ }
251
+ }
252
+
253
+ async function rowEdit(row: Record<string, unknown>) {
254
+ if (!permission.value.update) return
255
+ await loadRowForm(row, 'update')
256
+ mitt.emit('crud.proxy', { name: 'edit', data: [row] })
257
+ }
258
+
259
+ async function rowInfo(row: Record<string, unknown>) {
260
+ if (!permission.value.info) return
261
+ await loadRowForm(row, 'info')
262
+ mitt.emit('crud.proxy', { name: 'info', data: [row] })
263
+ }
264
+
265
+ function rowClose() {
266
+ upsertClose()
267
+ mitt.emit('crud.proxy', { name: 'close' })
268
+ }
269
+
270
+ /** 打开 upsert(不校验 add 权限;供转移部门等自定义场景) */
271
+ function openUpsert(
272
+ form: Record<string, unknown> = {},
273
+ mode: 'add' | 'update' | 'info' = 'add',
274
+ ) {
275
+ upsertMode.value = mode
276
+ upsertForm.value = { ...form }
277
+ upsertVisible.value = true
278
+ }
279
+
280
+ async function doDelete(targets: Record<string, unknown>[], force = false) {
281
+ const svc = service.value
282
+ if (!svc.delete) {
283
+ toast.error('未配置 delete')
284
+ return
285
+ }
286
+ const ids = targets
287
+ .map((r) => r[dict.primaryId] as string | number)
288
+ .filter((id) => id != null)
289
+ await svc.delete({ ids, force })
290
+ toast.success(force ? '已彻底删除' : dict.label.deleteSuccess || '已删除')
291
+ await refresh()
292
+ }
293
+
294
+ async function rowDelete(rows?: Record<string, unknown>[], force = false) {
295
+ // 回收站内删除一律硬删(避免操作列/批量删漏传 force 仍走软删文案)
296
+ const hard = force || trashMode.value === 'onlyTrashed'
297
+ if (!permission.value.delete && !hard) return
298
+ const targets = rows?.length ? rows : selection.value
299
+ if (!targets.length) {
300
+ toast.warning('请先选择数据')
301
+ return
302
+ }
303
+
304
+ const next = async (payload?: Record<string, unknown>) => {
305
+ if (payload?.ids) {
306
+ const svc = service.value
307
+ if (!svc.delete) throw new Error('未配置 delete')
308
+ await svc.delete({
309
+ ...(payload as { ids: Array<string | number> }),
310
+ force: hard,
311
+ })
312
+ toast.success(hard ? '已彻底删除' : dict.label.deleteSuccess || '已删除')
313
+ await refresh()
314
+ return
315
+ }
316
+ await doDelete(targets, hard)
317
+ }
318
+
319
+ if (hookOptions?.onDelete && !hard) {
320
+ await hookOptions.onDelete(targets, { next })
321
+ return
322
+ }
323
+
324
+ const tip = hard
325
+ ? dict.label.forceDeleteConfirm || '此操作将永久删除选中数据,是否继续?'
326
+ : dict.label.deleteConfirm || '确定删除?'
327
+ const ok = await vmConfirm({
328
+ title: hard ? '彻底删除' : '删除确认',
329
+ message: tip,
330
+ confirmText: '确定',
331
+ cancelText: '取消',
332
+ })
333
+ if (!ok) return
334
+ await next()
335
+ }
336
+
337
+ async function rowRestore(rows?: Record<string, unknown>[]) {
338
+ const targets = rows?.length ? rows : selection.value
339
+ if (!targets.length) {
340
+ toast.warning('请先选择数据')
341
+ return
342
+ }
343
+ const svc = service.value
344
+ if (!svc.restore) {
345
+ toast.error('未配置 restore')
346
+ return
347
+ }
348
+ if (!(await vmConfirm({ title: '恢复确认', message: '确定恢复?' }))) return
349
+ const ids = targets
350
+ .map((r) => r[dict.primaryId] as string | number)
351
+ .filter((id) => id != null)
352
+ await svc.restore({ ids })
353
+ toast.success('已恢复')
354
+ await refresh()
355
+ }
356
+
357
+ async function upsertSubmit() {
358
+ if (upsertMode.value === 'info') {
359
+ upsertClose()
360
+ return
361
+ }
362
+ // 系统时间字段由 BaseService.request 统一剥离,后端 Repository 再兜底
363
+ const data = { ...upsertForm.value }
364
+ const svc = service.value
365
+ try {
366
+ if (upsertMode.value === 'add') {
367
+ if (!svc.add) throw new Error('未配置 add')
368
+ await svc.add(data)
369
+ toast.success(dict.label.saveSuccess || '已新增')
370
+ } else {
371
+ if (!svc.update) throw new Error('未配置 update')
372
+ await svc.update(data)
373
+ toast.success(dict.label.saveSuccess || '已更新')
374
+ }
375
+ const after = async () => {
376
+ upsertVisible.value = false
377
+ await refresh()
378
+ }
379
+ if (upsertOpts?.onClose) {
380
+ upsertOpts.onClose('submit', () => {
381
+ void after()
382
+ })
383
+ } else {
384
+ await after()
385
+ }
386
+ } catch (e) {
387
+ console.error(e)
388
+ const err = e as Error & { toasted?: boolean }
389
+ const msg =
390
+ e instanceof Error && e.message ? e.message : '保存失败'
391
+ // request 已 toast 过则不再重复;本地校验/配置错误仍要提示
392
+ if (!err.toasted) toast.error(msg.slice(0, 200))
393
+ }
394
+ }
395
+
396
+ function upsertClose() {
397
+ const finish = () => {
398
+ upsertVisible.value = false
399
+ }
400
+ if (upsertOpts?.onClose) {
401
+ upsertOpts.onClose('close', finish)
402
+ return
403
+ }
404
+ finish()
405
+ }
406
+
407
+ /** 供页面可靠改表单(避免 template ref 解包 upsertForm 后误写) */
408
+ function patchUpsertForm(patch: Record<string, unknown>) {
409
+ upsertForm.value = { ...upsertForm.value, ...patch }
410
+ }
411
+
412
+ function getUpsertForm() {
413
+ return upsertForm.value
414
+ }
415
+
416
+ const ctx: CrudContext = {
417
+ service: service.value,
418
+ loading,
419
+ list,
420
+ selection,
421
+ pagination,
422
+ params,
423
+ trashMode,
424
+ permission,
425
+ dict,
426
+ refresh,
427
+ setParams,
428
+ resetParams,
429
+ setSelection,
430
+ getPermission: getPermission as CrudContext['getPermission'],
431
+ getParams,
432
+ rowAdd,
433
+ rowAppend,
434
+ rowEdit,
435
+ rowInfo,
436
+ rowClose,
437
+ openUpsert,
438
+ rowDelete,
439
+ rowRestore,
440
+ upsertVisible,
441
+ upsertMode,
442
+ upsertForm,
443
+ upsertSubmit,
444
+ upsertClose,
445
+ patchUpsertForm,
446
+ getUpsertForm,
447
+ searchActions,
448
+ hasSearchFilters,
449
+ }
450
+
451
+ provide(CRUD_KEY, ctx)
452
+ provide(CRUD_MITT_KEY, mitt)
453
+
454
+ watch(
455
+ () => props.params,
456
+ (v) => {
457
+ params.value = { ...v }
458
+ },
459
+ )
460
+
461
+ watch(
462
+ service,
463
+ (s) => {
464
+ ctx.service = s
465
+ permission.value = mergePermission(
466
+ hookOptions?.permission,
467
+ permFromService(s),
468
+ )
469
+ },
470
+ { immediate: true },
471
+ )
472
+
473
+ if (props.immediate) void refresh()
474
+
475
+ defineExpose({
476
+ ...ctx,
477
+ refresh,
478
+ setParams,
479
+ resetParams,
480
+ rowAdd,
481
+ rowAppend,
482
+ rowEdit,
483
+ rowInfo,
484
+ rowClose,
485
+ openUpsert,
486
+ rowDelete,
487
+ rowRestore,
488
+ upsertForm,
489
+ upsertClose,
490
+ patchUpsertForm,
491
+ getUpsertForm,
492
+ selection,
493
+ list,
494
+ pagination,
495
+ trashMode,
496
+ permission,
497
+ getPermission,
498
+ getParams,
499
+ })
500
+ </script>
501
+
502
+ <style lang="scss" scoped>
503
+ .vm-crud {
504
+ display: flex;
505
+ width: 100%;
506
+ min-width: 0;
507
+ min-height: 0;
508
+ flex: 1;
509
+ flex-direction: column;
510
+ gap: 8px;
511
+ padding: 12px 16px;
512
+ overflow: hidden;
513
+ border-radius: 24px;
514
+ background: var(--card);
515
+ box-shadow: var(--shadow-soft);
516
+ }
517
+ </style>