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,530 @@
1
+ <template>
2
+ <Dialog
3
+ :open="crud.upsertVisible.value"
4
+ @update:open="(v) => (!v ? crud.upsertClose() : undefined)"
5
+ >
6
+ <DialogContent
7
+ shell="crud"
8
+ class="vm-crud-upsert"
9
+ :class="{ 'is-empty': isBodyEmpty }"
10
+ @pointer-down-outside="onPointerDownOutside"
11
+ @focus-outside="onFocusOutside"
12
+ @interact-outside="onInteractOutside"
13
+ >
14
+ <DialogHeader class="vm-crud-upsert__head">
15
+ <DialogTitle class="vm-crud-upsert__title">{{ titleText }}</DialogTitle>
16
+ </DialogHeader>
17
+ <div class="vm-crud-upsert__body">
18
+ <template v-if="!isBodyEmpty">
19
+ <div
20
+ v-for="item in formItemsBeforeSlot"
21
+ :key="item.prop"
22
+ class="vm-crud-upsert__field"
23
+ :class="{ 'is-error': fieldErrors[item.prop] }"
24
+ :style="{ gridColumn: `span ${itemSpan(item)}` }"
25
+ >
26
+ <Label class="vm-crud-upsert__label">
27
+ {{ item.label }}
28
+ <span v-if="item.required" class="vm-crud-upsert__req">*</span>
29
+ </Label>
30
+
31
+ <vm-radio
32
+ v-if="itemType(item) === 'radio'"
33
+ :name="`upsert-${item.prop}`"
34
+ :options="resolveOptions(item.options)"
35
+ :model-value="String(crud.upsertForm.value[item.prop] ?? '')"
36
+ :disabled="isInfo"
37
+ @update:model-value="(v) => setField(item.prop, v)"
38
+ />
39
+
40
+ <component
41
+ :is="resolveField(item)"
42
+ v-else-if="resolveField(item)"
43
+ :class="fieldClass(item)"
44
+ v-bind="fieldProps(item)"
45
+ :model-value="crud.upsertForm.value[item.prop]"
46
+ :disabled="isInfo"
47
+ @update:model-value="(v: unknown) => setField(item.prop, v)"
48
+ />
49
+
50
+ <Input
51
+ v-else
52
+ class="vm-crud-upsert__control"
53
+ :placeholder="item.placeholder"
54
+ :disabled="isInfo"
55
+ :aria-invalid="fieldErrors[item.prop] ? true : undefined"
56
+ :model-value="String(crud.upsertForm.value[item.prop] ?? '')"
57
+ @update:model-value="(v) => setField(item.prop, v)"
58
+ />
59
+ <p v-if="fieldErrors[item.prop]" class="vm-crud-upsert__error">
60
+ {{ fieldErrors[item.prop] }}
61
+ </p>
62
+ </div>
63
+ <slot :form="crud.upsertForm.value" />
64
+ <div
65
+ v-for="item in formItemsAfterSlot"
66
+ :key="item.prop"
67
+ class="vm-crud-upsert__field"
68
+ :class="{ 'is-error': fieldErrors[item.prop] }"
69
+ :style="{ gridColumn: `span ${itemSpan(item)}` }"
70
+ >
71
+ <Label class="vm-crud-upsert__label">
72
+ {{ item.label }}
73
+ <span v-if="item.required" class="vm-crud-upsert__req">*</span>
74
+ </Label>
75
+
76
+ <textarea
77
+ class="vm-crud-upsert__textarea"
78
+ :placeholder="item.placeholder"
79
+ :disabled="isInfo"
80
+ :value="String(crud.upsertForm.value[item.prop] ?? '')"
81
+ @input="setField(item.prop, ($event.target as HTMLTextAreaElement).value)"
82
+ />
83
+ <p v-if="fieldErrors[item.prop]" class="vm-crud-upsert__error">
84
+ {{ fieldErrors[item.prop] }}
85
+ </p>
86
+ </div>
87
+ </template>
88
+ <div v-else class="vm-crud-upsert__empty">
89
+ <vm-empty kind="暂无内容" />
90
+ </div>
91
+ </div>
92
+ <DialogFooter class="vm-crud-upsert__foot">
93
+ <button
94
+ type="button"
95
+ class="vm-crud-upsert__btn is-ghost"
96
+ :disabled="submitting"
97
+ @click="crud.upsertClose()"
98
+ >
99
+ {{ crud.dict.label.close }}
100
+ </button>
101
+ <button
102
+ v-if="showConfirm"
103
+ type="button"
104
+ class="vm-crud-upsert__btn is-primary"
105
+ :disabled="submitting"
106
+ @click="onSubmit"
107
+ >
108
+ {{ submitting ? '提交中…' : crud.dict.label.confirm }}
109
+ </button>
110
+ </DialogFooter>
111
+ </DialogContent>
112
+ </Dialog>
113
+ </template>
114
+
115
+ <script setup lang="ts">
116
+ import { useCrud } from './useCrud'
117
+ import { injectUpsertOptions } from './key'
118
+ import { getCrudStyle } from './style'
119
+ import { resolveCrudComponent } from './registry'
120
+ import { applySetRules, validateFormFields } from './validate'
121
+ import { resolveFormSpan, useFormCols } from './span'
122
+ import { isDialogFloatTarget } from '../lib/dialog-float'
123
+ defineOptions({ name: 'vm-upsert' })
124
+
125
+ const props = withDefaults(
126
+ defineProps<{
127
+ items?: CrudFormItem[]
128
+ title?: string
129
+ }>(),
130
+ {},
131
+ )
132
+
133
+ const slots = useSlots()
134
+ const crud = useCrud()
135
+ const upsertOpts = injectUpsertOptions()
136
+ const formCols = useFormCols()
137
+ const submitting = ref(false)
138
+ const fieldErrors = ref<Record<string, string>>({})
139
+
140
+ const mergedItems = computed(() => {
141
+ const raw = props.items?.length ? props.items : upsertOpts?.items || []
142
+ const useRules = getCrudStyle().form.plugins.some(
143
+ (p) => (p as { __plugin?: string })?.__plugin === 'setRules',
144
+ )
145
+ return useRules ? applySetRules(raw) : raw
146
+ })
147
+
148
+ const titleText = computed(() => {
149
+ if (props.title != null && props.title !== '') return props.title
150
+ if (upsertOpts?.dialog?.title) return upsertOpts.dialog.title
151
+ if (crud.upsertMode.value === 'add') return crud.dict.label.add
152
+ if (crud.upsertMode.value === 'info') return crud.dict.label.info
153
+ return crud.dict.label.update
154
+ })
155
+
156
+ const visibleItems = computed(() =>
157
+ mergedItems.value.filter((it) => {
158
+ if (typeof it.hidden === 'function') return !it.hidden(crud.upsertForm.value)
159
+ return !it.hidden
160
+ }),
161
+ )
162
+
163
+ /** 备注等 textarea 固定渲染在 slot 之后 */
164
+ const formItemsBeforeSlot = computed(() =>
165
+ visibleItems.value.filter((it) => itemType(it) !== 'textarea'),
166
+ )
167
+ const formItemsAfterSlot = computed(() =>
168
+ visibleItems.value.filter((it) => itemType(it) === 'textarea'),
169
+ )
170
+
171
+ const isInfo = computed(() => crud.upsertMode.value === 'info')
172
+ /** dialog.confirm 显式控制;未设时 info 模式自动隐藏确定 */
173
+ const showConfirm = computed(() => {
174
+ const flag = upsertOpts?.dialog?.confirm
175
+ if (typeof flag === 'boolean') return flag
176
+ return !isInfo.value
177
+ })
178
+ const isBodyEmpty = computed(
179
+ () => !visibleItems.value.length && !slots.default,
180
+ )
181
+
182
+ function onPointerDownOutside(event: Event) {
183
+ if (isDialogFloatTarget(event)) event.preventDefault()
184
+ }
185
+
186
+ function onFocusOutside(event: Event) {
187
+ if (isDialogFloatTarget(event)) event.preventDefault()
188
+ }
189
+
190
+ function onInteractOutside(event: Event) {
191
+ if (isDialogFloatTarget(event)) event.preventDefault()
192
+ }
193
+
194
+ function setField(prop: string, value: unknown) {
195
+ if (isInfo.value) return
196
+ if (fieldErrors.value[prop]) {
197
+ const next = { ...fieldErrors.value }
198
+ delete next[prop]
199
+ fieldErrors.value = next
200
+ }
201
+ crud.upsertForm.value = { ...crud.upsertForm.value, [prop]: value }
202
+ }
203
+
204
+ function itemType(item: CrudFormItem) {
205
+ if (item.type) return item.type
206
+ const name = item.component?.name
207
+ if (typeof name !== 'string') return 'input'
208
+ if (name === 'vm-switch') return 'switch'
209
+ if (name === 'vm-select' || name === 'vm-tree-select') {
210
+ return 'select'
211
+ }
212
+ return 'input'
213
+ }
214
+
215
+ function isSwitchField(item: CrudFormItem) {
216
+ return itemType(item) === 'switch'
217
+ }
218
+
219
+ function isUploadField(item: CrudFormItem) {
220
+ const name = item.component?.name
221
+ return name === 'vm-upload' || name === 'upload'
222
+ }
223
+
224
+ function fieldClass(item: CrudFormItem) {
225
+ if (isSwitchField(item)) return 'vm-crud-upsert__switch-field'
226
+ if (isUploadField(item)) return 'vm-crud-upsert__upload-field'
227
+ return 'vm-crud-upsert__control'
228
+ }
229
+
230
+ function resolveField(item: CrudFormItem) {
231
+ if (itemType(item) === 'radio' || itemType(item) === 'textarea') return undefined
232
+ const name = item.component?.name
233
+ if (typeof name === 'string') {
234
+ if (name === 'upload') return resolveCrudComponent('vm-upload')
235
+ const comp = resolveCrudComponent(name)
236
+ if (comp) return comp
237
+ } else if (name) {
238
+ return name
239
+ }
240
+ if (item.type === 'switch') return resolveCrudComponent('vm-switch')
241
+ if (item.type === 'select') return resolveCrudComponent('vm-select')
242
+ return undefined
243
+ }
244
+
245
+ function fieldProps(item: CrudFormItem) {
246
+ return {
247
+ options: resolveOptions(item.options),
248
+ placeholder: item.placeholder,
249
+ refreshOnChange: false,
250
+ ...(item.component?.props || {}),
251
+ }
252
+ }
253
+
254
+ /** options: dict.get('x') → ComputedRef,渲染时 toValue */
255
+ function resolveOptions(options: CrudFormItem['options']) {
256
+ const list = options == null ? [] : toValue(options)
257
+ return Array.isArray(list) ? list : []
258
+ }
259
+
260
+ function itemSpan(item: CrudFormItem) {
261
+ return resolveFormSpan(item.span, formCols.value)
262
+ }
263
+
264
+ async function onSubmit() {
265
+ if (isInfo.value) {
266
+ crud.upsertClose()
267
+ return
268
+ }
269
+ if (submitting.value) return
270
+ const errors = await validateFormFields(
271
+ visibleItems.value,
272
+ crud.upsertForm.value,
273
+ )
274
+ fieldErrors.value = errors
275
+ if (Object.keys(errors).length) return
276
+ submitting.value = true
277
+ try {
278
+ if (upsertOpts?.onSubmit) {
279
+ await upsertOpts.onSubmit(
280
+ { ...crud.upsertForm.value },
281
+ {
282
+ next: async (data) => {
283
+ crud.upsertForm.value = { ...crud.upsertForm.value, ...data }
284
+ await crud.upsertSubmit()
285
+ },
286
+ done: () => {},
287
+ close: () => crud.upsertClose(),
288
+ },
289
+ )
290
+ return
291
+ }
292
+ await crud.upsertSubmit()
293
+ } catch (e) {
294
+ console.error(e)
295
+ const errObj = e as Error & { toasted?: boolean }
296
+ const msg = e instanceof Error && e.message ? e.message : '操作失败'
297
+ if (!errObj.toasted) toast.error(msg.slice(0, 200))
298
+ } finally {
299
+ submitting.value = false
300
+ }
301
+ }
302
+
303
+ watch(
304
+ () => crud.upsertVisible.value,
305
+ (open) => {
306
+ if (!open) {
307
+ submitting.value = false
308
+ fieldErrors.value = {}
309
+ return
310
+ }
311
+ const data = { ...crud.upsertForm.value }
312
+ upsertOpts?.onOpen?.(data)
313
+ for (const it of mergedItems.value) {
314
+ if (data[it.prop] === undefined && it.value !== undefined) {
315
+ data[it.prop] = it.value
316
+ }
317
+ }
318
+ crud.upsertForm.value = data
319
+ upsertOpts?.onOpened?.({ ...crud.upsertForm.value })
320
+ const focusPlugin = getCrudStyle().form.plugins.find(
321
+ (p) => (p as { __plugin?: string })?.__plugin === 'setFocus',
322
+ ) as { prop?: string } | undefined
323
+ const prop =
324
+ focusPlugin?.prop ||
325
+ mergedItems.value.find((i) => !i.hidden)?.prop
326
+ requestAnimationFrame(() => {
327
+ const el = document.querySelector(
328
+ `.vm-crud-upsert__field input, .vm-crud-upsert__field textarea`,
329
+ ) as HTMLInputElement | null
330
+ if (prop) {
331
+ const named = document.querySelector(
332
+ `[name="upsert-${prop}"], .vm-crud-upsert__field:has(input) input`,
333
+ ) as HTMLInputElement | null
334
+ ;(named || el)?.focus?.()
335
+ } else {
336
+ el?.focus?.()
337
+ }
338
+ })
339
+ },
340
+ )
341
+
342
+ defineExpose({
343
+ form: crud.upsertForm,
344
+ mode: crud.upsertMode,
345
+ submit: onSubmit,
346
+ close: () => crud.upsertClose(),
347
+ add: () => crud.rowAdd(),
348
+ append: (data?: Record<string, unknown>) => crud.rowAppend(data),
349
+ edit: (data: Record<string, unknown>) => crud.rowEdit(data),
350
+ info: (data: Record<string, unknown>) => crud.rowInfo(data),
351
+ })
352
+ </script>
353
+
354
+ <style lang="scss" scoped>
355
+ .vm-crud-upsert__head {
356
+ flex-shrink: 0;
357
+ padding: 12px 22px;
358
+ border-bottom: 1px solid var(--border);
359
+
360
+ @media (max-width: 768px) {
361
+ padding: 8px 12px;
362
+ }
363
+ }
364
+
365
+ .vm-crud-upsert__title {
366
+ font-size: 15px;
367
+ font-weight: 650;
368
+ letter-spacing: -0.01em;
369
+ line-height: 1.3;
370
+ }
371
+
372
+ .vm-crud-upsert__body {
373
+ display: grid;
374
+ flex: 1 1 auto;
375
+ grid-template-columns: repeat(24, minmax(0, 1fr));
376
+ gap: 16px;
377
+ min-height: 0;
378
+ overflow: auto;
379
+ overscroll-behavior: contain;
380
+ padding: 18px 22px;
381
+ align-content: start;
382
+
383
+ /* slot 内容在宿主 SFC,须 :deep 才能命中 label / control 样式 */
384
+ :deep(.vm-crud-upsert__field) {
385
+ display: grid;
386
+ gap: 6px;
387
+ align-content: start;
388
+ min-width: 0;
389
+ }
390
+
391
+ :deep(.vm-crud-upsert__label) {
392
+ font-size: 12px;
393
+ color: var(--muted-foreground);
394
+ }
395
+
396
+ :deep(.vm-crud-upsert__req) {
397
+ color: var(--destructive);
398
+ }
399
+
400
+ :deep(.vm-crud-upsert__control) {
401
+ width: 100%;
402
+ height: var(--vm-control-height) !important;
403
+ min-height: var(--vm-control-height);
404
+ border-radius: var(--vm-control-radius) !important;
405
+ font-size: var(--vm-control-font-size) !important;
406
+ line-height: 1.4 !important;
407
+ }
408
+
409
+ @media (max-width: 768px) {
410
+ grid-template-columns: repeat(12, minmax(0, 1fr));
411
+ flex: 1 1 auto;
412
+ min-height: 0;
413
+ gap: 10px;
414
+ padding: 10px 12px;
415
+ }
416
+ }
417
+
418
+ .vm-crud-upsert__empty {
419
+ grid-column: 1 / -1;
420
+ display: flex;
421
+ min-height: 160px;
422
+ align-items: center;
423
+ justify-content: center;
424
+ }
425
+
426
+ .vm-crud-upsert__field {
427
+ &.is-error :deep(.vm-crud-upsert__control) {
428
+ border-color: var(--destructive) !important;
429
+ }
430
+ }
431
+
432
+ .vm-crud-upsert__error {
433
+ margin: 0;
434
+ font-size: 12px;
435
+ line-height: 1.4;
436
+ color: var(--destructive);
437
+ }
438
+
439
+ :deep(.vm-group-cascader.vm-crud-upsert__control) {
440
+ height: auto !important;
441
+ min-height: var(--vm-control-height);
442
+ }
443
+
444
+ /* 上传:按 size 撑开,勿套用单行输入框高度 */
445
+ :deep(.vm-upload.vm-crud-upsert__upload-field) {
446
+ width: 100%;
447
+ height: auto !important;
448
+ min-height: 0;
449
+ }
450
+
451
+ :deep(.vm-crud-upsert__switch-field.vm-switch) {
452
+ width: 40px;
453
+ height: 22px;
454
+ margin-top: 8px;
455
+ }
456
+
457
+ .vm-crud-upsert__textarea {
458
+ width: 100%;
459
+ min-height: 72px;
460
+ padding: 8px var(--vm-control-padding-x);
461
+ border: 1px solid var(--input);
462
+ border-radius: var(--vm-control-radius);
463
+ background: var(--background);
464
+ color: var(--foreground);
465
+ font-size: 14px;
466
+ line-height: 1.5;
467
+ resize: vertical;
468
+ outline: none;
469
+ transition: border-color 0.15s ease;
470
+
471
+ &:focus,
472
+ &:focus-visible {
473
+ border-color: var(--focus-border);
474
+ box-shadow: none;
475
+ }
476
+ }
477
+
478
+ .vm-crud-upsert__foot {
479
+ display: flex;
480
+ flex-shrink: 0;
481
+ justify-content: flex-end;
482
+ gap: 8px;
483
+ padding: 10px 22px;
484
+ border-top: 1px solid var(--border);
485
+
486
+ @media (max-width: 768px) {
487
+ flex-direction: column-reverse;
488
+ gap: 8px;
489
+ padding: 8px 12px calc(8px + env(safe-area-inset-bottom, 0px));
490
+
491
+ .vm-crud-upsert__btn {
492
+ width: 100%;
493
+ }
494
+ }
495
+ }
496
+
497
+ .vm-crud-upsert__btn {
498
+ height: 36px;
499
+ padding: 0 16px;
500
+ border: none;
501
+ border-radius: 12px;
502
+ font-size: 13px;
503
+ font-weight: 600;
504
+ cursor: pointer;
505
+ transition: background 0.15s ease, color 0.15s ease;
506
+
507
+ &.is-primary {
508
+ background: var(--brand);
509
+ color: #fff;
510
+
511
+ &:hover:not(:disabled) {
512
+ background: var(--brand-deep);
513
+ }
514
+ }
515
+
516
+ &.is-ghost {
517
+ background: var(--muted);
518
+ color: var(--foreground);
519
+
520
+ &:hover:not(:disabled) {
521
+ background: color-mix(in srgb, var(--muted) 70%, var(--foreground) 8%);
522
+ }
523
+ }
524
+
525
+ &:disabled {
526
+ cursor: wait;
527
+ opacity: 0.65;
528
+ }
529
+ }
530
+ </style>