vome-core 0.0.6 → 0.0.8

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 (41) hide show
  1. package/dist/index.js +236 -1
  2. package/dist/server/index.js +3261 -1
  3. package/package.json +10 -2
  4. package/src/admin/components/vm-auto-perm-dialog.vue +3 -0
  5. package/src/admin/components/vm-check-tree.vue +1 -0
  6. package/src/admin/components/vm-dept-tree.vue +3 -0
  7. package/src/admin/components/vm-empty.vue +1 -0
  8. package/src/admin/components/vm-eps-perm-picker.vue +2 -0
  9. package/src/admin/components/vm-group-cascader.vue +1 -0
  10. package/src/admin/components/vm-icon-picker.vue +1 -0
  11. package/src/admin/components/vm-markdown.vue +1 -0
  12. package/src/admin/components/vm-ri-icon.vue +2 -0
  13. package/src/admin/components/vm-role-picker.vue +2 -0
  14. package/src/admin/components/vm-view-path-picker.vue +1 -0
  15. package/src/admin/crud/components/vm-column-custom.vue +1 -0
  16. package/src/admin/crud/components/vm-context-menu.vue +1 -0
  17. package/src/admin/crud/components/vm-date-picker.vue +1 -0
  18. package/src/admin/crud/components/vm-date-range.vue +1 -0
  19. package/src/admin/crud/components/vm-date-text.vue +1 -0
  20. package/src/admin/crud/components/vm-dict-tag.vue +1 -0
  21. package/src/admin/crud/components/vm-file-icon.vue +1 -0
  22. package/src/admin/crud/components/vm-multi-select.vue +1 -0
  23. package/src/admin/crud/components/vm-select.vue +2 -0
  24. package/src/admin/crud/components/vm-switch.vue +1 -0
  25. package/src/admin/crud/components/vm-tree-select.vue +1 -0
  26. package/src/admin/crud/components/vm-upload-item.vue +1 -0
  27. package/src/admin/crud/components/vm-upload.vue +2 -0
  28. package/src/admin/crud/components/vm-user-select.vue +1 -0
  29. package/src/admin/crud/vm-add-btn.vue +1 -0
  30. package/src/admin/crud/vm-adv-search.vue +1 -0
  31. package/src/admin/crud/vm-crud.vue +2 -0
  32. package/src/admin/crud/vm-dialog.vue +1 -0
  33. package/src/admin/crud/vm-form.vue +1 -0
  34. package/src/admin/crud/vm-pagination.vue +1 -0
  35. package/src/admin/crud/vm-search-key.vue +1 -0
  36. package/src/admin/crud/vm-search.vue +1 -0
  37. package/src/admin/crud/vm-table.vue +1 -0
  38. package/src/admin/crud/vm-toolbar.vue +1 -0
  39. package/src/admin/crud/vm-upsert.vue +2 -0
  40. package/src/admin/lib/eps.ts +3 -2
  41. package/src/admin/service/index.ts +143 -46
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vome-core",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "Vome framework — shared + Bun/Elysia server + Vue admin",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -48,7 +48,9 @@
48
48
  "vue-sonner": ">=2.0.0",
49
49
  "@vueuse/core": ">=10.0.0",
50
50
  "clsx": ">=2.0.0",
51
- "tailwind-merge": ">=2.0.0"
51
+ "tailwind-merge": ">=2.0.0",
52
+ "dompurify": ">=3.0.0",
53
+ "marked": ">=9.0.0"
52
54
  },
53
55
  "peerDependenciesMeta": {
54
56
  "drizzle-orm": {
@@ -77,6 +79,12 @@
77
79
  },
78
80
  "tailwind-merge": {
79
81
  "optional": true
82
+ },
83
+ "dompurify": {
84
+ "optional": true
85
+ },
86
+ "marked": {
87
+ "optional": true
80
88
  }
81
89
  },
82
90
  "devDependencies": {
@@ -71,6 +71,9 @@
71
71
  </template>
72
72
 
73
73
  <script setup lang="ts">
74
+ import { ref, computed, watch } from 'vue'
75
+ import { api } from '../api/client'
76
+ import { toast } from 'vue-sonner'
74
77
  import { isDialogFloatTarget } from '../lib/dialog-float'
75
78
 
76
79
  defineOptions({ name: 'vm-auto-perm-dialog' })
@@ -42,6 +42,7 @@
42
42
  </template>
43
43
 
44
44
  <script setup lang="ts">
45
+ import { ref, computed, watch } from 'vue'
45
46
  import type { CheckTreeNode } from '../../../typings/admin/comm/check-tree'
46
47
 
47
48
  defineOptions({ name: 'vm-check-tree' })
@@ -101,6 +101,9 @@
101
101
  </template>
102
102
 
103
103
  <script setup lang="ts">
104
+ import { useVome } from '../hooks/useVome'
105
+ import { ref, computed, watch, onMounted } from 'vue'
106
+ import { toast } from 'vue-sonner'
104
107
  import { deepTree } from '../crud'
105
108
  import { vmConfirm } from '../crud/confirm'
106
109
 
@@ -1,4 +1,5 @@
1
1
  <script setup lang="ts">
2
+ import { computed } from 'vue'
2
3
  defineOptions({ name: 'vm-empty' })
3
4
 
4
5
  const scarceModules = import.meta.glob('../static/scarce/*.svg', {
@@ -19,6 +19,8 @@
19
19
  </template>
20
20
 
21
21
  <script setup lang="ts">
22
+ import { ref, computed, watch, onMounted } from 'vue'
23
+ import { api } from '../api/client'
22
24
  import type { EpsSide } from '../lib/eps'
23
25
 
24
26
  defineOptions({ name: 'vm-eps-perm-picker' })
@@ -90,6 +90,7 @@
90
90
  </template>
91
91
 
92
92
  <script setup lang="ts">
93
+ import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
93
94
  defineOptions({ name: 'vm-group-cascader' })
94
95
 
95
96
  const props = withDefaults(
@@ -107,6 +107,7 @@
107
107
  </template>
108
108
 
109
109
  <script setup lang="ts">
110
+ import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
110
111
  import { FocusScope } from 'reka-ui'
111
112
  import allIcons from '../data/remixicon-all.json'
112
113
 
@@ -3,6 +3,7 @@
3
3
  </template>
4
4
 
5
5
  <script setup lang="ts">
6
+ import { computed } from 'vue'
6
7
  import DOMPurify from 'dompurify'
7
8
  import { marked } from 'marked'
8
9
 
@@ -1,4 +1,6 @@
1
1
  <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { cn } from '../lib/cn'
2
4
  defineOptions({ name: 'vm-ri-icon' })
3
5
 
4
6
  const props = withDefaults(
@@ -48,6 +48,8 @@
48
48
  </template>
49
49
 
50
50
  <script setup lang="ts">
51
+ import { useVome } from '../hooks/useVome'
52
+ import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
51
53
  defineOptions({ name: 'vm-role-picker' })
52
54
 
53
55
  const props = withDefaults(
@@ -11,6 +11,7 @@
11
11
  </template>
12
12
 
13
13
  <script setup lang="ts">
14
+ import { ref, onMounted, onUnmounted } from 'vue'
14
15
  import {
15
16
  listViewsTree,
16
17
  VIEWS_RESCAN_EVENT,
@@ -40,6 +40,7 @@
40
40
  </template>
41
41
 
42
42
  <script setup lang="ts">
43
+ import { ref, watch, inject } from 'vue'
43
44
  import { injectTableOptions, TABLE_API_KEY } from '../key'
44
45
  defineOptions({ name: 'vm-column-custom' })
45
46
 
@@ -21,6 +21,7 @@
21
21
  </template>
22
22
 
23
23
  <script setup lang="ts">
24
+ import { ref, onMounted, onUnmounted } from 'vue'
24
25
  defineOptions({ name: 'vm-context-menu' })
25
26
 
26
27
  const props = defineProps<{
@@ -10,6 +10,7 @@
10
10
  </template>
11
11
 
12
12
  <script setup lang="ts">
13
+ import { computed } from 'vue'
13
14
  import { useCrud } from '../useCrud'
14
15
 
15
16
  defineOptions({ name: 'vm-date-picker' })
@@ -45,6 +45,7 @@
45
45
  </template>
46
46
 
47
47
  <script setup lang="ts">
48
+ import { ref, computed } from 'vue'
48
49
  defineOptions({ name: 'vm-date-range' })
49
50
 
50
51
  const props = withDefaults(
@@ -3,6 +3,7 @@
3
3
  </template>
4
4
 
5
5
  <script setup lang="ts">
6
+ import { computed } from 'vue'
6
7
  defineOptions({ name: 'vm-date-text' })
7
8
 
8
9
  const props = withDefaults(
@@ -4,6 +4,7 @@
4
4
  </template>
5
5
 
6
6
  <script setup lang="ts">
7
+ import { computed } from 'vue'
7
8
  import { getCrudStyle } from '../style'
8
9
 
9
10
  defineOptions({ name: 'vm-dict-tag' })
@@ -1,4 +1,5 @@
1
1
  <script setup lang="ts">
2
+ import { computed, onMounted } from 'vue'
2
3
  import { extname } from '../../lib/upload'
3
4
 
4
5
  defineOptions({ name: 'vm-file-icon' })
@@ -30,6 +30,7 @@
30
30
  </template>
31
31
 
32
32
  <script setup lang="ts">
33
+ import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
33
34
  defineOptions({ name: 'vm-multi-select' })
34
35
 
35
36
  const props = withDefaults(
@@ -24,6 +24,8 @@
24
24
  </template>
25
25
 
26
26
  <script setup lang="ts">
27
+ import { ref, computed, watch } from 'vue'
28
+ import { api } from '../../api/client'
27
29
  import { useCrud } from '../useCrud'
28
30
 
29
31
  defineOptions({ name: 'vm-select' })
@@ -13,6 +13,7 @@
13
13
  </template>
14
14
 
15
15
  <script setup lang="ts">
16
+ import { ref, computed, watch } from 'vue'
16
17
  import { toast } from 'vue-sonner'
17
18
  import { vmConfirm } from '../confirm'
18
19
  import { useCrud } from '../useCrud'
@@ -53,6 +53,7 @@
53
53
  </template>
54
54
 
55
55
  <script setup lang="ts">
56
+ import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
56
57
  type FlatRow = {
57
58
  key: string
58
59
  label: string
@@ -61,6 +61,7 @@
61
61
  </template>
62
62
 
63
63
  <script setup lang="ts">
64
+ import { ref, computed, watch } from 'vue'
64
65
  import { extname, getUploadRule } from '../../lib/upload'
65
66
  import type { UploadItem } from '../../../../typings/admin/comm/upload'
66
67
 
@@ -73,6 +73,8 @@
73
73
  </template>
74
74
 
75
75
  <script setup lang="ts">
76
+ import { ref, computed, watch } from 'vue'
77
+ import { toast } from 'vue-sonner'
76
78
  import { useUpload } from '../../hooks/useUpload'
77
79
  import { getFileType, getUrls, uploadUid } from '../../lib/upload'
78
80
  import type { UploadItem } from '../../../../typings/admin/comm/upload'
@@ -48,6 +48,7 @@
48
48
  </template>
49
49
 
50
50
  <script setup lang="ts">
51
+ import { ref, computed, watch } from 'vue'
51
52
  import { service } from '../../service'
52
53
 
53
54
  defineOptions({ name: 'vm-user-select' })
@@ -11,6 +11,7 @@
11
11
  </template>
12
12
 
13
13
  <script setup lang="ts">
14
+ import { computed } from 'vue'
14
15
  import { useCrud } from './useCrud'
15
16
 
16
17
  defineOptions({ name: 'vm-add-btn' })
@@ -19,6 +19,7 @@
19
19
  </template>
20
20
 
21
21
  <script setup lang="ts">
22
+ import { ref } from 'vue'
22
23
  import { useCrud } from './useCrud'
23
24
  defineOptions({ name: 'vm-adv-search' })
24
25
 
@@ -5,6 +5,8 @@
5
5
  </template>
6
6
 
7
7
  <script setup lang="ts">
8
+ import { ref, computed, watch, shallowRef, provide } from 'vue'
9
+ import { service } from '../service/index'
8
10
  import { toast } from 'vue-sonner'
9
11
  import { resolveService } from '../service'
10
12
  import { deepTree } from '../lib/tree'
@@ -44,6 +44,7 @@
44
44
  </template>
45
45
 
46
46
  <script setup lang="ts">
47
+ import { computed, getCurrentInstance, useSlots } from 'vue'
47
48
  import { isDialogFloatTarget } from '../lib/dialog-float'
48
49
 
49
50
  defineOptions({ name: 'vm-dialog' })
@@ -48,6 +48,7 @@
48
48
  </template>
49
49
 
50
50
  <script setup lang="ts">
51
+ import { ref, computed, watch, reactive } from 'vue'
51
52
  import { resolveCrudComponent } from './registry'
52
53
  import { getCrudStyle } from './style'
53
54
  import { resolveFormSpan, useFormCols } from './span'
@@ -40,6 +40,7 @@
40
40
  </template>
41
41
 
42
42
  <script setup lang="ts">
43
+ import { computed } from 'vue'
43
44
  import { useCrud } from './useCrud'
44
45
 
45
46
  defineOptions({ name: 'vm-pagination' })
@@ -14,6 +14,7 @@
14
14
  </template>
15
15
 
16
16
  <script setup lang="ts">
17
+ import { ref } from 'vue'
17
18
  import { useCrud } from './useCrud'
18
19
 
19
20
  defineOptions({ name: 'vm-search-key' })
@@ -48,6 +48,7 @@
48
48
  </template>
49
49
 
50
50
  <script setup lang="ts">
51
+ import { ref, computed, watch, reactive, onMounted } from 'vue'
51
52
  import { onBeforeUnmount } from 'vue'
52
53
  import { useCrud } from './useCrud'
53
54
  import { injectSearchOptions } from './key'
@@ -224,6 +224,7 @@
224
224
  </template>
225
225
 
226
226
  <script setup lang="ts">
227
+ import { ref, computed, watch, onMounted, onBeforeUnmount, provide, useSlots } from 'vue'
227
228
  import { useCrud } from './useCrud'
228
229
  import { injectTableOptions, TABLE_API_KEY } from './key'
229
230
  import { flattenTree } from '../lib/tree'
@@ -55,6 +55,7 @@
55
55
  </template>
56
56
 
57
57
  <script setup lang="ts">
58
+ import { computed } from 'vue'
58
59
  import { useCrud } from './useCrud'
59
60
  defineOptions({ name: 'vm-toolbar' })
60
61
 
@@ -113,6 +113,8 @@
113
113
  </template>
114
114
 
115
115
  <script setup lang="ts">
116
+ import { ref, computed, watch, useSlots } from 'vue'
117
+ import { toast } from 'vue-sonner'
116
118
  import { useCrud } from './useCrud'
117
119
  import { injectUpsertOptions } from './key'
118
120
  import { getCrudStyle } from './style'
@@ -12,10 +12,11 @@ const cache: Record<EpsSide, EpsModuleMap | null> = {
12
12
  app: null,
13
13
  }
14
14
 
15
- /** 拉取 EPS(默认 admin */
15
+ /** 拉取 EPS(默认 admin);公开接口失败不弹 toast */
16
16
  export async function loadEps(force = false, side: EpsSide = 'admin') {
17
17
  if (cache[side] && !force) return cache[side]
18
- cache[side] = (await request<EpsModuleMap>(EPS_URL[side])) || {}
18
+ cache[side] =
19
+ (await request<EpsModuleMap>(EPS_URL[side], { toast: false })) || {}
19
20
  return cache[side]
20
21
  }
21
22
 
@@ -1,7 +1,57 @@
1
1
  import { loadEps } from '../lib/eps'
2
2
  import { BaseService } from './base'
3
+
3
4
  const SIDE_ID = 'admin'
4
5
 
6
+ /** 标准 CRUD 之外的常用接口(EPS 未就绪时也可用) */
7
+ const FALLBACK_EXTRA_APIS: Record<
8
+ string,
9
+ Array<{ method: string; path: string; perms?: string[] }>
10
+ > = {
11
+ '/admin/base/department': [{ method: 'get', path: '/tree' }],
12
+ '/admin/base/menu': [{ method: 'get', path: '/tree' }],
13
+ '/admin/base/role': [
14
+ { method: 'get', path: '/menus' },
15
+ { method: 'post', path: '/setMenus' },
16
+ { method: 'get', path: '/departments' },
17
+ { method: 'post', path: '/setDepartments' },
18
+ ],
19
+ '/admin/base/user': [
20
+ { method: 'get', path: '/roles' },
21
+ { method: 'get', path: '/roleMap' },
22
+ { method: 'post', path: '/setRoles' },
23
+ { method: 'post', path: '/transferSuper' },
24
+ { method: 'post', path: '/moveDepartment' },
25
+ ],
26
+ '/admin/base/task': [
27
+ { method: 'post', path: '/start' },
28
+ { method: 'post', path: '/stop' },
29
+ { method: 'post', path: '/once' },
30
+ { method: 'get', path: '/log' },
31
+ ],
32
+ '/admin/base/log': [
33
+ { method: 'post', path: '/clear' },
34
+ { method: 'post', path: '/setKeep' },
35
+ { method: 'post', path: '/getKeep' },
36
+ ],
37
+ '/admin/base/dict/info': [
38
+ { method: 'get', path: '/types' },
39
+ { method: 'post', path: '/data' },
40
+ { method: 'post', path: '/get' },
41
+ { method: 'post', path: '/find' },
42
+ { method: 'post', path: '/getValues' },
43
+ { method: 'post', path: '/pathValue' },
44
+ { method: 'post', path: '/childValue' },
45
+ ],
46
+ '/admin/base/module': [{ method: 'post', path: '/install' }],
47
+ '/admin/base/comm': [{ method: 'post', path: '/upload' }],
48
+ '/admin/user/info': [
49
+ { method: 'get', path: '/roles' },
50
+ { method: 'get', path: '/roleMap' },
51
+ { method: 'post', path: '/setRoles' },
52
+ ],
53
+ }
54
+
5
55
  /** EPS 不可用时仍保证业务页链式可调 */
6
56
  const FALLBACK_PREFIXES = [
7
57
  '/admin/base/menu',
@@ -11,8 +61,13 @@ const FALLBACK_PREFIXES = [
11
61
  '/admin/base/tenant',
12
62
  '/admin/base/task',
13
63
  '/admin/base/log',
14
- '/admin/base/plugin-info',
15
- '/admin/user/user-role',
64
+ '/admin/base/plugin',
65
+ '/admin/base/dict/info',
66
+ '/admin/base/dict/type',
67
+ '/admin/base/module',
68
+ '/admin/base/comm',
69
+ '/admin/user/info',
70
+ '/admin/user/role',
16
71
  ]
17
72
 
18
73
  function toCamel(str: string) {
@@ -30,7 +85,6 @@ function isValidName(name: string) {
30
85
  /** 无 EPS perms 时生成权限码 */
31
86
  function resolvePermCode(prefix: string, action: string) {
32
87
  const parts = prefix.replace(/^\//, '').split('/').filter(Boolean)
33
- // admin / module / resource...
34
88
  const module = parts[1] || parts[0] || 'base'
35
89
  let resource = parts.slice(2).join(':')
36
90
  if (resource === module) {
@@ -51,14 +105,7 @@ function getMethodKeys(leaf: Record<string, unknown>) {
51
105
  )
52
106
  }
53
107
 
54
- function createLeaf(namespace: string, apis: EpsApiItem[] = []): ServiceLeaf {
55
- const base = new BaseService(namespace)
56
- const leaf = base as unknown as ServiceLeaf
57
- leaf.namespace = namespace.replace(/^\//, '')
58
- leaf.permission = {}
59
- leaf._permission = {}
60
-
61
- // 挂到实例自身,避免业务侧 `...leaf` 丢失原型方法
108
+ function bindCrud(leaf: ServiceLeaf, base: BaseService) {
62
109
  const proto = BaseService.prototype as unknown as Record<string, unknown>
63
110
  for (const name of [
64
111
  'request',
@@ -72,15 +119,31 @@ function createLeaf(namespace: string, apis: EpsApiItem[] = []): ServiceLeaf {
72
119
  ]) {
73
120
  const fn = proto[name]
74
121
  if (typeof fn === 'function') {
75
- ;(leaf as Record<string, unknown>)[name] = (fn as (...a: unknown[]) => unknown).bind(base)
122
+ ;(leaf as Record<string, unknown>)[name] = (
123
+ fn as (...a: unknown[]) => unknown
124
+ ).bind(base)
76
125
  }
77
126
  }
127
+ }
78
128
 
129
+ function bindApis(leaf: ServiceLeaf, base: BaseService, apis: EpsApiItem[]) {
130
+ leaf.permission ||= {}
79
131
  for (const api of apis) {
80
132
  const name = methodName(api.path)
81
133
  if (!isValidName(name) || /[-:]/g.test(name)) continue
82
134
  const method = (api.method || 'get').toLowerCase()
83
- leaf[name] = function (this: BaseService, data?: unknown) {
135
+ // 已有同名方法则保留(避免 fallback 覆盖 EPS 绑定)
136
+ if (typeof (leaf as Record<string, unknown>)[name] === 'function') {
137
+ if (!leaf.permission[name]) {
138
+ leaf.permission[name] =
139
+ api.perms?.[0] || resolvePermCode(`/${leaf.namespace}`, name)
140
+ }
141
+ continue
142
+ }
143
+ ;(leaf as Record<string, unknown>)[name] = function (
144
+ this: BaseService,
145
+ data?: unknown,
146
+ ) {
84
147
  return this.request({
85
148
  url: api.path.startsWith('/') ? api.path : `/${api.path}`,
86
149
  method,
@@ -90,8 +153,18 @@ function createLeaf(namespace: string, apis: EpsApiItem[] = []): ServiceLeaf {
90
153
  leaf.permission[name] =
91
154
  api.perms?.[0] || resolvePermCode(`/${leaf.namespace}`, name)
92
155
  }
156
+ }
157
+
158
+ function createLeaf(namespace: string, apis: EpsApiItem[] = []): ServiceLeaf {
159
+ const base = new BaseService(namespace)
160
+ const leaf = base as unknown as ServiceLeaf
161
+ leaf.namespace = namespace.replace(/^\//, '')
162
+ leaf.permission = {}
163
+ leaf._permission = {}
164
+
165
+ bindCrud(leaf, base)
166
+ bindApis(leaf, base, apis)
93
167
 
94
- // 标准 CRUD 权限兜底(EPS 未列时仍有码)
95
168
  for (const name of getMethodKeys(leaf as unknown as Record<string, unknown>)) {
96
169
  if (!leaf.permission[name]) {
97
170
  leaf.permission[name] = resolvePermCode(`/${leaf.namespace}`, name)
@@ -101,7 +174,28 @@ function createLeaf(namespace: string, apis: EpsApiItem[] = []): ServiceLeaf {
101
174
  return leaf
102
175
  }
103
176
 
104
- function attachLeaf(tree: ServiceTree | Eps.Service, prefix: string, apis: EpsApiItem[] = []) {
177
+ /** 合并到已有叶子:补方法,不抹掉已有自定义 API */
178
+ function mergeLeaf(existing: ServiceLeaf, namespace: string, apis: EpsApiItem[]) {
179
+ const base = new BaseService(namespace)
180
+ // 确保 CRUD 在实例上(兼容旧 fallback)
181
+ bindCrud(existing, base)
182
+ bindApis(existing, base, apis)
183
+ existing.namespace = namespace.replace(/^\//, '')
184
+ existing.permission ||= {}
185
+ existing._permission ||= {}
186
+ for (const name of getMethodKeys(existing as unknown as Record<string, unknown>)) {
187
+ if (!existing.permission[name]) {
188
+ existing.permission[name] = resolvePermCode(`/${existing.namespace}`, name)
189
+ }
190
+ }
191
+ return existing
192
+ }
193
+
194
+ function attachLeaf(
195
+ tree: ServiceTree | Eps.Service,
196
+ prefix: string,
197
+ apis: EpsApiItem[] = [],
198
+ ) {
105
199
  const raw = prefix.replace(/^\//, '')
106
200
  const arr = raw
107
201
  .replace(new RegExp(`^${SIDE_ID}/?`), '')
@@ -122,10 +216,7 @@ function attachLeaf(tree: ServiceTree | Eps.Service, prefix: string, apis: EpsAp
122
216
  }
123
217
  const existing = node[key] as ServiceLeaf | undefined
124
218
  if (existing?.namespace) {
125
- // 已有叶子:补方法
126
- const next = createLeaf(raw, apis)
127
- Object.assign(existing, next)
128
- return existing
219
+ return mergeLeaf(existing, raw, apis)
129
220
  }
130
221
  const leaf = createLeaf(raw, apis)
131
222
  node[key] = leaf
@@ -156,39 +247,41 @@ function buildFromEps(map: EpsModuleMap) {
156
247
 
157
248
  function buildFallback() {
158
249
  for (const prefix of FALLBACK_PREFIXES) {
159
- const path = prefix.replace(/^\//, '')
160
- const arr = path
161
- .replace(new RegExp(`^${SIDE_ID}/?`), '')
162
- .split('/')
163
- .filter(Boolean)
164
- .map(toCamel)
165
- let node: Record<string, unknown> = service as unknown as Record<string, unknown>
166
- let exists = true
167
- for (let i = 0; i < arr.length; i++) {
168
- const key = arr[i]!
169
- if (!node[key]) {
170
- exists = false
171
- break
172
- }
173
- node = node[key] as Record<string, unknown>
174
- }
175
- if (!exists || !(node as ServiceLeaf).namespace) {
176
- attachLeaf(service, prefix)
177
- }
250
+ const extra = FALLBACK_EXTRA_APIS[prefix] || []
251
+ const apis = extra.map(
252
+ (a) =>
253
+ ({
254
+ method: a.method,
255
+ path: a.path,
256
+ perms: a.perms,
257
+ }) as EpsApiItem,
258
+ )
259
+ attachLeaf(service, prefix, apis)
178
260
  }
179
261
  }
180
262
 
181
- const root = new BaseService()
263
+ type ServiceRoot = Eps.Service & { request: BaseService['request'] }
264
+
265
+ /**
266
+ * 全局单例:npm 包经多路径解析时避免出现两份 service(一份有 tree、一份没有)。
267
+ */
268
+ const g = globalThis as typeof globalThis & { __vome_admin_service__?: ServiceRoot }
269
+
270
+ function createServiceRoot(): ServiceRoot {
271
+ const root = new BaseService()
272
+ return {
273
+ request: root.request.bind(root),
274
+ } as ServiceRoot
275
+ }
182
276
 
183
277
  /** 全局链式 service(service.base.user.page()) */
184
- export const service = {
185
- request: root.request.bind(root),
186
- } as unknown as Eps.Service
278
+ export const service: ServiceRoot =
279
+ g.__vome_admin_service__ ?? (g.__vome_admin_service__ = createServiceRoot())
187
280
 
188
- // 同步兜底,保证页面 import 时即可 service.base.xxx
281
+ // 同步兜底(含 tree / menus 等),页面 import 时即可调用
189
282
  buildFallback()
190
283
 
191
- /** 从 EPS 构建/刷新 service 树;失败时用兜底 prefix */
284
+ /** 从 EPS 构建/刷新 service 树;失败时用兜底 prefix + 自定义 API */
192
285
  export async function createEps(force = false) {
193
286
  try {
194
287
  const map = (await loadEps(force)) || {}
@@ -200,7 +293,6 @@ export async function createEps(force = false) {
200
293
  return service
201
294
  }
202
295
 
203
- // Vite 保存源码 / 刷新时:插件重拉 EPS 后通知客户端热更新 service 树
204
296
  if (import.meta.hot) {
205
297
  import.meta.hot.on('eps-update', () => {
206
298
  void createEps(true).then(() => {
@@ -269,7 +361,12 @@ export function serviceOf(prefix: string): ServiceLeaf {
269
361
  .join('.')
270
362
  const hit = resolveService(path)
271
363
  if (hit) return hit
272
- return attachLeaf(service, raw) as ServiceLeaf
364
+ const extra = FALLBACK_EXTRA_APIS[raw.startsWith('/') ? raw : `/${raw}`] || []
365
+ return attachLeaf(
366
+ service,
367
+ raw,
368
+ extra.map((a) => ({ method: a.method, path: a.path, perms: a.perms }) as EpsApiItem),
369
+ ) as ServiceLeaf
273
370
  }
274
371
 
275
372
  /** @deprecated 请用 service.base.xxx */