@xclqmc/base-form 1.0.0

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.
@@ -0,0 +1,366 @@
1
+ <template>
2
+ <div class="popup-dialog">
3
+ <el-dialog v-model="tempShow" :title="title" :draggable="draggable" :fullscreen="params.fullScreen"
4
+ :close-on-click-modal="false" :width="props.width" @close="cancel" :destroy-on-close="props.destroy"
5
+ :append-to-body="props.appendToBody" :top="top" :class="{ 'fullscreen-dialog': params.fullScreen }"
6
+ :style="{ height: props.height || undefined, borderRadius: !params.fullScreen ? props.radius : '0' }" class="form-popup-dialog-custom">
7
+ <!-- 标题 -->
8
+ <template #header>
9
+ <div class="title">
10
+ <slot name="title">
11
+ <div>
12
+ {{ title }}
13
+ </div>
14
+ </slot>
15
+ <div class="full" @click="openFull">
16
+ <el-icon>
17
+ <FullScreen />
18
+ </el-icon>
19
+ </div>
20
+ </div>
21
+ </template>
22
+
23
+ <!-- 内容 -->
24
+ <template #default>
25
+ <el-scrollbar :style="scrollbarStyle">
26
+ <div :style="{ padding: props.padding }">
27
+ <div v-loading="props.loading">
28
+ <div :style="{ opacity: props.loading ? 0 : 1 }">
29
+ <slot></slot>
30
+ </div>
31
+ </div>
32
+ </div>
33
+ </el-scrollbar>
34
+ </template>
35
+
36
+ <!-- 底部 -->
37
+ <template #footer>
38
+ <div class="foot">
39
+ <slot name="foot-left"></slot>
40
+ <el-button @click="cancel" v-if="showCancel" class="btn">取消</el-button>
41
+ <el-button type="primary" @click="submit" v-if="showSubmit" :disabled="disabledSubmit || loading"
42
+ class="btn">
43
+ 确认
44
+ </el-button>
45
+ <el-button type="primary" @click="cancel" v-if="showClose" class="btn">关闭</el-button>
46
+ <slot name="foot-right"></slot>
47
+ <slot name="footer"></slot>
48
+ </div>
49
+ </template>
50
+ </el-dialog>
51
+ </div>
52
+ </template>
53
+
54
+ <script lang="ts" setup>
55
+ import { ElButton } from 'element-plus';
56
+ import { FullScreen } from '@element-plus/icons-vue';
57
+ import { computed, reactive, ref, watch } from 'vue';
58
+
59
+ //==========================================================================================================数据
60
+ const emit = defineEmits(['cancel', 'submit', 'update:show', 'close']);
61
+ const props = defineProps({
62
+ // 显示状态
63
+ show: {
64
+ type: Boolean,
65
+ default: false,
66
+ },
67
+ // 宽度
68
+ width: {
69
+ type: String,
70
+ default: '1000px',
71
+ },
72
+ // 标题
73
+ title: {
74
+ type: String,
75
+ default: '标题',
76
+ },
77
+ // 显示状态
78
+ draggable: {
79
+ type: Boolean,
80
+ default: true,
81
+ },
82
+ // 显示取消
83
+ showCancel: {
84
+ type: Boolean,
85
+ default: true,
86
+ },
87
+ //显示确认
88
+ showSubmit: {
89
+ type: Boolean,
90
+ default: true,
91
+ },
92
+ //自动取消
93
+ autoCancel: {
94
+ type: Boolean,
95
+ default: true,
96
+ },
97
+ //最大高度
98
+ maxHeight: {
99
+ type: String,
100
+ default: '90vh',
101
+ },
102
+ //加载中
103
+ loading: {
104
+ type: Boolean,
105
+ default: false,
106
+ },
107
+ //高度
108
+ top: {
109
+ type: String,
110
+ default: '5vh',
111
+ },
112
+ // 弹窗高度:缺省 null(不限制,内容自适应);传入如 '70vh' / '500px' 则固定弹窗高度,内容区滚动
113
+ height: {
114
+ type: String,
115
+ default: null,
116
+ },
117
+ //关闭后销毁
118
+ destroy: {
119
+ type: Boolean,
120
+ default: true,
121
+ },
122
+ //内边距
123
+ padding: {
124
+ type: String,
125
+ default: '10px 15px 20px 15px',
126
+ },
127
+ //显示关闭按钮
128
+ showClose: {
129
+ type: Boolean,
130
+ default: false,
131
+ },
132
+ //是否警用提交按钮
133
+ disabledSubmit: {
134
+ type: Boolean,
135
+ default: false,
136
+ },
137
+ //是否内嵌
138
+ appendToBody: {
139
+ type: Boolean,
140
+ default: false,
141
+ },
142
+ //全屏
143
+ fullScreen: {
144
+ type: Boolean,
145
+ default: false,
146
+ },
147
+ //圆角
148
+ radius: {
149
+ type: String,
150
+ default: '5px',
151
+ },
152
+ });
153
+
154
+ const tempShow = ref<any>(false);
155
+ watch(
156
+ () => props.show,
157
+ () => {
158
+ tempShow.value = props.show;
159
+ },
160
+ {
161
+ immediate: true,
162
+ }
163
+ );
164
+
165
+ //参数
166
+ const params = reactive({
167
+ fullScreen: false,
168
+ });
169
+
170
+ //计算滚动区域样式
171
+ const scrollbarStyle = computed(() => {
172
+ // 固定高度模式(height 有值):内容区充满弹窗 body 并滚动
173
+ if (props.height) {
174
+ return {
175
+ 'max-height': '100%',
176
+ height: '100%',
177
+ 'overflow-y': 'auto',
178
+ };
179
+ }
180
+ const maxHeight = params.fullScreen ? '100vh' : props.maxHeight;
181
+ return {
182
+ 'max-height': `calc(${maxHeight} - 90px)`,
183
+ height: 'auto', // 改为auto让内容自适应
184
+ 'overflow-y': 'auto',
185
+ };
186
+ });
187
+
188
+ //==========================================================================================================方法
189
+ //取消
190
+ function cancel() {
191
+ emit('close');
192
+ if (props.autoCancel) {
193
+ emit('update:show', false);
194
+ tempShow.value = false;
195
+ }
196
+ }
197
+
198
+ //提交
199
+ function submit() {
200
+ emit('submit');
201
+ }
202
+
203
+ watch(
204
+ () => props.fullScreen,
205
+ () => {
206
+ params.fullScreen = props.fullScreen;
207
+ },
208
+ {
209
+ immediate: true,
210
+ }
211
+ );
212
+
213
+ //打开全屏
214
+ function openFull() {
215
+ params.fullScreen = !params.fullScreen;
216
+ }
217
+ </script>
218
+
219
+ <style lang="scss">
220
+ .form-popup-dialog-custom {
221
+ padding: 0;
222
+ position: relative;
223
+ overflow: hidden;
224
+ display: flex;
225
+ flex-direction: column;
226
+
227
+ .el-dialog__header {
228
+ width: 100%;
229
+ height: 40px;
230
+ line-height: 40px;
231
+ border-bottom: 1px solid rgba($color: #000000, $alpha: 0.1);
232
+ box-sizing: border-box;
233
+ padding: 0 20px;
234
+ font-size: 16px;
235
+ position: relative;
236
+ display: flex;
237
+ flex-shrink: 0;
238
+
239
+ .el-dialog__headerbtn {
240
+ height: 40px;
241
+ display: flex;
242
+ align-items: center;
243
+ justify-content: center;
244
+ }
245
+
246
+ .title {
247
+ width: 100%;
248
+ display: flex;
249
+ align-items: center;
250
+ justify-content: space-between;
251
+
252
+ .full {
253
+ height: 40px;
254
+ position: absolute;
255
+ top: 0%;
256
+ right: 30px;
257
+ font-size: var(--el-message-close-size, 16px);
258
+ color: gray;
259
+ cursor: pointer;
260
+ padding-right: 20px;
261
+ display: flex;
262
+ align-items: center;
263
+ justify-content: center;
264
+
265
+ &:hover {
266
+ color: #409eff;
267
+ }
268
+ }
269
+ }
270
+ }
271
+
272
+ .el-dialog__body {
273
+ padding: 0;
274
+ flex: 1;
275
+ overflow: hidden;
276
+ display: flex;
277
+ flex-direction: column;
278
+ }
279
+
280
+ &.is-fullscreen {
281
+ .el-dialog__body {
282
+ max-height: calc(100vh - 90px) !important;
283
+ }
284
+ }
285
+
286
+ .el-dialog__footer {
287
+ width: 100%;
288
+ padding: 0px 20px;
289
+ height: 40px;
290
+ display: flex;
291
+ align-items: center;
292
+ justify-content: flex-end;
293
+ text-align: center;
294
+ border-top: 1px solid rgba($color: #000000, $alpha: 0.1);
295
+ z-index: 99;
296
+ flex-shrink: 0;
297
+
298
+ .foot {
299
+ width: 100%;
300
+ height: 100%;
301
+ z-index: 99;
302
+ display: flex;
303
+ align-items: center;
304
+ justify-content: flex-end;
305
+ }
306
+
307
+ .el-button {
308
+ height: 28px;
309
+ padding: 0 20px;
310
+ }
311
+
312
+ .btn {
313
+ height: 28px;
314
+ padding: 0 20px;
315
+ }
316
+ }
317
+ }
318
+
319
+ .fullscreen-dialog {
320
+ .el-dialog__body {
321
+ max-height: calc(100vh - 90px) !important;
322
+ }
323
+ }
324
+
325
+ /* 全局覆盖 */
326
+ .dialog-fade-enter-active .el-dialog,
327
+ .dialog-fade-leave-active .el-dialog {
328
+ animation-fill-mode: forwards;
329
+ }
330
+
331
+ /* 进入动画:缩放+淡入 */
332
+ .dialog-fade-enter-active .el-dialog {
333
+ animation-duration: 0.2s;
334
+ animation-name: anim-open;
335
+ animation-timing-function: cubic-bezier(0.6, 0, 0.4, 1);
336
+ }
337
+
338
+ /* 离开动画:缩小+淡出 */
339
+ .dialog-fade-leave-active .el-dialog {
340
+ animation-duration: 0.25s;
341
+ animation-name: anim-close;
342
+ }
343
+
344
+ @keyframes anim-open {
345
+ 0% {
346
+ opacity: 0;
347
+ transform: scale3d(0, 0, 1);
348
+ }
349
+
350
+ 100% {
351
+ opacity: 1;
352
+ transform: scale3d(1, 1, 1);
353
+ }
354
+ }
355
+
356
+ @keyframes anim-close {
357
+ 0% {
358
+ opacity: 1;
359
+ }
360
+
361
+ 100% {
362
+ opacity: 0;
363
+ transform: scale3d(0.5, 0.5, 1);
364
+ }
365
+ }
366
+ </style>
@@ -0,0 +1,113 @@
1
+ <template>
2
+ <el-radio-group
3
+ v-model="data.valueCopy"
4
+ @change="change"
5
+ @click="(e: any) => emit('click', e)"
6
+ :disabled="disabled"
7
+ >
8
+ <el-radio
9
+ v-for="item in tempList"
10
+ :key="item.value"
11
+ :value="item[value]"
12
+ border
13
+ class="mr10"
14
+ >
15
+ {{ item[label] }}
16
+ </el-radio>
17
+ </el-radio-group>
18
+ </template>
19
+ <script setup lang="ts">
20
+ import { reactive, ref, watch, inject } from "vue";
21
+ const emit = defineEmits(["update:modelValue", "change", "click"]);
22
+
23
+ //==============================================================================数据
24
+ const props = defineProps({
25
+ //选择的数据
26
+ modelValue: {
27
+ type: [String, Number],
28
+ default: null,
29
+ },
30
+ //选项数据:数组直接用;字符串视为字典 code,经 BaseForm 注入的 api.dict 拉取
31
+ format: {
32
+ type: [Array, String],
33
+ default: () => {
34
+ return [];
35
+ },
36
+ },
37
+ //提示信息
38
+ placeholder: {
39
+ type: String,
40
+ default: "请选择",
41
+ },
42
+ //label
43
+ label: {
44
+ type: String,
45
+ default: "label",
46
+ },
47
+ //value
48
+ value: {
49
+ type: String,
50
+ default: "value",
51
+ },
52
+ //是否禁用
53
+ disabled: {
54
+ type: Boolean,
55
+ default: false,
56
+ },
57
+ });
58
+
59
+ const data = reactive<any>({
60
+ valueCopy: "",
61
+ });
62
+ //选项列表
63
+ const tempList = ref<any[]>([]);
64
+ //字典拉取函数(由 BaseForm provide;单独使用控件且无 BaseForm 时为 null,字符串 format 解析为空)
65
+ const dictFetch = inject<((code: string) => Promise<any[]>) | null>(
66
+ "baseFormDict",
67
+ null,
68
+ );
69
+
70
+ //==============================================================================监听
71
+ //选择数据
72
+ watch(
73
+ () => props.modelValue,
74
+ () => {
75
+ data.valueCopy = props.modelValue;
76
+ },
77
+ {
78
+ immediate: true,
79
+ },
80
+ );
81
+
82
+ //选项数据:数组直接用;字符串为字典 code,经注入的 api.dict 拉取(结果仅接受数组)
83
+ watch(
84
+ () => props.format,
85
+ async (fmt) => {
86
+ if (Array.isArray(fmt)) {
87
+ tempList.value = fmt;
88
+ return;
89
+ }
90
+ if (typeof fmt !== "string") {
91
+ tempList.value = [];
92
+ return;
93
+ }
94
+ try {
95
+ const res = await dictFetch?.(fmt);
96
+ if (props.format !== fmt) return; //拉取期间 format 已变,丢弃过期结果
97
+ tempList.value = Array.isArray(res) ? res : [];
98
+ } catch (err) {
99
+ console.error(`获取字典 ${fmt} 选项失败:`, err);
100
+ tempList.value = [];
101
+ }
102
+ },
103
+ {
104
+ immediate: true,
105
+ },
106
+ );
107
+
108
+ //==============================================================================方法
109
+ function change() {
110
+ emit("update:modelValue", data.valueCopy);
111
+ emit("change", data.valueCopy);
112
+ }
113
+ </script>
@@ -0,0 +1,136 @@
1
+ <template>
2
+ <el-select
3
+ v-model="data.valueCopy"
4
+ :placeholder="placeholder"
5
+ @change="change"
6
+ @click="(e: any) => emit('click', e)"
7
+ :disabled="disabled"
8
+ filterable
9
+ clearable
10
+ :multiple="multiple"
11
+ :max-collapse-tags="maxCollapseTags"
12
+ :collapse-tags="props.maxCollapseTags != null"
13
+ :collapse-tags-tooltip="props.maxCollapseTags != null"
14
+ >
15
+ <el-option
16
+ v-for="item in tempList"
17
+ :key="item[value]"
18
+ :label="item[label]"
19
+ :value="item[value]"
20
+ :disabled="item[disabledName]"
21
+ >
22
+ </el-option>
23
+ </el-select>
24
+ </template>
25
+
26
+ <script setup lang="ts">
27
+ import { reactive, ref, watch, inject } from "vue";
28
+
29
+ const emit = defineEmits(["update:modelValue", "change", "click"]);
30
+
31
+ //=====================================================================================数据
32
+ const props = defineProps({
33
+ //选择的数据
34
+ modelValue: {
35
+ type: [String, Array],
36
+ default: null,
37
+ },
38
+ //选项数据:数组直接用;字符串视为字典 code,经 BaseForm 注入的 api.dict 拉取
39
+ format: {
40
+ type: [Array, String],
41
+ default: () => [],
42
+ },
43
+ //是否禁用
44
+ disabled: {
45
+ type: Boolean,
46
+ default: false,
47
+ },
48
+ //是否多选
49
+ multiple: {
50
+ type: Boolean,
51
+ default: false,
52
+ },
53
+ //提示信息
54
+ placeholder: {
55
+ type: String,
56
+ default: "请选择",
57
+ },
58
+ //label
59
+ label: {
60
+ type: String,
61
+ default: "label",
62
+ },
63
+ //value
64
+ value: {
65
+ type: String,
66
+ default: "value",
67
+ },
68
+ //禁用名称
69
+ disabledName: {
70
+ type: String,
71
+ default: "disabled",
72
+ },
73
+ //标签折叠数量
74
+ maxCollapseTags: {
75
+ type: Number,
76
+ default: null,
77
+ },
78
+ });
79
+
80
+ const data = reactive<any>({
81
+ valueCopy: "",
82
+ });
83
+
84
+ //选项列表
85
+ const tempList = ref<any[]>([]);
86
+ //字典拉取函数(由 BaseForm provide;单独使用控件且无 BaseForm 时为 null,字符串 format 解析为空)
87
+ const dictFetch = inject<((code: string) => Promise<any[]>) | null>(
88
+ "baseFormDict",
89
+ null,
90
+ );
91
+
92
+ //==============================================================================监听
93
+ //选择数据
94
+ watch(
95
+ () => props.modelValue,
96
+ () => {
97
+ data.valueCopy = props.modelValue;
98
+ },
99
+ {
100
+ immediate: true,
101
+ },
102
+ );
103
+
104
+ //选项数据:数组直接用;字符串为字典 code,经注入的 api.dict 拉取(结果仅接受数组)
105
+ watch(
106
+ () => props.format,
107
+ async (fmt) => {
108
+ if (Array.isArray(fmt)) {
109
+ tempList.value = fmt;
110
+ return;
111
+ }
112
+ if (typeof fmt !== "string") {
113
+ tempList.value = [];
114
+ return;
115
+ }
116
+ try {
117
+ const res = await dictFetch?.(fmt);
118
+ if (props.format !== fmt) return; //拉取期间 format 已变,丢弃过期结果
119
+ tempList.value = Array.isArray(res) ? res : [];
120
+ } catch (err) {
121
+ console.error(`获取字典 ${fmt} 选项失败:`, err);
122
+ tempList.value = [];
123
+ }
124
+ },
125
+ {
126
+ immediate: true,
127
+ },
128
+ );
129
+
130
+ //=====================================================================================方法
131
+ function change() {
132
+ const val = props.multiple ? (data.valueCopy ?? []) : (data.valueCopy ?? "");
133
+ emit("update:modelValue", val);
134
+ emit("change", val);
135
+ }
136
+ </script>
@@ -0,0 +1,39 @@
1
+ /**
2
+ * 字段组件注册表:内置控件(input/radio/select/editor)+ 宿主按 type 注入的自定义组件。
3
+ * 解析优先级:宿主注入 > 内置(type 重复时以最后一次注入为准,覆盖内置渲染)。
4
+ */
5
+ import { shallowReactive } from "vue";
6
+ import type { Component } from "vue";
7
+ import BaseInput from "./BaseInput.vue";
8
+ import BaseRadio from "./BaseRadio.vue";
9
+ import BaseSelect from "./BaseSelect.vue";
10
+ import BaseEditor from "./BaseEditor.vue";
11
+
12
+ /** 内置字段控件 */
13
+ const builtinComponents: Record<string, Component> = {
14
+ input: BaseInput,
15
+ radio: BaseRadio,
16
+ select: BaseSelect,
17
+ editor: BaseEditor,
18
+ };
19
+
20
+ /** 宿主注入表(key 为字段 type) */
21
+ const fieldComponents = shallowReactive(new Map<string, Component>());
22
+
23
+ /** 注入单个字段组件:type 与字段配置的 field.type 对应;与内置/已注入重复时覆盖 */
24
+ export function registerField(type: string, component: Component) {
25
+ if (!type || !component) return;
26
+ fieldComponents.set(type, component);
27
+ }
28
+
29
+ /** 批量注入字段组件:形如 { rate: RateField, input: MyInput } */
30
+ export function registerFields(map: Record<string, Component> = {}) {
31
+ Object.entries(map).forEach(([type, component]) =>
32
+ registerField(type, component),
33
+ );
34
+ }
35
+
36
+ /** 按字段 type 解析渲染组件:注入优先,未注入回落内置;两者皆无返回 undefined */
37
+ export function resolveFieldComponent(type: string): Component | undefined {
38
+ return fieldComponents.get(type) ?? builtinComponents[type];
39
+ }