vome-core 0.3.7 → 0.3.9

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 (82) hide show
  1. package/dist/admin/components/ui/avatar/index.js +1 -1
  2. package/dist/admin/components/ui/checkbox/index.js +1 -1
  3. package/dist/admin/components/ui/dialog/DialogContent.vue +132 -0
  4. package/dist/admin/components/ui/dialog/index.js +1 -1
  5. package/dist/admin/components/ui/dropdown-menu/index.js +1 -1
  6. package/dist/admin/components/ui/input/Input.vue +12 -0
  7. package/dist/admin/components/ui/input/index.js +1 -1
  8. package/dist/admin/components/ui/label/index.js +1 -1
  9. package/dist/admin/components/ui/scroll-area/index.js +1 -1
  10. package/dist/admin/components/ui/select/SelectItem.vue +9 -0
  11. package/dist/admin/components/ui/select/SelectTrigger.vue +11 -0
  12. package/dist/admin/components/ui/select/index.js +1 -1
  13. package/dist/admin/components/ui/separator/index.js +1 -1
  14. package/dist/admin/components/ui/sheet/index.js +1 -1
  15. package/dist/admin/components/ui/sonner/index.js +1 -1
  16. package/dist/admin/components/ui/table/index.js +1 -1
  17. package/dist/admin/components/ui/tabs/index.js +1 -1
  18. package/dist/admin/components/ui/tooltip/index.js +1 -1
  19. package/dist/admin/components/vm-dept-tree.vue +79 -74
  20. package/dist/admin/components/vm-form-dialog.vue +630 -0
  21. package/dist/admin/components/vm-loading.vue +113 -0
  22. package/dist/admin/crud/auto-fields.js +1 -1
  23. package/dist/admin/crud/components/vm-richtext-extensions.js +1 -1
  24. package/dist/admin/crud/components/vm-select.vue +96 -5
  25. package/dist/admin/crud/config.js +1 -1
  26. package/dist/admin/crud/confirm.js +1 -1
  27. package/dist/admin/crud/crud-page-request.js +1 -1
  28. package/dist/admin/crud/data-i18n.js +1 -1
  29. package/dist/admin/crud/dict.js +1 -1
  30. package/dist/admin/crud/export-fetch.js +1 -1
  31. package/dist/admin/crud/index.d.ts +0 -1
  32. package/dist/admin/crud/index.js +0 -1
  33. package/dist/admin/crud/key.js +1 -1
  34. package/dist/admin/crud/plugins.js +1 -1
  35. package/dist/admin/crud/span.js +1 -1
  36. package/dist/admin/crud/style.js +1 -1
  37. package/dist/admin/crud/validate.js +1 -1
  38. package/dist/admin/crud/vm-upsert.vue +0 -1
  39. package/dist/admin/directives/perm.js +1 -1
  40. package/dist/admin/hooks/useUpload.js +1 -1
  41. package/dist/admin/lib/browser.js +1 -1
  42. package/dist/admin/lib/cn.js +1 -1
  43. package/dist/admin/lib/dialog-float.js +1 -1
  44. package/dist/admin/lib/export-excel.js +1 -1
  45. package/dist/admin/lib/import-excel.js +1 -1
  46. package/dist/admin/lib/json.js +1 -1
  47. package/dist/admin/lib/menu.js +1 -1
  48. package/dist/admin/lib/micro-locale.js +1 -1
  49. package/dist/admin/lib/micro-theme.js +1 -1
  50. package/dist/admin/lib/shell.js +1 -1
  51. package/dist/admin/lib/theme-color-options.js +1 -1
  52. package/dist/admin/lib/tree.js +1 -1
  53. package/dist/admin/lib/upload.js +1 -1
  54. package/dist/admin/lib/video-frame.js +1 -1
  55. package/dist/admin/router/create.js +1 -1
  56. package/dist/admin/router/guards.js +1 -1
  57. package/dist/admin/styles/base.css +2 -152
  58. package/dist/ai/index.js +1 -1
  59. package/dist/client/ai-model-registry.js +1 -1
  60. package/dist/client/base.js +1 -1
  61. package/dist/client/configure.js +1 -1
  62. package/dist/client/dict.js +1 -1
  63. package/dist/client/eps-payload.js +1 -1
  64. package/dist/client/eps.js +1 -1
  65. package/dist/client/index.js +1 -1
  66. package/dist/client/locale-store.js +1 -1
  67. package/dist/client/service.js +1 -1
  68. package/dist/client/vite-admin.js +1 -1
  69. package/dist/client/vite-auto-import.js +1 -1
  70. package/dist/client/vite-micro-proxy.js +1 -1
  71. package/dist/client/vite-plugin-eps.js +1 -1
  72. package/dist/client/vite-resolve.js +1 -1
  73. package/dist/config/index.js +1 -1
  74. package/dist/index.js +1 -1
  75. package/dist/server/index.js +1 -1
  76. package/dist/shared/datetime-picker.js +1 -1
  77. package/dist/shared/excel.js +1 -1
  78. package/dist/shared/index.js +1 -1
  79. package/dist/shared/locale-json.js +1 -1
  80. package/dist/shared/tree.js +1 -1
  81. package/package.json +1 -1
  82. package/dist/admin/crud/components/vm-user-select.vue +0 -234
@@ -0,0 +1,630 @@
1
+ <template>
2
+ <Dialog
3
+ :open="open"
4
+ @update:open="(v) => (!v ? onCloseClick() : undefined)"
5
+ >
6
+ <DialogContent
7
+ shell="crud"
8
+ :show-close-button="false"
9
+ class="vm-crud-upsert"
10
+ :class="{
11
+ 'is-empty': isBodyEmpty,
12
+ 'is-height': !!heightCss,
13
+ }"
14
+ :style="heightCss ? { '--vm-dialog-height': heightCss } : undefined"
15
+ @pointer-down-outside="preventDialogOutsideClose"
16
+ @focus-outside="preventDialogOutsideClose"
17
+ @interact-outside="preventDialogOutsideClose"
18
+ >
19
+ <DialogHeader class="vm-crud-shell__head">
20
+ <DialogTitle class="vm-crud-shell__title">{{ title }}</DialogTitle>
21
+ <slot name="title-extra" />
22
+ <DialogClose class="vm-crud-shell__close">
23
+ <i class="ri-close-line" />
24
+ <span class="sr-only">Close</span>
25
+ </DialogClose>
26
+ </DialogHeader>
27
+ <div
28
+ class="vm-crud-upsert__body"
29
+ :class="{ 'is-fill': layout === 'fill' }"
30
+ >
31
+ <template v-if="!isBodyEmpty">
32
+ <div
33
+ v-for="item in formItemsBeforeSlot"
34
+ :key="item.prop"
35
+ class="vm-crud-upsert__field"
36
+ :class="{ 'is-error': fieldErrors[item.prop] }"
37
+ :style="{ gridColumn: `span ${itemSpan(item)}` }"
38
+ >
39
+ <Label class="vm-crud-upsert__label">
40
+ {{ item.label }}
41
+ <span
42
+ v-if="resolveItemRequired(item, form)"
43
+ class="vm-crud-upsert__req"
44
+ >*</span>
45
+ </Label>
46
+
47
+ <vm-radio
48
+ v-if="itemType(item) === 'radio'"
49
+ :name="`form-dialog-${item.prop}`"
50
+ :options="resolveOptions(item)"
51
+ :model-value="String(form[item.prop] ?? '')"
52
+ :disabled="readonly || item.disabled"
53
+ @update:model-value="(v) => setField(item.prop, v)"
54
+ />
55
+
56
+ <component
57
+ :is="resolveField(item)"
58
+ v-else-if="resolveField(item)"
59
+ :class="fieldClass(item)"
60
+ v-bind="fieldProps(item)"
61
+ :model-value="form[item.prop]"
62
+ :disabled="readonly || item.disabled"
63
+ @update:model-value="(v: unknown) => setField(item.prop, v)"
64
+ />
65
+
66
+ <Input
67
+ v-else
68
+ class="vm-crud-upsert__control"
69
+ v-bind="inputBind(item)"
70
+ :placeholder="item.placeholder"
71
+ :disabled="readonly || item.disabled"
72
+ :aria-invalid="fieldErrors[item.prop] ? true : undefined"
73
+ :model-value="
74
+ form[item.prop] == null ? '' : (form[item.prop] as string | number)
75
+ "
76
+ @update:model-value="(v) => setField(item.prop, v)"
77
+ />
78
+ <p v-if="fieldErrors[item.prop]" class="vm-crud-upsert__error">
79
+ {{ fieldErrors[item.prop] }}
80
+ </p>
81
+ </div>
82
+ <slot :form="form" />
83
+ <div
84
+ v-for="item in formItemsAfterSlot"
85
+ :key="item.prop"
86
+ class="vm-crud-upsert__field"
87
+ :class="{ 'is-error': fieldErrors[item.prop] }"
88
+ :style="{ gridColumn: `span ${itemSpan(item)}` }"
89
+ >
90
+ <Label class="vm-crud-upsert__label">
91
+ {{ item.label }}
92
+ <span
93
+ v-if="resolveItemRequired(item, form)"
94
+ class="vm-crud-upsert__req"
95
+ >*</span>
96
+ </Label>
97
+
98
+ <div class="vm-crud-upsert__textarea-wrap">
99
+ <textarea
100
+ class="vm-crud-upsert__textarea"
101
+ :placeholder="item.placeholder"
102
+ :disabled="readonly || item.disabled"
103
+ :rows="textareaRows(item)"
104
+ :style="textareaStyle(item)"
105
+ :value="String(form[item.prop] ?? '')"
106
+ @input="setField(item.prop, ($event.target as HTMLTextAreaElement).value)"
107
+ />
108
+ <i
109
+ v-if="
110
+ textareaClearable(item) &&
111
+ String(form[item.prop] ?? '') !== '' &&
112
+ !readonly &&
113
+ !item.disabled
114
+ "
115
+ class="ri-close-circle-fill vm-crud-upsert__textarea-clear"
116
+ role="button"
117
+ tabindex="-1"
118
+ aria-label="清除"
119
+ @mousedown.prevent
120
+ @click="setField(item.prop, '')"
121
+ />
122
+ </div>
123
+ <p v-if="fieldErrors[item.prop]" class="vm-crud-upsert__error">
124
+ {{ fieldErrors[item.prop] }}
125
+ </p>
126
+ </div>
127
+ </template>
128
+ <div v-else class="vm-crud-upsert__empty">
129
+ <vm-empty kind="暂无内容" />
130
+ </div>
131
+ </div>
132
+ <DialogFooter v-if="footer !== false" class="vm-crud-shell__foot">
133
+ <slot
134
+ v-if="slots.footer"
135
+ name="footer"
136
+ v-bind="footerSlotProps"
137
+ />
138
+ <template v-else>
139
+ <slot name="footer-prepend" v-bind="footerSlotProps" />
140
+ <button
141
+ v-if="close !== false"
142
+ type="button"
143
+ class="vm-crud-shell__btn is-ghost"
144
+ :disabled="submitting"
145
+ @click="onCloseClick"
146
+ >
147
+ {{ closeText || '取消' }}
148
+ </button>
149
+ <button
150
+ v-if="confirm !== false && !readonly"
151
+ type="button"
152
+ class="vm-crud-shell__btn is-primary"
153
+ :disabled="submitting"
154
+ @click="onConfirmClick"
155
+ >
156
+ {{ submitting ? '提交中…' : confirmText || '确定' }}
157
+ </button>
158
+ <slot name="footer-append" v-bind="footerSlotProps" />
159
+ </template>
160
+ </DialogFooter>
161
+ </DialogContent>
162
+ </Dialog>
163
+ </template>
164
+
165
+ <script setup lang="ts">
166
+ import { ref, computed, watch, useSlots, toValue } from 'vue'
167
+ import { preventDialogOutsideClose } from '../lib/dialog-float'
168
+ import { resolveCrudComponent } from '../crud/registry'
169
+ import { resolveItemRequired, validateFormFields } from '../crud/validate'
170
+ import { resolveFormSpan, useFormCols } from '../crud/span'
171
+ import { mapDictOptions } from '../crud/auto-fields'
172
+ import type { CrudFormItem } from '../../../typings/admin/comm/crud'
173
+
174
+ defineOptions({ name: 'vm-form-dialog' })
175
+
176
+ export type VmFormDialogSubmitHelpers = {
177
+ /** 关闭弹窗并复位 submitting */
178
+ close: () => void
179
+ /** 仅复位 submitting(弹窗保持打开) */
180
+ done: () => void
181
+ }
182
+
183
+ const props = withDefaults(
184
+ defineProps<{
185
+ open?: boolean
186
+ /** 表单数据(v-model) */
187
+ modelValue?: Record<string, unknown>
188
+ title?: string
189
+ items?: CrudFormItem[]
190
+ height?: string | number
191
+ confirm?: boolean
192
+ close?: boolean
193
+ footer?: boolean
194
+ confirmText?: string
195
+ closeText?: string
196
+ layout?: 'form' | 'fill'
197
+ /** 只读:隐藏确定、控件禁用 */
198
+ readonly?: boolean
199
+ }>(),
200
+ {
201
+ open: false,
202
+ modelValue: () => ({}),
203
+ title: '',
204
+ items: () => [],
205
+ confirm: true,
206
+ close: true,
207
+ footer: true,
208
+ layout: 'form',
209
+ readonly: false,
210
+ },
211
+ )
212
+
213
+ const emit = defineEmits<{
214
+ 'update:open': [v: boolean]
215
+ 'update:modelValue': [v: Record<string, unknown>]
216
+ close: []
217
+ /** 校验通过后触发;业务自行请求,完成后调用 helpers.close() */
218
+ submit: [data: Record<string, unknown>, helpers: VmFormDialogSubmitHelpers]
219
+ }>()
220
+
221
+ const slots = useSlots()
222
+ const formCols = useFormCols()
223
+ const submitting = ref(false)
224
+ const fieldErrors = ref<Record<string, string>>({})
225
+
226
+ const form = computed({
227
+ get: () => props.modelValue || {},
228
+ set: (v) => emit('update:modelValue', v),
229
+ })
230
+
231
+ function normalizeHeight(h?: string | number) {
232
+ if (h == null || h === '') return ''
233
+ if (typeof h === 'number') return `${h}px`
234
+ return String(h)
235
+ }
236
+
237
+ const heightCss = computed(() => normalizeHeight(props.height))
238
+
239
+ const visibleItems = computed(() =>
240
+ (props.items || []).filter((it) => {
241
+ if (typeof it.hidden === 'function') return !it.hidden(form.value)
242
+ return !it.hidden
243
+ }),
244
+ )
245
+
246
+ const formItemsBeforeSlot = computed(() =>
247
+ visibleItems.value.filter((it) => itemType(it) !== 'textarea'),
248
+ )
249
+ const formItemsAfterSlot = computed(() =>
250
+ visibleItems.value.filter((it) => itemType(it) === 'textarea'),
251
+ )
252
+
253
+ const isBodyEmpty = computed(
254
+ () => !visibleItems.value.length && !slots.default,
255
+ )
256
+
257
+ const footerSlotProps = computed(() => ({
258
+ close: onCloseClick,
259
+ submit: onConfirmClick,
260
+ confirm: onConfirmClick,
261
+ submitting: submitting.value,
262
+ form: form.value,
263
+ }))
264
+
265
+ function setField(prop: string, value: unknown) {
266
+ if (props.readonly) return
267
+ if (fieldErrors.value[prop]) {
268
+ const next = { ...fieldErrors.value }
269
+ delete next[prop]
270
+ fieldErrors.value = next
271
+ }
272
+ form.value = { ...form.value, [prop]: value }
273
+ }
274
+
275
+ function inputBind(item: CrudFormItem) {
276
+ if (itemType(item) !== 'number') return {}
277
+ const raw = toValue(item.component?.props)
278
+ if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
279
+ return raw as Record<string, unknown>
280
+ }
281
+ return { type: 'number', controls: false }
282
+ }
283
+
284
+ function itemType(item: CrudFormItem) {
285
+ if (item.type) return item.type
286
+ const name = item.component?.name
287
+ if (typeof name !== 'string') return 'input'
288
+ if (name === 'vm-switch') return 'switch'
289
+ if (name === 'vm-select' || name === 'vm-tree-select') return 'select'
290
+ return 'input'
291
+ }
292
+
293
+ function fieldClass(item: CrudFormItem) {
294
+ const name = item.component?.name
295
+ if (itemType(item) === 'switch') return 'vm-crud-upsert__switch-field'
296
+ if (name === 'vm-upload' || name === 'upload') return 'vm-crud-upsert__upload-field'
297
+ if (name === 'vm-markdown') return 'vm-crud-upsert__markdown-field'
298
+ if (name === 'vm-json-editor') return 'vm-crud-upsert__json-field'
299
+ if (name === 'vm-richtext') return 'vm-crud-upsert__richtext-field'
300
+ return 'vm-crud-upsert__control'
301
+ }
302
+
303
+ function resolveField(item: CrudFormItem) {
304
+ if (itemType(item) === 'radio' || itemType(item) === 'textarea') return undefined
305
+ const name = item.component?.name
306
+ if (typeof name === 'string') {
307
+ if (name === 'upload') return resolveCrudComponent('vm-upload')
308
+ const comp = resolveCrudComponent(name)
309
+ if (comp) return comp
310
+ } else if (name) {
311
+ return name
312
+ }
313
+ if (item.type === 'switch') return resolveCrudComponent('vm-switch')
314
+ if (item.type === 'select') {
315
+ if (item.multiple) return resolveCrudComponent('vm-multi-select')
316
+ return resolveCrudComponent('vm-select')
317
+ }
318
+ return undefined
319
+ }
320
+
321
+ function fieldProps(item: CrudFormItem) {
322
+ const raw = item.component?.props
323
+ const unwrapped = toValue(raw)
324
+ if (Array.isArray(unwrapped)) {
325
+ return {
326
+ options: unwrapped,
327
+ placeholder: item.placeholder,
328
+ refreshOnChange: false,
329
+ }
330
+ }
331
+ return {
332
+ options: resolveOptions(item),
333
+ placeholder: item.placeholder,
334
+ refreshOnChange: false,
335
+ ...(unwrapped && typeof unwrapped === 'object'
336
+ ? (unwrapped as Record<string, unknown>)
337
+ : {}),
338
+ }
339
+ }
340
+
341
+ function resolveOptions(item: CrudFormItem | CrudFormItem['options']) {
342
+ if (item && typeof item === 'object' && 'prop' in item) {
343
+ const formItem = item as CrudFormItem
344
+ if (formItem.dict) return mapDictOptions(formItem.dict)
345
+ const list = formItem.options == null ? [] : toValue(formItem.options)
346
+ return Array.isArray(list) ? list : []
347
+ }
348
+ const list = item == null ? [] : toValue(item)
349
+ return Array.isArray(list) ? list : []
350
+ }
351
+
352
+ function itemSpan(item: CrudFormItem) {
353
+ return resolveFormSpan(item.span, formCols.value)
354
+ }
355
+
356
+ function textareaProps(item: CrudFormItem) {
357
+ const raw = toValue(item.component?.props)
358
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {}
359
+ return raw as Record<string, unknown>
360
+ }
361
+
362
+ function textareaClearable(item: CrudFormItem) {
363
+ return textareaProps(item).clearable !== false
364
+ }
365
+
366
+ function textareaRows(item: CrudFormItem) {
367
+ const rows = textareaProps(item).rows
368
+ return typeof rows === 'number' || typeof rows === 'string' ? rows : undefined
369
+ }
370
+
371
+ function textareaStyle(item: CrudFormItem) {
372
+ const p = textareaProps(item)
373
+ const style: Record<string, string> = {
374
+ ...((p.style && typeof p.style === 'object'
375
+ ? (p.style as Record<string, string>)
376
+ : {}) as Record<string, string>),
377
+ }
378
+ if (p.height != null && p.height !== '') {
379
+ const h = typeof p.height === 'number' ? `${p.height}px` : String(p.height)
380
+ style.height = h
381
+ if (!style.minHeight) style.minHeight = h
382
+ }
383
+ return Object.keys(style).length ? style : undefined
384
+ }
385
+
386
+ function onCloseClick() {
387
+ submitting.value = false
388
+ fieldErrors.value = {}
389
+ emit('update:open', false)
390
+ emit('close')
391
+ }
392
+
393
+ async function onConfirmClick() {
394
+ if (props.readonly || submitting.value) return
395
+ const errors = await validateFormFields(visibleItems.value, form.value)
396
+ fieldErrors.value = errors
397
+ if (Object.keys(errors).length) return
398
+ submitting.value = true
399
+ const helpers: VmFormDialogSubmitHelpers = {
400
+ close: () => {
401
+ submitting.value = false
402
+ fieldErrors.value = {}
403
+ emit('update:open', false)
404
+ emit('close')
405
+ },
406
+ done: () => {
407
+ submitting.value = false
408
+ },
409
+ }
410
+ emit('submit', { ...form.value }, helpers)
411
+ }
412
+
413
+ watch(
414
+ () => props.open,
415
+ (open) => {
416
+ if (!open) {
417
+ submitting.value = false
418
+ fieldErrors.value = {}
419
+ return
420
+ }
421
+ const data = { ...form.value }
422
+ for (const it of props.items || []) {
423
+ if (data[it.prop] === undefined && it.value !== undefined) {
424
+ data[it.prop] = it.value
425
+ }
426
+ }
427
+ form.value = data
428
+ },
429
+ )
430
+
431
+ defineExpose({
432
+ form,
433
+ submit: onConfirmClick,
434
+ close: onCloseClick,
435
+ })
436
+ </script>
437
+
438
+ <style lang="scss" scoped>
439
+ .vm-crud-upsert__body {
440
+ display: grid;
441
+ flex: 1 1 auto;
442
+ grid-template-columns: repeat(24, minmax(0, 1fr));
443
+ gap: 16px;
444
+ min-height: 0;
445
+ overflow: auto;
446
+ overscroll-behavior: contain;
447
+ padding: 18px 22px;
448
+ align-content: start;
449
+
450
+ &.is-fill {
451
+ display: flex;
452
+ flex-direction: column;
453
+ align-items: stretch;
454
+ gap: 0;
455
+ padding: 0;
456
+
457
+ > * {
458
+ width: 100%;
459
+ min-width: 0;
460
+ }
461
+ }
462
+
463
+ /* slot 内容在宿主 SFC,须 :deep 才能命中 label / control 样式 */
464
+ :deep(.vm-crud-upsert__field) {
465
+ display: grid;
466
+ gap: 6px;
467
+ align-content: start;
468
+ min-width: 0;
469
+ }
470
+
471
+ :deep(.vm-crud-upsert__label) {
472
+ font-size: 12px;
473
+ color: var(--muted-foreground);
474
+ }
475
+
476
+ :deep(.vm-crud-upsert__req) {
477
+ color: var(--destructive);
478
+ }
479
+
480
+ :deep(.vm-crud-upsert__control) {
481
+ width: 100%;
482
+ height: var(--vm-control-height) !important;
483
+ min-height: var(--vm-control-height);
484
+ border-radius: var(--vm-control-radius) !important;
485
+ font-size: var(--vm-control-font-size) !important;
486
+ line-height: 1.4 !important;
487
+ }
488
+
489
+ /* Input 外壳:高度交给内部 input,避免 wrap 压扁清除按钮 */
490
+ :deep(.vm-input-wrap.vm-crud-upsert__control) {
491
+ height: auto !important;
492
+ min-height: var(--vm-control-height);
493
+ }
494
+
495
+ /* 单行 input:白底;select / tree-select 等:淡蓝底 */
496
+ :deep(input.vm-crud-upsert__control),
497
+ :deep(.vm-crud-upsert__control[data-slot='input']),
498
+ :deep(.vm-input-wrap.vm-crud-upsert__control input) {
499
+ background: var(--card) !important;
500
+ }
501
+
502
+ :deep(.vm-crud-select.vm-crud-upsert__control),
503
+ :deep(.vm-group-cascader.vm-crud-upsert__control),
504
+ :deep(.vm-multi-select.vm-crud-upsert__control),
505
+ :deep(.vm-date-picker.vm-crud-upsert__control),
506
+ :deep(.vm-icon-picker.vm-crud-upsert__control) {
507
+ background: var(--muted) !important;
508
+ }
509
+
510
+ :deep(.vm-tree-select.vm-crud-upsert__control .vm-tree-select__trigger) {
511
+ background: var(--muted) !important;
512
+ }
513
+
514
+ @media (max-width: 768px) {
515
+ grid-template-columns: repeat(12, minmax(0, 1fr));
516
+ flex: 1 1 auto;
517
+ min-height: 0;
518
+ gap: 10px;
519
+ padding: 10px 12px;
520
+ }
521
+ }
522
+
523
+ .vm-crud-upsert__empty {
524
+ grid-column: 1 / -1;
525
+ display: flex;
526
+ min-height: 160px;
527
+ align-items: center;
528
+ justify-content: center;
529
+ }
530
+
531
+ .vm-crud-upsert__field {
532
+ &.is-error :deep(.vm-crud-upsert__control) {
533
+ border-color: var(--destructive) !important;
534
+ }
535
+ }
536
+
537
+ .vm-crud-upsert__error {
538
+ margin: 0;
539
+ font-size: 12px;
540
+ line-height: 1.4;
541
+ color: var(--destructive);
542
+ }
543
+
544
+ :deep(.vm-group-cascader.vm-crud-upsert__control) {
545
+ height: auto !important;
546
+ min-height: var(--vm-control-height);
547
+ }
548
+
549
+ /* 上传:勿套用单行高度;多图时铺满 span 以便平铺换行 */
550
+ :deep(.vm-upload.vm-crud-upsert__upload-field) {
551
+ width: 100%;
552
+ max-width: 100%;
553
+ height: auto !important;
554
+ min-height: 0;
555
+ background: transparent !important;
556
+ }
557
+
558
+ /* Markdown:保留组件自身 min-height,勿被 control 行高压扁 */
559
+ :deep(.vm-markdown.vm-crud-upsert__markdown-field) {
560
+ width: 100%;
561
+ height: auto !important;
562
+ min-height: 0;
563
+ background: transparent !important;
564
+ }
565
+
566
+ /* JSON:与备注 textarea 同视觉,勿套单行 control */
567
+ :deep(.vm-json-editor.vm-crud-upsert__json-field) {
568
+ width: 100%;
569
+ height: auto !important;
570
+ min-height: 0;
571
+ background: transparent !important;
572
+ }
573
+
574
+ /* 富文本:保留组件默认 minHeight,勿被 control 行高压扁 */
575
+ :deep(.vm-richtext.vm-crud-upsert__richtext-field) {
576
+ width: 100%;
577
+ height: auto !important;
578
+ min-height: 0;
579
+ background: transparent !important;
580
+ }
581
+
582
+ :deep(.vm-crud-upsert__switch-field.vm-switch) {
583
+ width: 40px;
584
+ height: 22px;
585
+ margin-top: 8px;
586
+ }
587
+
588
+ .vm-crud-upsert__textarea-wrap {
589
+ position: relative;
590
+ width: 100%;
591
+ min-width: 0;
592
+ }
593
+
594
+ .vm-crud-upsert__textarea-clear {
595
+ position: absolute;
596
+ top: 10px;
597
+ right: var(--vm-control-padding-x);
598
+ z-index: 1;
599
+ color: var(--muted-foreground);
600
+ font-size: 14px;
601
+ line-height: 1;
602
+ cursor: pointer;
603
+
604
+ &:hover {
605
+ color: var(--foreground);
606
+ }
607
+ }
608
+
609
+ .vm-crud-upsert__textarea,
610
+ :deep(.vm-crud-upsert__textarea) {
611
+ width: 100%;
612
+ min-height: 72px;
613
+ padding: 8px 28px 8px var(--vm-control-padding-x);
614
+ border: 1px solid var(--input);
615
+ border-radius: var(--vm-control-radius);
616
+ background: var(--muted);
617
+ color: var(--foreground);
618
+ font-size: 14px;
619
+ line-height: 1.5;
620
+ resize: vertical;
621
+ outline: none;
622
+ transition: border-color 0.15s ease;
623
+
624
+ &:focus,
625
+ &:focus-visible {
626
+ border-color: var(--focus-border);
627
+ box-shadow: none;
628
+ }
629
+ }
630
+ </style>