vome-core 0.0.19 → 0.0.21

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 (72) hide show
  1. package/dist/admin/config/plugin-dev.js +1 -1
  2. package/dist/admin/crud/components/vm-audio.vue +217 -0
  3. package/dist/admin/crud/components/vm-column-custom.vue +50 -56
  4. package/dist/admin/crud/components/vm-file-preview.vue +90 -0
  5. package/dist/admin/crud/components/vm-image.vue +413 -0
  6. package/dist/admin/crud/components/vm-preview-viewer.vue +520 -0
  7. package/dist/admin/crud/components/vm-richtext.vue +275 -0
  8. package/dist/admin/crud/components/vm-upload-item.vue +92 -31
  9. package/dist/admin/crud/components/vm-upload.vue +2 -6
  10. package/dist/admin/crud/components/vm-user-select.vue +48 -49
  11. package/dist/admin/crud/config.js +1 -1
  12. package/dist/admin/crud/confirm.js +1 -1
  13. package/dist/admin/crud/dict.js +1 -1
  14. package/dist/admin/crud/index.d.ts +6 -3
  15. package/dist/admin/crud/index.js +1 -1
  16. package/dist/admin/crud/key.js +1 -1
  17. package/dist/admin/crud/mitt.js +1 -1
  18. package/dist/admin/crud/plugins.js +1 -1
  19. package/dist/admin/crud/span.js +1 -1
  20. package/dist/admin/crud/style.js +1 -1
  21. package/dist/admin/crud/validate.js +1 -1
  22. package/dist/admin/crud/vm-adv-search.vue +39 -32
  23. package/dist/admin/crud/vm-crud.vue +3 -8
  24. package/dist/admin/crud/vm-export-btn.vue +169 -0
  25. package/dist/admin/crud/{vm-flex1.vue → vm-flex.vue} +3 -3
  26. package/dist/admin/crud/vm-search.vue +18 -3
  27. package/dist/admin/crud/vm-toolbar.vue +20 -0
  28. package/dist/admin/crud/vm-upsert.vue +166 -119
  29. package/dist/admin/directives/perm.js +1 -1
  30. package/dist/admin/hooks/useUpload.js +1 -1
  31. package/dist/admin/lib/browser.js +1 -1
  32. package/dist/admin/lib/cn.js +1 -1
  33. package/dist/admin/lib/dialog-float.d.ts +2 -0
  34. package/dist/admin/lib/dialog-float.js +1 -1
  35. package/dist/admin/lib/export-excel.d.ts +11 -0
  36. package/dist/admin/lib/export-excel.js +1 -0
  37. package/dist/admin/lib/file-preview.d.ts +11 -0
  38. package/dist/admin/lib/file-preview.js +1 -0
  39. package/dist/admin/lib/menu.js +1 -1
  40. package/dist/admin/lib/tree.js +1 -1
  41. package/dist/admin/lib/upload.js +1 -1
  42. package/dist/admin/lib/video-frame.d.ts +9 -0
  43. package/dist/admin/lib/video-frame.js +1 -0
  44. package/dist/admin/styles/base.css +100 -0
  45. package/dist/index.js +1 -1
  46. package/dist/server/index.js +1 -1
  47. package/dist/shared/excel.d.ts +13 -0
  48. package/dist/shared/excel.js +1 -0
  49. package/dist/shared/index.js +1 -1
  50. package/dist/shared/tree.js +1 -1
  51. package/dist/src/core/context/index.d.ts +11 -1
  52. package/dist/src/routing/base.d.ts +2 -0
  53. package/dist/src/server/index.d.ts +2 -0
  54. package/dist/src/shared/excel.d.ts +13 -0
  55. package/dist/src/shared/index.d.ts +2 -0
  56. package/dist/typings/routing/crud.d.ts +2 -0
  57. package/package.json +3 -2
  58. package/src/shared/excel.d.ts +13 -0
  59. package/src/shared/excel.ts +46 -0
  60. package/src/shared/index.ts +6 -0
  61. package/typings/admin/comm/crud.d.ts +8 -2
  62. package/typings/admin/comm/crud.ts +8 -3
  63. package/dist/admin/crud/components/vm-file-icon.vue +0 -55
  64. package/dist/admin/crud/vm-dialog.vue +0 -235
  65. package/dist/admin/static/fileicon/demo.css +0 -539
  66. package/dist/admin/static/fileicon/demo_index.html +0 -901
  67. package/dist/admin/static/fileicon/iconfont.css +0 -139
  68. package/dist/admin/static/fileicon/iconfont.js +0 -1
  69. package/dist/admin/static/fileicon/iconfont.json +0 -226
  70. package/dist/admin/static/fileicon/iconfont.ttf +0 -0
  71. package/dist/admin/static/fileicon/iconfont.woff +0 -0
  72. package/dist/admin/static/fileicon/iconfont.woff2 +0 -0
@@ -0,0 +1,169 @@
1
+ <template>
2
+ <button
3
+ v-if="visible"
4
+ type="button"
5
+ class="vm-crud-toolbar__btn is-ghost"
6
+ :disabled="disabled || submitting"
7
+ @click="onExport"
8
+ >
9
+ <i class="ri-download-2-line" />
10
+ {{ buttonText }}
11
+ </button>
12
+ </template>
13
+
14
+ <script setup lang="ts">
15
+ import { computed, inject, ref } from 'vue'
16
+ import { toast } from 'vue-sonner'
17
+ import {
18
+ exportCrudExcel,
19
+ flattenExportRows,
20
+ pickExportColumns,
21
+ } from '../lib/export-excel'
22
+ import {
23
+ injectTableOptions,
24
+ TABLE_API_KEY,
25
+ type TableApi,
26
+ } from './key'
27
+ import { useCrud } from './useCrud'
28
+
29
+ defineOptions({ name: 'vm-export-btn' })
30
+
31
+ const props = withDefaults(
32
+ defineProps<{
33
+ /** 列配置;默认取 Table.getColumns() 或 useTable columns */
34
+ columns?: CrudColumn[]
35
+ /** 自定义数据源;默认按当前筛选 POST page 拉全量 */
36
+ data?:
37
+ | Record<string, unknown>[]
38
+ | ((params: Record<string, unknown>) => Promise<Record<string, unknown>[]>)
39
+ filename?: string | (() => string)
40
+ maxExportLimit?: number
41
+ disabled?: boolean
42
+ text?: string
43
+ /** 权限键,默认 page */
44
+ permission?: keyof CrudPermission
45
+ }>(),
46
+ {
47
+ filename: '导出',
48
+ maxExportLimit: 5000,
49
+ disabled: false,
50
+ permission: 'page',
51
+ },
52
+ )
53
+
54
+ const crud = useCrud()
55
+ const tableOpts = injectTableOptions()
56
+ const tableApi = inject(TABLE_API_KEY, null as TableApi | null)
57
+ const submitting = ref(false)
58
+
59
+ const visible = computed(() => Boolean(crud.getPermission(props.permission)))
60
+
61
+ const buttonText = computed(
62
+ () => props.text || crud.dict.label.export || '导出',
63
+ )
64
+
65
+ const exportColumns = computed(() => {
66
+ const raw =
67
+ props.columns?.length
68
+ ? props.columns
69
+ : tableApi?.getColumns() || tableOpts?.columns || []
70
+ return pickExportColumns(raw)
71
+ })
72
+
73
+ function resolveFilename() {
74
+ const raw = props.filename
75
+ const name = typeof raw === 'function' ? raw() : raw
76
+ return name || '导出'
77
+ }
78
+
79
+ async function fetchRows(): Promise<Record<string, unknown>[]> {
80
+ if (props.data) {
81
+ if (typeof props.data === 'function') {
82
+ return props.data({ ...crud.getParams() })
83
+ }
84
+ return props.data
85
+ }
86
+
87
+ const svc = crud.service
88
+ const limit =
89
+ props.maxExportLimit && props.maxExportLimit > 0
90
+ ? props.maxExportLimit
91
+ : Math.max(crud.pagination.value.total, 1)
92
+
93
+ if (svc.page) {
94
+ const body: Record<string, unknown> = {
95
+ ...crud.getParams(),
96
+ [crud.dict.pagination.page]: 1,
97
+ [crud.dict.pagination.size]: limit,
98
+ }
99
+ if (crud.trashMode.value === 'onlyTrashed') body.onlyTrashed = true
100
+ if (crud.trashMode.value === 'withTrashed') body.withTrashed = true
101
+ const res = await svc.page(body)
102
+ return res.list || []
103
+ }
104
+
105
+ if (svc.list) {
106
+ const body: Record<string, unknown> = { ...crud.getParams() }
107
+ if (crud.trashMode.value === 'onlyTrashed') body.onlyTrashed = true
108
+ if (crud.trashMode.value === 'withTrashed') body.withTrashed = true
109
+ const list = await svc.list(body)
110
+ return Array.isArray(list) ? list : []
111
+ }
112
+
113
+ return flattenExportRows(crud.list.value, tableOpts?.tree)
114
+ }
115
+
116
+ async function onExport() {
117
+ if (submitting.value || props.disabled) return
118
+ if (!exportColumns.value.length) {
119
+ toast.error('没有可导出的列')
120
+ return
121
+ }
122
+
123
+ submitting.value = true
124
+ try {
125
+ const rows = await fetchRows()
126
+ if (!rows.length) {
127
+ toast.warning('暂无数据可导出')
128
+ return
129
+ }
130
+ const flat = flattenExportRows(rows, tableOpts?.tree)
131
+ exportCrudExcel(exportColumns.value, flat, resolveFilename())
132
+ toast.success('导出成功')
133
+ } catch (e) {
134
+ toast.error(e instanceof Error ? e.message : '导出失败')
135
+ } finally {
136
+ submitting.value = false
137
+ }
138
+ }
139
+ </script>
140
+
141
+ <style lang="scss" scoped>
142
+ .vm-crud-toolbar__btn {
143
+ display: inline-flex;
144
+ height: 36px;
145
+ align-items: center;
146
+ gap: 4px;
147
+ padding: 0 14px;
148
+ border: none;
149
+ border-radius: 12px;
150
+ font-size: 13px;
151
+ font-weight: 600;
152
+ cursor: pointer;
153
+ transition: background 0.15s ease, color 0.15s ease, opacity 0.15s ease;
154
+
155
+ &:disabled {
156
+ cursor: not-allowed;
157
+ opacity: 0.45;
158
+ }
159
+
160
+ &.is-ghost {
161
+ background: var(--muted);
162
+ color: var(--foreground);
163
+
164
+ &:hover:not(:disabled) {
165
+ background: color-mix(in srgb, var(--muted) 70%, var(--foreground) 8%);
166
+ }
167
+ }
168
+ }
169
+ </style>
@@ -1,13 +1,13 @@
1
1
  <template>
2
- <div class="vm-crud-flex1" />
2
+ <div class="vm-crud-flex" />
3
3
  </template>
4
4
 
5
5
  <script setup lang="ts">
6
- defineOptions({ name: 'vm-flex1' })
6
+ defineOptions({ name: 'vm-flex' })
7
7
  </script>
8
8
 
9
9
  <style lang="scss" scoped>
10
- .vm-crud-flex1 {
10
+ .vm-crud-flex {
11
11
  flex: 1;
12
12
  min-width: 0;
13
13
  }
@@ -186,14 +186,29 @@ function resolveField(item: CrudSearchItem) {
186
186
  }
187
187
 
188
188
  function fieldProps(item: CrudSearchItem) {
189
+ const name = String(item.placeholder || item.label || '').trim()
190
+ const rangePlaceholders =
191
+ item.type === 'number-range' && name
192
+ ? {
193
+ startPlaceholder: `${name}最小值`,
194
+ endPlaceholder: `${name}最大值`,
195
+ }
196
+ : item.type === 'daterange' && name
197
+ ? {
198
+ startPlaceholder: `${name}开始`,
199
+ endPlaceholder: `${name}结束`,
200
+ }
201
+ : {}
189
202
  return {
190
203
  options: item.options,
191
204
  placeholder: item.placeholder || item.label,
192
205
  refreshOnChange: false,
193
206
  precision: item.range?.rangeType,
194
- mode: item.range?.rangeType === 'float' || item.range?.rangeType === 'int'
195
- ? item.range.rangeType
196
- : undefined,
207
+ mode:
208
+ item.range?.rangeType === 'float' || item.range?.rangeType === 'int'
209
+ ? item.range.rangeType
210
+ : undefined,
211
+ ...rangePlaceholders,
197
212
  ...(item.component?.props || {}),
198
213
  }
199
214
  }
@@ -20,6 +20,14 @@
20
20
  <i class="ri-delete-bin-line" />
21
21
  {{ deleteText || crud.dict.label.multiDelete }}
22
22
  </button>
23
+ <vm-export-btn
24
+ v-if="showExport"
25
+ :text="exportText || crud.dict.label.export"
26
+ :filename="exportFilename"
27
+ :max-export-limit="maxExportLimit"
28
+ :columns="exportColumns"
29
+ :data="exportData"
30
+ />
23
31
  <slot />
24
32
  </div>
25
33
  <div class="vm-crud-toolbar__right">
@@ -57,6 +65,7 @@
57
65
  <script setup lang="ts">
58
66
  import { computed } from 'vue'
59
67
  import { useCrud } from './useCrud'
68
+ import VmExportBtn from './vm-export-btn.vue'
60
69
  defineOptions({ name: 'vm-toolbar' })
61
70
 
62
71
  const props = withDefaults(
@@ -66,21 +75,32 @@ const props = withDefaults(
66
75
  search?: boolean
67
76
  showAdd?: boolean
68
77
  showDelete?: boolean
78
+ /** 显示导出(默认开;需 page/list 权限) */
79
+ showExport?: boolean
69
80
  /** 打开新增表单时的默认值 */
70
81
  addData?: Record<string, unknown> | (() => Record<string, unknown>)
71
82
  /** 回收站中是否隐藏新增(默认 true) */
72
83
  hideAddInTrash?: boolean
73
84
  addText?: string
74
85
  deleteText?: string
86
+ exportText?: string
87
+ exportFilename?: string | (() => string)
88
+ maxExportLimit?: number
89
+ exportColumns?: CrudColumn[]
90
+ exportData?:
91
+ | Record<string, unknown>[]
92
+ | ((params: Record<string, unknown>) => Promise<Record<string, unknown>[]>)
75
93
  }>(),
76
94
  {
77
95
  trash: true,
78
96
  search: true,
79
97
  showAdd: true,
80
98
  showDelete: true,
99
+ showExport: true,
81
100
  hideAddInTrash: true,
82
101
  addText: '新增',
83
102
  deleteText: '删除',
103
+ maxExportLimit: 5000,
84
104
  },
85
105
  )
86
106
 
@@ -1,20 +1,27 @@
1
1
  <template>
2
2
  <Dialog
3
3
  :open="crud.upsertVisible.value"
4
- @update:open="(v) => (!v ? crud.upsertClose() : undefined)"
4
+ @update:open="(v) => (!v ? onCloseClick() : undefined)"
5
5
  >
6
6
  <DialogContent
7
7
  shell="crud"
8
8
  class="vm-crud-upsert"
9
- :class="{ 'is-empty': isBodyEmpty }"
10
- @pointer-down-outside="onPointerDownOutside"
11
- @focus-outside="onFocusOutside"
12
- @interact-outside="onInteractOutside"
9
+ :class="{
10
+ 'is-empty': isBodyEmpty,
11
+ 'is-height': !!heightCss,
12
+ }"
13
+ :style="heightCss ? { '--vm-dialog-height': heightCss } : undefined"
14
+ @pointer-down-outside="preventDialogOutsideClose"
15
+ @focus-outside="preventDialogOutsideClose"
16
+ @interact-outside="preventDialogOutsideClose"
13
17
  >
14
- <DialogHeader class="vm-crud-upsert__head">
15
- <DialogTitle class="vm-crud-upsert__title">{{ titleText }}</DialogTitle>
18
+ <DialogHeader class="vm-crud-shell__head">
19
+ <DialogTitle class="vm-crud-shell__title">{{ titleText }}</DialogTitle>
16
20
  </DialogHeader>
17
- <div class="vm-crud-upsert__body">
21
+ <div
22
+ class="vm-crud-upsert__body"
23
+ :class="{ 'is-fill': layout === 'fill' }"
24
+ >
18
25
  <template v-if="!isBodyEmpty">
19
26
  <div
20
27
  v-for="item in formItemsBeforeSlot"
@@ -33,7 +40,7 @@
33
40
  :name="`upsert-${item.prop}`"
34
41
  :options="resolveOptions(item.options)"
35
42
  :model-value="String(crud.upsertForm.value[item.prop] ?? '')"
36
- :disabled="isInfo"
43
+ :disabled="isInfo || item.disabled"
37
44
  @update:model-value="(v) => setField(item.prop, v)"
38
45
  />
39
46
 
@@ -43,7 +50,7 @@
43
50
  :class="fieldClass(item)"
44
51
  v-bind="fieldProps(item)"
45
52
  :model-value="crud.upsertForm.value[item.prop]"
46
- :disabled="isInfo"
53
+ :disabled="isInfo || item.disabled"
47
54
  @update:model-value="(v: unknown) => setField(item.prop, v)"
48
55
  />
49
56
 
@@ -51,7 +58,7 @@
51
58
  v-else
52
59
  class="vm-crud-upsert__control"
53
60
  :placeholder="item.placeholder"
54
- :disabled="isInfo"
61
+ :disabled="isInfo || item.disabled"
55
62
  :aria-invalid="fieldErrors[item.prop] ? true : undefined"
56
63
  :model-value="String(crud.upsertForm.value[item.prop] ?? '')"
57
64
  @update:model-value="(v) => setField(item.prop, v)"
@@ -76,7 +83,7 @@
76
83
  <textarea
77
84
  class="vm-crud-upsert__textarea"
78
85
  :placeholder="item.placeholder"
79
- :disabled="isInfo"
86
+ :disabled="isInfo || item.disabled"
80
87
  :value="String(crud.upsertForm.value[item.prop] ?? '')"
81
88
  @input="setField(item.prop, ($event.target as HTMLTextAreaElement).value)"
82
89
  />
@@ -89,24 +96,34 @@
89
96
  <vm-empty kind="暂无内容" />
90
97
  </div>
91
98
  </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>
99
+ <DialogFooter v-if="showFooter" class="vm-crud-shell__foot">
100
+ <slot
101
+ v-if="slots.footer"
102
+ name="footer"
103
+ v-bind="footerSlotProps"
104
+ />
105
+ <template v-else>
106
+ <slot name="footer-prepend" v-bind="footerSlotProps" />
107
+ <button
108
+ v-if="showClose"
109
+ type="button"
110
+ class="vm-crud-shell__btn is-ghost"
111
+ :disabled="submitting"
112
+ @click="onCloseClick"
113
+ >
114
+ {{ closeLabel }}
115
+ </button>
116
+ <button
117
+ v-if="showConfirm"
118
+ type="button"
119
+ class="vm-crud-shell__btn is-primary"
120
+ :disabled="submitting"
121
+ @click="onConfirmClick"
122
+ >
123
+ {{ submitting ? '提交中…' : confirmLabel }}
124
+ </button>
125
+ <slot name="footer-append" v-bind="footerSlotProps" />
126
+ </template>
110
127
  </DialogFooter>
111
128
  </DialogContent>
112
129
  </Dialog>
@@ -121,17 +138,39 @@ import { getCrudStyle } from './style'
121
138
  import { resolveCrudComponent } from './registry'
122
139
  import { applySetRules, validateFormFields } from './validate'
123
140
  import { resolveFormSpan, useFormCols } from './span'
124
- import { isDialogFloatTarget } from '../lib/dialog-float'
141
+ import { preventDialogOutsideClose } from '../lib/dialog-float'
125
142
  defineOptions({ name: 'vm-upsert' })
126
143
 
127
144
  const props = withDefaults(
128
145
  defineProps<{
129
146
  items?: CrudFormItem[]
130
147
  title?: string
148
+ /** 弹窗高度,如 800 / '800px';最高受壳层 800px 约束 */
149
+ height?: string | number
150
+ /** 是否显示确定;未设时 info 模式自动隐藏 */
151
+ confirm?: boolean
152
+ /** 是否显示关闭,默认 true */
153
+ close?: boolean
154
+ /** false 时隐藏整个底栏 */
155
+ footer?: boolean
156
+ confirmText?: string
157
+ closeText?: string
158
+ /** form=栅格表单;fill=纵向铺满(只读列表/文档) */
159
+ layout?: 'form' | 'fill'
131
160
  }>(),
132
- {},
161
+ {
162
+ close: undefined,
163
+ confirm: undefined,
164
+ footer: true,
165
+ layout: 'form',
166
+ },
133
167
  )
134
168
 
169
+ const emit = defineEmits<{
170
+ close: []
171
+ confirm: []
172
+ }>()
173
+
135
174
  const slots = useSlots()
136
175
  const crud = useCrud()
137
176
  const upsertOpts = injectUpsertOptions()
@@ -139,6 +178,16 @@ const formCols = useFormCols()
139
178
  const submitting = ref(false)
140
179
  const fieldErrors = ref<Record<string, string>>({})
141
180
 
181
+ function normalizeHeight(h?: string | number) {
182
+ if (h == null || h === '') return ''
183
+ if (typeof h === 'number') return `${h}px`
184
+ return String(h)
185
+ }
186
+
187
+ const heightCss = computed(() =>
188
+ normalizeHeight(props.height ?? upsertOpts?.dialog?.height),
189
+ )
190
+
142
191
  const mergedItems = computed(() => {
143
192
  const raw = props.items?.length ? props.items : upsertOpts?.items || []
144
193
  const useRules = getCrudStyle().form.plugins.some(
@@ -171,27 +220,45 @@ const formItemsAfterSlot = computed(() =>
171
220
  )
172
221
 
173
222
  const isInfo = computed(() => crud.upsertMode.value === 'info')
174
- /** dialog.confirm 显式控制;未设时 info 模式自动隐藏确定 */
223
+ /** prop > dialog.confirm > info 自动隐藏 */
175
224
  const showConfirm = computed(() => {
225
+ if (typeof props.confirm === 'boolean') return props.confirm
176
226
  const flag = upsertOpts?.dialog?.confirm
177
227
  if (typeof flag === 'boolean') return flag
178
228
  return !isInfo.value
179
229
  })
230
+ const showClose = computed(() => {
231
+ if (typeof props.close === 'boolean') return props.close
232
+ const flag = upsertOpts?.dialog?.close
233
+ if (typeof flag === 'boolean') return flag
234
+ return true
235
+ })
236
+ const showFooter = computed(() => props.footer !== false)
237
+ const confirmLabel = computed(
238
+ () =>
239
+ props.confirmText ||
240
+ upsertOpts?.dialog?.confirmText ||
241
+ crud.dict.label.confirm,
242
+ )
243
+ const closeLabel = computed(
244
+ () =>
245
+ props.closeText ||
246
+ upsertOpts?.dialog?.closeText ||
247
+ crud.dict.label.close,
248
+ )
180
249
  const isBodyEmpty = computed(
181
250
  () => !visibleItems.value.length && !slots.default,
182
251
  )
183
252
 
184
- function onPointerDownOutside(event: Event) {
185
- if (isDialogFloatTarget(event)) event.preventDefault()
186
- }
187
-
188
- function onFocusOutside(event: Event) {
189
- if (isDialogFloatTarget(event)) event.preventDefault()
190
- }
191
-
192
- function onInteractOutside(event: Event) {
193
- if (isDialogFloatTarget(event)) event.preventDefault()
194
- }
253
+ const footerSlotProps = computed(() => ({
254
+ close: onCloseClick,
255
+ submit: onSubmit,
256
+ confirm: onConfirmClick,
257
+ submitting: submitting.value,
258
+ showConfirm: showConfirm.value,
259
+ showClose: showClose.value,
260
+ form: crud.upsertForm.value,
261
+ }))
195
262
 
196
263
  function setField(prop: string, value: unknown) {
197
264
  if (isInfo.value) return
@@ -263,9 +330,25 @@ function itemSpan(item: CrudFormItem) {
263
330
  return resolveFormSpan(item.span, formCols.value)
264
331
  }
265
332
 
333
+ function onCloseClick() {
334
+ const done = () => crud.upsertClose()
335
+ if (upsertOpts?.onClose) {
336
+ upsertOpts.onClose('close', done)
337
+ emit('close')
338
+ return
339
+ }
340
+ done()
341
+ emit('close')
342
+ }
343
+
344
+ function onConfirmClick() {
345
+ emit('confirm')
346
+ void onSubmit()
347
+ }
348
+
266
349
  async function onSubmit() {
267
350
  if (isInfo.value) {
268
- crud.upsertClose()
351
+ onCloseClick()
269
352
  return
270
353
  }
271
354
  if (submitting.value) return
@@ -345,7 +428,7 @@ defineExpose({
345
428
  form: crud.upsertForm,
346
429
  mode: crud.upsertMode,
347
430
  submit: onSubmit,
348
- close: () => crud.upsertClose(),
431
+ close: onCloseClick,
349
432
  add: () => crud.rowAdd(),
350
433
  append: (data?: Record<string, unknown>) => crud.rowAppend(data),
351
434
  edit: (data: Record<string, unknown>) => crud.rowEdit(data),
@@ -354,23 +437,6 @@ defineExpose({
354
437
  </script>
355
438
 
356
439
  <style lang="scss" scoped>
357
- .vm-crud-upsert__head {
358
- flex-shrink: 0;
359
- padding: 12px 22px;
360
- border-bottom: 1px solid var(--border);
361
-
362
- @media (max-width: 768px) {
363
- padding: 8px 12px;
364
- }
365
- }
366
-
367
- .vm-crud-upsert__title {
368
- font-size: 15px;
369
- font-weight: 650;
370
- letter-spacing: -0.01em;
371
- line-height: 1.3;
372
- }
373
-
374
440
  .vm-crud-upsert__body {
375
441
  display: grid;
376
442
  flex: 1 1 auto;
@@ -382,6 +448,19 @@ defineExpose({
382
448
  padding: 18px 22px;
383
449
  align-content: start;
384
450
 
451
+ &.is-fill {
452
+ display: flex;
453
+ flex-direction: column;
454
+ align-items: stretch;
455
+ gap: 0;
456
+ padding: 0;
457
+
458
+ > * {
459
+ width: 100%;
460
+ min-width: 0;
461
+ }
462
+ }
463
+
385
464
  /* slot 内容在宿主 SFC,须 :deep 才能命中 label / control 样式 */
386
465
  :deep(.vm-crud-upsert__field) {
387
466
  display: grid;
@@ -408,6 +487,25 @@ defineExpose({
408
487
  line-height: 1.4 !important;
409
488
  }
410
489
 
490
+ /* 单行 input:白底;select / tree-select 等:淡蓝底 */
491
+ :deep(input.vm-crud-upsert__control),
492
+ :deep(.vm-crud-upsert__control[data-slot='input']) {
493
+ background: var(--card) !important;
494
+ }
495
+
496
+ :deep(.vm-crud-select.vm-crud-upsert__control),
497
+ :deep(.vm-group-cascader.vm-crud-upsert__control),
498
+ :deep(.vm-multi-select.vm-crud-upsert__control),
499
+ :deep(.vm-user-select.vm-crud-upsert__control),
500
+ :deep(.vm-date-picker.vm-crud-upsert__control),
501
+ :deep(.vm-icon-picker.vm-crud-upsert__control) {
502
+ background: var(--muted) !important;
503
+ }
504
+
505
+ :deep(.vm-tree-select.vm-crud-upsert__control .vm-tree-select__trigger) {
506
+ background: var(--muted) !important;
507
+ }
508
+
411
509
  @media (max-width: 768px) {
412
510
  grid-template-columns: repeat(12, minmax(0, 1fr));
413
511
  flex: 1 1 auto;
@@ -443,11 +541,12 @@ defineExpose({
443
541
  min-height: var(--vm-control-height);
444
542
  }
445
543
 
446
- /* 上传:按 size 撑开,勿套用单行输入框高度 */
544
+ /* 上传:按 size 撑开,勿套用单行输入框高度;淡蓝底 */
447
545
  :deep(.vm-upload.vm-crud-upsert__upload-field) {
448
546
  width: 100%;
449
547
  height: auto !important;
450
548
  min-height: 0;
549
+ background: var(--muted) !important;
451
550
  }
452
551
 
453
552
  :deep(.vm-crud-upsert__switch-field.vm-switch) {
@@ -456,13 +555,14 @@ defineExpose({
456
555
  margin-top: 8px;
457
556
  }
458
557
 
459
- .vm-crud-upsert__textarea {
558
+ .vm-crud-upsert__textarea,
559
+ :deep(.vm-crud-upsert__textarea) {
460
560
  width: 100%;
461
561
  min-height: 72px;
462
562
  padding: 8px var(--vm-control-padding-x);
463
563
  border: 1px solid var(--input);
464
564
  border-radius: var(--vm-control-radius);
465
- background: var(--background);
565
+ background: var(--muted);
466
566
  color: var(--foreground);
467
567
  font-size: 14px;
468
568
  line-height: 1.5;
@@ -476,57 +576,4 @@ defineExpose({
476
576
  box-shadow: none;
477
577
  }
478
578
  }
479
-
480
- .vm-crud-upsert__foot {
481
- display: flex;
482
- flex-shrink: 0;
483
- justify-content: flex-end;
484
- gap: 8px;
485
- padding: 10px 22px;
486
- border-top: 1px solid var(--border);
487
-
488
- @media (max-width: 768px) {
489
- flex-direction: column-reverse;
490
- gap: 8px;
491
- padding: 8px 12px calc(8px + env(safe-area-inset-bottom, 0px));
492
-
493
- .vm-crud-upsert__btn {
494
- width: 100%;
495
- }
496
- }
497
- }
498
-
499
- .vm-crud-upsert__btn {
500
- height: 36px;
501
- padding: 0 16px;
502
- border: none;
503
- border-radius: 12px;
504
- font-size: 13px;
505
- font-weight: 600;
506
- cursor: pointer;
507
- transition: background 0.15s ease, color 0.15s ease;
508
-
509
- &.is-primary {
510
- background: var(--brand);
511
- color: #fff;
512
-
513
- &:hover:not(:disabled) {
514
- background: var(--brand-deep);
515
- }
516
- }
517
-
518
- &.is-ghost {
519
- background: var(--muted);
520
- color: var(--foreground);
521
-
522
- &:hover:not(:disabled) {
523
- background: color-mix(in srgb, var(--muted) 70%, var(--foreground) 8%);
524
- }
525
- }
526
-
527
- &:disabled {
528
- cursor: wait;
529
- opacity: 0.65;
530
- }
531
- }
532
579
  </style>