@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.
@@ -1,91 +1,399 @@
1
- <template>
2
- <div>
3
- <CusSelect
4
- :model-value="modelValue"
5
- :options="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
- @update:model-value="handleUpdate"
15
- @change="handleChange"
16
- />
17
- </div>
18
- </template>
19
-
20
- <script>
21
- import { defineComponent } from 'vue'
22
- import CusSelect from '../CusSelect/index.vue'
23
-
24
- export default defineComponent({
25
- name: 'CusStoreSelect',
26
- components: {
27
- CusSelect
28
- },
29
- props: {
30
- // 当前值:单个时为字符串/数字,多个时为数组(支持 v-model)
31
- modelValue: {
32
- type: [String, Number, Array],
33
- default: null
34
- },
35
- // 选项列表
36
- options: {
37
- type: Array,
38
- default: () => []
39
- },
40
- // 是否多选
41
- multiple: {
42
- type: Boolean,
43
- default: false
44
- },
45
- // 最多显示的 tag 数量(多选模式下有效)
46
- maxTagCount: {
47
- type: Number,
48
- default: undefined // 不限制时显示所有
49
- },
50
- // 占位符
51
- placeholder: {
52
- type: String,
53
- default: '请选择'
54
- },
55
- // 是否禁用样式
56
- disabled: {
57
- type: Boolean,
58
- default: false
59
- },
60
- // 自定义样式
61
- style: {
62
- type: [String, Object],
63
- default: () => ({ width: '100%' })
64
- },
65
- // 选项的 value 字段名
66
- valueKey: {
67
- type: String,
68
- default: 'value'
69
- },
70
- // 选项的 label 字段名
71
- labelKey: {
72
- type: String,
73
- default: 'label'
74
- },
75
- // 是否允许清除
76
- allowClear: {
77
- type: Boolean,
78
- default: false
79
- }
80
- },
81
- emits: ['update:modelValue', 'change'],
82
- methods: {
83
- handleUpdate(value) {
84
- this.$emit('update:modelValue', value)
85
- },
86
- handleChange(value) {
87
- this.$emit('change', value)
88
- }
89
- }
90
- })
91
- </script>
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
- return ctx.$render.defaultRender(ctx, children)
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
  }
@@ -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;