@longhongguo/form-create-ant-design-vue 3.2.45 → 3.2.46
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/form-create.css +21 -0
- package/dist/form-create.esm.css +21 -0
- package/dist/form-create.esm.js +2 -2
- package/dist/form-create.esm.js.map +1 -1
- package/dist/form-create.js +2 -2
- package/dist/form-create.js.map +1 -1
- package/package.json +1 -1
- package/src/components/CusSelect/index.vue +135 -32
- package/src/components/CusStoreSelect/index.vue +399 -91
- package/src/parsers/cusStoreSelect.js +111 -1
- package/src/style/index.css +21 -0
|
@@ -1,91 +1,399 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div>
|
|
3
|
-
<CusSelect
|
|
4
|
-
:model-value="modelValue"
|
|
5
|
-
:options="
|
|
6
|
-
:multiple="multiple"
|
|
7
|
-
:max-tag-count="maxTagCount"
|
|
8
|
-
:placeholder="placeholder"
|
|
9
|
-
:disabled="disabled"
|
|
10
|
-
:style="style"
|
|
11
|
-
:valueKey="valueKey"
|
|
12
|
-
:labelKey="labelKey"
|
|
13
|
-
:allowClear="allowClear"
|
|
14
|
-
|
|
15
|
-
@
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
</
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
import
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
1
|
+
<template>
|
|
2
|
+
<div @click="handleClick">
|
|
3
|
+
<CusSelect
|
|
4
|
+
:model-value="modelValue"
|
|
5
|
+
:options="mergedOptions"
|
|
6
|
+
:multiple="multiple"
|
|
7
|
+
:max-tag-count="maxTagCount"
|
|
8
|
+
:placeholder="placeholder"
|
|
9
|
+
:disabled="disabled"
|
|
10
|
+
:style="style"
|
|
11
|
+
:valueKey="valueKey"
|
|
12
|
+
:labelKey="labelKey"
|
|
13
|
+
:allowClear="allowClear"
|
|
14
|
+
:bordered="bordered"
|
|
15
|
+
@update:model-value="handleUpdate"
|
|
16
|
+
@change="handleChange"
|
|
17
|
+
/>
|
|
18
|
+
</div>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<script>
|
|
22
|
+
import { defineComponent } from 'vue'
|
|
23
|
+
import CusSelect from '../CusSelect/index.vue'
|
|
24
|
+
|
|
25
|
+
export default defineComponent({
|
|
26
|
+
name: 'CusStoreSelect',
|
|
27
|
+
components: {
|
|
28
|
+
CusSelect
|
|
29
|
+
},
|
|
30
|
+
props: {
|
|
31
|
+
// form-create 注入的对象,包含 API 等
|
|
32
|
+
formCreateInject: {
|
|
33
|
+
type: Object,
|
|
34
|
+
default: null
|
|
35
|
+
},
|
|
36
|
+
// 当前值:统一使用数组格式(支持 v-model)
|
|
37
|
+
// 单选时:[value] 或 []
|
|
38
|
+
// 多选时:[value1, value2, ...] 或 []
|
|
39
|
+
modelValue: {
|
|
40
|
+
type: Array,
|
|
41
|
+
default: () => []
|
|
42
|
+
},
|
|
43
|
+
// 选项列表
|
|
44
|
+
options: {
|
|
45
|
+
type: Array,
|
|
46
|
+
default: () => []
|
|
47
|
+
},
|
|
48
|
+
// 是否多选
|
|
49
|
+
multiple: {
|
|
50
|
+
type: Boolean,
|
|
51
|
+
default: false
|
|
52
|
+
},
|
|
53
|
+
// 最多显示的 tag 数量(多选模式下有效)
|
|
54
|
+
maxTagCount: {
|
|
55
|
+
type: Number,
|
|
56
|
+
default: undefined // 不限制时显示所有
|
|
57
|
+
},
|
|
58
|
+
// 占位符
|
|
59
|
+
placeholder: {
|
|
60
|
+
type: String,
|
|
61
|
+
default: '请选择'
|
|
62
|
+
},
|
|
63
|
+
// 是否禁用样式
|
|
64
|
+
disabled: {
|
|
65
|
+
type: Boolean,
|
|
66
|
+
default: false
|
|
67
|
+
},
|
|
68
|
+
// 自定义样式
|
|
69
|
+
style: {
|
|
70
|
+
type: [String, Object],
|
|
71
|
+
default: () => ({ width: '100%' })
|
|
72
|
+
},
|
|
73
|
+
// 选项的 value 字段名
|
|
74
|
+
valueKey: {
|
|
75
|
+
type: String,
|
|
76
|
+
default: 'value'
|
|
77
|
+
},
|
|
78
|
+
// 选项的 label 字段名
|
|
79
|
+
labelKey: {
|
|
80
|
+
type: String,
|
|
81
|
+
default: 'label'
|
|
82
|
+
},
|
|
83
|
+
// 是否允许清除
|
|
84
|
+
allowClear: {
|
|
85
|
+
type: Boolean,
|
|
86
|
+
default: false
|
|
87
|
+
},
|
|
88
|
+
// 字段名,用于跨窗口通信时标识字段
|
|
89
|
+
field: {
|
|
90
|
+
type: String,
|
|
91
|
+
default: ''
|
|
92
|
+
},
|
|
93
|
+
// 是否有边框
|
|
94
|
+
bordered: {
|
|
95
|
+
type: Boolean,
|
|
96
|
+
default: true
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
emits: ['update:modelValue', 'change'],
|
|
100
|
+
data() {
|
|
101
|
+
return {
|
|
102
|
+
// 消息ID计数器,用于标识每次请求
|
|
103
|
+
messageId: 0,
|
|
104
|
+
// 存储待处理的回调函数
|
|
105
|
+
pendingCallbacks: {},
|
|
106
|
+
// 内部维护的选项列表(合并父窗口返回的源对象)
|
|
107
|
+
internalOptions: []
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
computed: {
|
|
111
|
+
// 合并内部选项和外部传入的选项
|
|
112
|
+
mergedOptions() {
|
|
113
|
+
// 如果内部有选项,优先使用内部选项
|
|
114
|
+
if (this.internalOptions.length > 0) {
|
|
115
|
+
// 合并去重:根据 valueKey 去重,保留内部选项(后添加的优先)
|
|
116
|
+
const optionMap = new Map()
|
|
117
|
+
// 先添加外部选项
|
|
118
|
+
this.options.forEach((opt) => {
|
|
119
|
+
const value = typeof opt === 'object' ? opt[this.valueKey] : opt
|
|
120
|
+
optionMap.set(value, opt)
|
|
121
|
+
})
|
|
122
|
+
// 再添加内部选项(会覆盖相同 value 的选项)
|
|
123
|
+
this.internalOptions.forEach((opt) => {
|
|
124
|
+
const value = typeof opt === 'object' ? opt[this.valueKey] : opt
|
|
125
|
+
optionMap.set(value, opt)
|
|
126
|
+
})
|
|
127
|
+
return Array.from(optionMap.values())
|
|
128
|
+
}
|
|
129
|
+
return this.options
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
watch: {
|
|
133
|
+
// 监听外部 options 变化,初始化内部选项列表
|
|
134
|
+
options: {
|
|
135
|
+
immediate: true,
|
|
136
|
+
handler(newOptions) {
|
|
137
|
+
// 如果内部选项为空,且外部有选项,初始化内部选项
|
|
138
|
+
if (
|
|
139
|
+
this.internalOptions.length === 0 &&
|
|
140
|
+
Array.isArray(newOptions) &&
|
|
141
|
+
newOptions.length > 0
|
|
142
|
+
) {
|
|
143
|
+
this.internalOptions = [...newOptions]
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
mounted() {
|
|
149
|
+
// 监听父窗口返回的消息
|
|
150
|
+
window.addEventListener('message', this.handleMessage)
|
|
151
|
+
},
|
|
152
|
+
beforeUnmount() {
|
|
153
|
+
// 组件销毁时移除事件监听
|
|
154
|
+
window.removeEventListener('message', this.handleMessage)
|
|
155
|
+
},
|
|
156
|
+
methods: {
|
|
157
|
+
// 序列化数据,确保可以被 postMessage 发送
|
|
158
|
+
// postMessage 使用结构化克隆算法,无法克隆 Vue 响应式代理对象
|
|
159
|
+
serializeForPostMessage(data) {
|
|
160
|
+
// 处理 null 和 undefined
|
|
161
|
+
if (data === null || data === undefined) {
|
|
162
|
+
return data
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
// 使用 JSON 序列化和反序列化来创建可克隆的副本
|
|
167
|
+
// 这会移除 Vue 响应式代理、函数、Symbol 等不可序列化的内容
|
|
168
|
+
return JSON.parse(JSON.stringify(data))
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.warn('CusStoreSelect: 数据序列化失败,尝试递归处理', error)
|
|
171
|
+
|
|
172
|
+
// 如果 JSON 序列化失败(可能是循环引用),尝试递归处理
|
|
173
|
+
if (Array.isArray(data)) {
|
|
174
|
+
// 空数组直接返回
|
|
175
|
+
if (data.length === 0) {
|
|
176
|
+
return []
|
|
177
|
+
}
|
|
178
|
+
// 递归处理数组中的每个元素
|
|
179
|
+
return data.map((item) => this.serializeForPostMessage(item))
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (typeof data === 'object') {
|
|
183
|
+
const result = {}
|
|
184
|
+
for (const key in data) {
|
|
185
|
+
if (Object.prototype.hasOwnProperty.call(data, key)) {
|
|
186
|
+
try {
|
|
187
|
+
result[key] = this.serializeForPostMessage(data[key])
|
|
188
|
+
} catch (e) {
|
|
189
|
+
// 忽略无法序列化的属性,避免整个序列化失败
|
|
190
|
+
console.warn(`CusStoreSelect: 跳过无法序列化的属性: ${key}`, e)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return result
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 基本类型(string, number, boolean)直接返回
|
|
198
|
+
return data
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
handleClick() {
|
|
202
|
+
// 如果禁用,不处理
|
|
203
|
+
if (this.disabled) {
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 生成唯一消息ID
|
|
208
|
+
const msgId = `store-select-${
|
|
209
|
+
this.field || 'default'
|
|
210
|
+
}-${Date.now()}-${++this.messageId}`
|
|
211
|
+
|
|
212
|
+
// 序列化所有需要传递的数据,确保可以被 postMessage 发送
|
|
213
|
+
// postMessage 无法发送 Vue 响应式代理对象,必须序列化
|
|
214
|
+
// 获取当前值,确保是对象数组格式
|
|
215
|
+
const currentArrayValue = Array.isArray(this.modelValue)
|
|
216
|
+
? this.modelValue
|
|
217
|
+
: []
|
|
218
|
+
// 从对象数组中提取 value 值,用于发送给父窗口
|
|
219
|
+
// 单选时发送第一个对象的 value,多选时发送所有对象的 value 数组
|
|
220
|
+
let valueToSend = null
|
|
221
|
+
if (currentArrayValue.length > 0) {
|
|
222
|
+
if (this.multiple) {
|
|
223
|
+
// 多选:发送 value 数组
|
|
224
|
+
valueToSend = currentArrayValue.map((item) => {
|
|
225
|
+
if (typeof item === 'object' && item !== null) {
|
|
226
|
+
return item[this.valueKey] || item.value
|
|
227
|
+
}
|
|
228
|
+
return item
|
|
229
|
+
})
|
|
230
|
+
} else {
|
|
231
|
+
// 单选:发送第一个对象的 value
|
|
232
|
+
const firstItem = currentArrayValue[0]
|
|
233
|
+
if (typeof firstItem === 'object' && firstItem !== null) {
|
|
234
|
+
valueToSend = firstItem[this.valueKey] || firstItem.value
|
|
235
|
+
} else {
|
|
236
|
+
valueToSend = firstItem
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const serializedCurrentValue =
|
|
241
|
+
valueToSend === null || valueToSend === undefined
|
|
242
|
+
? null
|
|
243
|
+
: this.serializeForPostMessage(valueToSend)
|
|
244
|
+
|
|
245
|
+
// 发送消息给父窗口,请求打开门店选择弹窗
|
|
246
|
+
const message = {
|
|
247
|
+
type: 'OPEN_STORE_SELECT',
|
|
248
|
+
field: this.field || '',
|
|
249
|
+
multiple: this.multiple,
|
|
250
|
+
currentValue: serializedCurrentValue,
|
|
251
|
+
valueKey: this.valueKey,
|
|
252
|
+
labelKey: this.labelKey,
|
|
253
|
+
messageId: msgId
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 发送到父窗口(支持 iframe 场景)
|
|
257
|
+
if (window.parent && window.parent !== window) {
|
|
258
|
+
try {
|
|
259
|
+
window.parent.postMessage(message, '*')
|
|
260
|
+
} catch (error) {
|
|
261
|
+
console.error('CusStoreSelect: 发送消息失败', error)
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
// 如果不在 iframe 中,也可以发送到当前窗口(用于测试)
|
|
265
|
+
console.warn(
|
|
266
|
+
'CusStoreSelect: 当前不在 iframe 环境中,无法向父窗口发送消息'
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// 存储回调,等待父窗口返回结果
|
|
271
|
+
this.pendingCallbacks[msgId] = (value, sourceItems) => {
|
|
272
|
+
// 优先使用 sourceItems(源对象数组),如果没有则根据 value 和 options 构建
|
|
273
|
+
if (
|
|
274
|
+
sourceItems &&
|
|
275
|
+
Array.isArray(sourceItems) &&
|
|
276
|
+
sourceItems.length > 0
|
|
277
|
+
) {
|
|
278
|
+
// 合并到内部选项列表
|
|
279
|
+
this.mergeOptions(sourceItems)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
this.handleUpdate(value)
|
|
283
|
+
this.handleChange(value)
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
handleMessage(event) {
|
|
287
|
+
// 验证消息来源(可选,根据实际需求调整)
|
|
288
|
+
// if (event.origin !== 'expected-origin') return
|
|
289
|
+
|
|
290
|
+
const data = event.data
|
|
291
|
+
|
|
292
|
+
// 检查是否是门店选择返回的消息
|
|
293
|
+
if (data && data.type === 'STORE_SELECT_RESULT') {
|
|
294
|
+
const { field, value, sourceItems, messageId } = data
|
|
295
|
+
|
|
296
|
+
// 验证字段名是否匹配
|
|
297
|
+
if (field !== this.field) {
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 查找对应的回调函数
|
|
302
|
+
const callback = this.pendingCallbacks[messageId]
|
|
303
|
+
if (callback) {
|
|
304
|
+
// 执行回调,更新值并传递源对象
|
|
305
|
+
// sourceItems: 源对象数组,包含完整的选项信息
|
|
306
|
+
// 单选时:[{ value: '1001', label: '门店1', ... }]
|
|
307
|
+
// 多选时:[{ value: '1001', label: '门店1', ... }, { value: '1002', label: '门店2', ... }]
|
|
308
|
+
callback(value, sourceItems)
|
|
309
|
+
// 清理已处理的回调
|
|
310
|
+
delete this.pendingCallbacks[messageId]
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
// 合并选项到内部选项列表
|
|
315
|
+
mergeOptions(newItems) {
|
|
316
|
+
if (!Array.isArray(newItems) || newItems.length === 0) {
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// 创建选项映射,用于去重
|
|
321
|
+
const optionMap = new Map()
|
|
322
|
+
|
|
323
|
+
// 先将现有内部选项添加到映射
|
|
324
|
+
this.internalOptions.forEach((opt) => {
|
|
325
|
+
const value = typeof opt === 'object' ? opt[this.valueKey] : opt
|
|
326
|
+
optionMap.set(value, opt)
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
// 再添加新选项(会覆盖相同 value 的选项)
|
|
330
|
+
newItems.forEach((opt) => {
|
|
331
|
+
const value = typeof opt === 'object' ? opt[this.valueKey] : opt
|
|
332
|
+
optionMap.set(value, opt)
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
// 更新内部选项列表
|
|
336
|
+
this.internalOptions = Array.from(optionMap.values())
|
|
337
|
+
},
|
|
338
|
+
handleUpdate(value) {
|
|
339
|
+
this.$emit('update:modelValue', value)
|
|
340
|
+
// 值更新后触发校验
|
|
341
|
+
this.triggerValidate()
|
|
342
|
+
},
|
|
343
|
+
handleChange(value) {
|
|
344
|
+
this.$emit(
|
|
345
|
+
'change',
|
|
346
|
+
value,
|
|
347
|
+
this.internalOptions && this.internalOptions.length > 0
|
|
348
|
+
? this.multiple
|
|
349
|
+
? this.internalOptions
|
|
350
|
+
: this.internalOptions[0]
|
|
351
|
+
: this.multiple
|
|
352
|
+
? []
|
|
353
|
+
: null
|
|
354
|
+
)
|
|
355
|
+
},
|
|
356
|
+
// 触发字段校验
|
|
357
|
+
triggerValidate() {
|
|
358
|
+
// 使用 nextTick 确保值已经更新完成
|
|
359
|
+
this.$nextTick(() => {
|
|
360
|
+
this.$nextTick(() => {
|
|
361
|
+
try {
|
|
362
|
+
// 方式1:通过 formCreateInject 中的 API 触发校验
|
|
363
|
+
if (
|
|
364
|
+
this.formCreateInject &&
|
|
365
|
+
this.formCreateInject.api &&
|
|
366
|
+
this.field
|
|
367
|
+
) {
|
|
368
|
+
this.formCreateInject.api.validateField(this.field).catch(() => {
|
|
369
|
+
// 校验失败时静默处理,错误会通过 form-item 显示
|
|
370
|
+
})
|
|
371
|
+
return
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// 方式2:通过组件实例查找父组件中的 form-create API
|
|
375
|
+
let parent = this.$parent
|
|
376
|
+
while (parent) {
|
|
377
|
+
if (
|
|
378
|
+
parent.$options &&
|
|
379
|
+
parent.$options.name === 'FormCreate' &&
|
|
380
|
+
parent.fapi
|
|
381
|
+
) {
|
|
382
|
+
if (this.field && parent.fapi.validateField) {
|
|
383
|
+
parent.fapi.validateField(this.field).catch(() => {
|
|
384
|
+
// 校验失败时静默处理
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
break
|
|
388
|
+
}
|
|
389
|
+
parent = parent.$parent
|
|
390
|
+
}
|
|
391
|
+
} catch (error) {
|
|
392
|
+
console.warn('CusStoreSelect: 触发校验失败', error)
|
|
393
|
+
}
|
|
394
|
+
})
|
|
395
|
+
})
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
})
|
|
399
|
+
</script>
|
|
@@ -1,17 +1,127 @@
|
|
|
1
1
|
import { hasProperty } from '@form-create/utils/lib/type'
|
|
2
|
+
import { nextTick } from 'vue'
|
|
2
3
|
|
|
3
4
|
export default {
|
|
4
5
|
name: 'cusStoreSelect',
|
|
5
6
|
modelField: 'modelValue',
|
|
7
|
+
// 将 form-create 内部的值转换为对象数组格式
|
|
8
|
+
toFormValue(value, ctx) {
|
|
9
|
+
// 组件内部统一使用对象数组格式 [{value, label, ...}]
|
|
10
|
+
if (value === null || value === undefined || value === '') {
|
|
11
|
+
return []
|
|
12
|
+
}
|
|
13
|
+
if (Array.isArray(value)) {
|
|
14
|
+
// 确保数组中的每个元素都是对象格式
|
|
15
|
+
return value.map((item) => {
|
|
16
|
+
if (
|
|
17
|
+
typeof item === 'object' &&
|
|
18
|
+
item !== null &&
|
|
19
|
+
(item.value !== undefined || item.label !== undefined)
|
|
20
|
+
) {
|
|
21
|
+
// 已经是对象格式
|
|
22
|
+
return item
|
|
23
|
+
}
|
|
24
|
+
// 如果是单个值,需要转换为对象格式(但此时没有 label,会在组件内部处理)
|
|
25
|
+
return item
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
// 单个值转换为数组(组件内部会处理对象转换)
|
|
29
|
+
return [value]
|
|
30
|
+
},
|
|
31
|
+
// 将组件返回的数组格式转换为 form-create 内部格式(可选,保持原值)
|
|
32
|
+
toValue(formValue, ctx) {
|
|
33
|
+
// 保持数组格式,不转换
|
|
34
|
+
return formValue
|
|
35
|
+
},
|
|
6
36
|
mergeProp(ctx) {
|
|
7
37
|
const props = ctx.prop.props
|
|
8
38
|
// 确保 options 存在
|
|
9
39
|
if (!hasProperty(props, 'options')) {
|
|
10
40
|
props.options = ctx.prop.options || []
|
|
11
41
|
}
|
|
42
|
+
// 传递字段名到组件,用于跨窗口通信时标识字段
|
|
43
|
+
if (!hasProperty(props, 'field')) {
|
|
44
|
+
props.field = ctx.rule.field || ''
|
|
45
|
+
}
|
|
46
|
+
// 确保初始值转换为数组格式
|
|
47
|
+
if (ctx.rule.value !== undefined) {
|
|
48
|
+
const currentValue = ctx.rule.value
|
|
49
|
+
if (
|
|
50
|
+
currentValue === null ||
|
|
51
|
+
currentValue === undefined ||
|
|
52
|
+
currentValue === ''
|
|
53
|
+
) {
|
|
54
|
+
ctx.rule.value = []
|
|
55
|
+
} else if (!Array.isArray(currentValue)) {
|
|
56
|
+
ctx.rule.value = [currentValue]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
12
59
|
},
|
|
13
60
|
render(children, ctx) {
|
|
14
61
|
// 使用默认渲染
|
|
15
|
-
|
|
62
|
+
const vnode = ctx.$render.defaultRender(ctx, children)
|
|
63
|
+
|
|
64
|
+
// 添加校验触发逻辑:在值变化时触发校验
|
|
65
|
+
if (vnode && vnode.props && vnode.props.on) {
|
|
66
|
+
// 保存原始的 update:modelValue 事件处理器(form-create 自动添加的)
|
|
67
|
+
const originalUpdateModelValue = vnode.props.on['update:modelValue']
|
|
68
|
+
|
|
69
|
+
// 创建校验触发函数
|
|
70
|
+
const triggerValidate = () => {
|
|
71
|
+
// 延迟触发校验,确保值已经更新完成
|
|
72
|
+
// 使用多个 nextTick 确保 form-create 内部的值更新流程完成
|
|
73
|
+
nextTick(() => {
|
|
74
|
+
nextTick(() => {
|
|
75
|
+
try {
|
|
76
|
+
// 方式1:通过 field 名称触发校验(推荐)
|
|
77
|
+
const fieldName = ctx.rule.field
|
|
78
|
+
if (fieldName) {
|
|
79
|
+
ctx.$handle.api.validateField(fieldName).catch(() => {
|
|
80
|
+
// 校验失败时静默处理,错误会通过 form-item 显示
|
|
81
|
+
})
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 方式2:通过字段上下文ID触发校验(备用)
|
|
86
|
+
const fieldCtx = ctx.$handle.getFieldCtx(ctx.rule.field)
|
|
87
|
+
if (fieldCtx && fieldCtx.id) {
|
|
88
|
+
ctx.$handle.$manager.validateField(fieldCtx.id).catch(() => {
|
|
89
|
+
// 校验失败时静默处理,错误会通过 form-item 显示
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
// 静默处理错误,避免影响正常流程
|
|
94
|
+
console.warn('CusStoreSelect: 触发校验失败', error)
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 包装 update:modelValue 事件,在值更新后触发校验
|
|
101
|
+
vnode.props.on['update:modelValue'] = (...args) => {
|
|
102
|
+
// 先调用原始的事件处理器(form-create 的 onInput,会更新值和触发其他逻辑)
|
|
103
|
+
if (originalUpdateModelValue) {
|
|
104
|
+
originalUpdateModelValue(...args)
|
|
105
|
+
}
|
|
106
|
+
// 触发校验
|
|
107
|
+
triggerValidate()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 同时监听 change 事件,确保所有情况下都能触发校验
|
|
111
|
+
const originalChange = vnode.props.on['change']
|
|
112
|
+
if (originalChange) {
|
|
113
|
+
vnode.props.on['change'] = (...args) => {
|
|
114
|
+
// 先调用原始的 change 处理器
|
|
115
|
+
originalChange(...args)
|
|
116
|
+
// 触发校验
|
|
117
|
+
triggerValidate()
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
// 如果没有原始的 change 处理器,直接添加
|
|
121
|
+
vnode.props.on['change'] = triggerValidate
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return vnode
|
|
16
126
|
}
|
|
17
127
|
}
|
package/src/style/index.css
CHANGED
|
@@ -67,6 +67,27 @@
|
|
|
67
67
|
box-sizing: border-box;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/* 无边框模式 */
|
|
71
|
+
.fc-cus-select-selector-borderless {
|
|
72
|
+
border: none !important;
|
|
73
|
+
box-shadow: none !important;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.fc-cus-select:not(.fc-cus-select-disabled):hover
|
|
77
|
+
.fc-cus-select-selector-borderless {
|
|
78
|
+
border: none !important;
|
|
79
|
+
box-shadow: none !important;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.fc-cus-select:focus:not(.fc-cus-select-disabled)
|
|
83
|
+
.fc-cus-select-selector-borderless,
|
|
84
|
+
.fc-cus-select:focus-within:not(.fc-cus-select-disabled)
|
|
85
|
+
.fc-cus-select-selector-borderless {
|
|
86
|
+
border: none !important;
|
|
87
|
+
box-shadow: none !important;
|
|
88
|
+
outline: none !important;
|
|
89
|
+
}
|
|
90
|
+
|
|
70
91
|
.fc-cus-select-single .fc-cus-select-selector {
|
|
71
92
|
height: 32px;
|
|
72
93
|
padding: 0 11px;
|