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.
- package/dist/admin/config/plugin-dev.js +1 -1
- package/dist/admin/crud/components/vm-audio.vue +217 -0
- package/dist/admin/crud/components/vm-column-custom.vue +50 -56
- package/dist/admin/crud/components/vm-file-preview.vue +90 -0
- package/dist/admin/crud/components/vm-image.vue +413 -0
- package/dist/admin/crud/components/vm-preview-viewer.vue +520 -0
- package/dist/admin/crud/components/vm-richtext.vue +275 -0
- package/dist/admin/crud/components/vm-upload-item.vue +92 -31
- package/dist/admin/crud/components/vm-upload.vue +2 -6
- package/dist/admin/crud/components/vm-user-select.vue +48 -49
- package/dist/admin/crud/config.js +1 -1
- package/dist/admin/crud/confirm.js +1 -1
- package/dist/admin/crud/dict.js +1 -1
- package/dist/admin/crud/index.d.ts +6 -3
- package/dist/admin/crud/index.js +1 -1
- package/dist/admin/crud/key.js +1 -1
- package/dist/admin/crud/mitt.js +1 -1
- package/dist/admin/crud/plugins.js +1 -1
- package/dist/admin/crud/span.js +1 -1
- package/dist/admin/crud/style.js +1 -1
- package/dist/admin/crud/validate.js +1 -1
- package/dist/admin/crud/vm-adv-search.vue +39 -32
- package/dist/admin/crud/vm-crud.vue +3 -8
- package/dist/admin/crud/vm-export-btn.vue +169 -0
- package/dist/admin/crud/{vm-flex1.vue → vm-flex.vue} +3 -3
- package/dist/admin/crud/vm-search.vue +18 -3
- package/dist/admin/crud/vm-toolbar.vue +20 -0
- package/dist/admin/crud/vm-upsert.vue +166 -119
- package/dist/admin/directives/perm.js +1 -1
- package/dist/admin/hooks/useUpload.js +1 -1
- package/dist/admin/lib/browser.js +1 -1
- package/dist/admin/lib/cn.js +1 -1
- package/dist/admin/lib/dialog-float.d.ts +2 -0
- package/dist/admin/lib/dialog-float.js +1 -1
- package/dist/admin/lib/export-excel.d.ts +11 -0
- package/dist/admin/lib/export-excel.js +1 -0
- package/dist/admin/lib/file-preview.d.ts +11 -0
- package/dist/admin/lib/file-preview.js +1 -0
- package/dist/admin/lib/menu.js +1 -1
- package/dist/admin/lib/tree.js +1 -1
- package/dist/admin/lib/upload.js +1 -1
- package/dist/admin/lib/video-frame.d.ts +9 -0
- package/dist/admin/lib/video-frame.js +1 -0
- package/dist/admin/styles/base.css +100 -0
- package/dist/index.js +1 -1
- package/dist/server/index.js +1 -1
- package/dist/shared/excel.d.ts +13 -0
- package/dist/shared/excel.js +1 -0
- package/dist/shared/index.js +1 -1
- package/dist/shared/tree.js +1 -1
- package/dist/src/core/context/index.d.ts +11 -1
- package/dist/src/routing/base.d.ts +2 -0
- package/dist/src/server/index.d.ts +2 -0
- package/dist/src/shared/excel.d.ts +13 -0
- package/dist/src/shared/index.d.ts +2 -0
- package/dist/typings/routing/crud.d.ts +2 -0
- package/package.json +3 -2
- package/src/shared/excel.d.ts +13 -0
- package/src/shared/excel.ts +46 -0
- package/src/shared/index.ts +6 -0
- package/typings/admin/comm/crud.d.ts +8 -2
- package/typings/admin/comm/crud.ts +8 -3
- package/dist/admin/crud/components/vm-file-icon.vue +0 -55
- package/dist/admin/crud/vm-dialog.vue +0 -235
- package/dist/admin/static/fileicon/demo.css +0 -539
- package/dist/admin/static/fileicon/demo_index.html +0 -901
- package/dist/admin/static/fileicon/iconfont.css +0 -139
- package/dist/admin/static/fileicon/iconfont.js +0 -1
- package/dist/admin/static/fileicon/iconfont.json +0 -226
- package/dist/admin/static/fileicon/iconfont.ttf +0 -0
- package/dist/admin/static/fileicon/iconfont.woff +0 -0
- 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-
|
|
2
|
+
<div class="vm-crud-flex" />
|
|
3
3
|
</template>
|
|
4
4
|
|
|
5
5
|
<script setup lang="ts">
|
|
6
|
-
defineOptions({ name: 'vm-
|
|
6
|
+
defineOptions({ name: 'vm-flex' })
|
|
7
7
|
</script>
|
|
8
8
|
|
|
9
9
|
<style lang="scss" scoped>
|
|
10
|
-
.vm-crud-
|
|
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:
|
|
195
|
-
|
|
196
|
-
|
|
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 ?
|
|
4
|
+
@update:open="(v) => (!v ? onCloseClick() : undefined)"
|
|
5
5
|
>
|
|
6
6
|
<DialogContent
|
|
7
7
|
shell="crud"
|
|
8
8
|
class="vm-crud-upsert"
|
|
9
|
-
:class="{
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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-
|
|
15
|
-
<DialogTitle class="vm-crud-
|
|
18
|
+
<DialogHeader class="vm-crud-shell__head">
|
|
19
|
+
<DialogTitle class="vm-crud-shell__title">{{ titleText }}</DialogTitle>
|
|
16
20
|
</DialogHeader>
|
|
17
|
-
<div
|
|
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-
|
|
93
|
-
<
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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:
|
|
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(--
|
|
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>
|