Skip to content

Commit 8f49c23

Browse files
authored
Merge branch 'main' into next
2 parents f227b44 + 0936861 commit 8f49c23

File tree

18 files changed

+251
-42
lines changed

18 files changed

+251
-42
lines changed

docs/src/components/common-ui/vben-alert.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,11 @@ Alert提供的快捷方法alert、confirm、prompt动态创建的弹窗在已打
4242

4343
当弹窗的content、footer、icon使用自定义组件时,在这些组件中可以使用 `useAlertContext` 获取当前弹窗的上下文对象,用来主动控制弹窗。
4444

45-
::: tip 注意 `useAlertContext`只能用在setup或者函数式组件中。:::
45+
::: tip 注意
46+
47+
`useAlertContext`只能用在setup或者函数式组件中。
48+
49+
:::
4650

4751
### Methods
4852

docs/src/components/common-ui/vben-api-component.md

+8-6
Original file line numberDiff line numberDiff line change
@@ -151,21 +151,23 @@ function fetchApi(): Promise<Record<string, any>> {
151151
| options | 直接传入选项数据,也作为api返回空数据时的后备数据 | `OptionsItem[]` | - | - |
152152
| visibleEvent | 触发重新请求数据的事件名 | `string` | - | - |
153153
| loadingSlot | 目标组件的插槽名称,用来显示一个"加载中"的图标 | `string` | - | - |
154-
| autoSelect | 自动设置选项 | `'first' \| 'last' \| 'one'\| (item: OptionsItem[]) => OptionsItem \| false` | `false` | >5.5.4 |
154+
| autoSelect | 自动设置选项 | `'first' \| 'last' \| 'one'\| ((item: OptionsItem[]) => OptionsItem) \| false` | `false` | >5.5.4 |
155155

156156
#### autoSelect 自动设置选项
157157

158158
如果当前值为undefined,在选项数据成功加载之后,自动从备选项中选择一个作为当前值。默认值为`false`,即不自动选择选项。注意:该属性不应用于多选组件。可选值有:
159159

160-
- `first`:自动选择第一个选项
161-
- `last`:自动选择最后一个选项
162-
- `one`:有且仅有一个选项时,自动选择它
163-
- `函数`:自定义选择逻辑,函数的参数为options,返回值为选择的选项
164-
- false:不自动选择选项
160+
- `"first"`:自动选择第一个选项
161+
- `"last"`:自动选择最后一个选项
162+
- `"one"`:有且仅有一个选项时,自动选择它
163+
- `自定义函数`:自定义选择逻辑,函数的参数为options,返回值为选择的选项
164+
- `false`:不自动选择选项
165165

166166
### Methods
167167

168168
| 方法 | 描述 | 类型 | 版本要求 |
169169
| --- | --- | --- | --- |
170170
| getComponentRef | 获取被包装的组件的实例 | ()=>T | >5.5.4 |
171171
| updateParam | 设置接口请求参数(将与params属性合并) | (newParams: Record<string, any>)=>void | >5.5.4 |
172+
| getOptions | 获取已加载的选项数据 | ()=>OptionsItem[] | >5.5.4 |
173+
| getValue | 获取当前值 | ()=>any | >5.5.4 |

docs/src/components/common-ui/vben-drawer.md

+8-7
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,14 @@ const [Drawer, drawerApi] = useVbenDrawer({
127127

128128
除了上面的属性类型包含`slot`,还可以通过插槽来自定义弹窗的内容。
129129

130-
| 插槽名 | 描述 |
131-
| -------------- | ------------------- |
132-
| default | 默认插槽 - 弹窗内容 |
133-
| prepend-footer | 取消按钮左侧 |
134-
| append-footer | 取消按钮右侧 |
135-
| close-icon | 关闭按钮图标 |
136-
| extra | 额外内容(标题右侧) |
130+
| 插槽名 | 描述 |
131+
| -------------- | -------------------------------------------------- |
132+
| default | 默认插槽 - 弹窗内容 |
133+
| prepend-footer | 取消按钮左侧 |
134+
| center-footer | 取消按钮和确认按钮中间(不使用 footer 插槽时有效) |
135+
| append-footer | 确认按钮右侧 |
136+
| close-icon | 关闭按钮图标 |
137+
| extra | 额外内容(标题右侧) |
137138

138139
### drawerApi
139140

docs/src/components/common-ui/vben-form.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
310310
| actionWrapperClass | 表单操作区域class | `any` | - |
311311
| handleReset | 表单重置回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
312312
| handleSubmit | 表单提交回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
313-
| handleValuesChange | 表单值变化回调 | `(values: Record<string, any>,) => void` | - |
313+
| handleValuesChange | 表单值变化回调 | `(values: Record<string, any>, fieldsChanged: string[]) => void` | - |
314314
| actionButtonsReverse | 调换操作按钮位置 | `boolean` | `false` |
315315
| resetButtonOptions | 重置按钮组件参数 | `ActionButtonOptions` | - |
316316
| submitButtonOptions | 提交按钮组件参数 | `ActionButtonOptions` | - |
@@ -325,6 +325,12 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
325325
| submitOnChange | 字段值改变时提交表单(内部防抖,这个属性一般用于表格的搜索表单) | `boolean` | false |
326326
| compact | 是否紧凑模式(忽略为校验信息所预留的空间) | `boolean` | false |
327327

328+
::: tip handleValuesChange
329+
330+
`handleValuesChange` 回调函数的第一个参数`values`装载了表单改变后的当前值对象,第二个参数`fieldsChanged`是一个数组,包含了所有被改变的字段名。注意:第二个参数仅在v5.5.4(不含)以上版本可用。
331+
332+
:::
333+
328334
::: tip fieldMappingTime
329335

330336
此属性用于将表单内的数组值映射成 2 个字段,它应当传入一个数组,数组的每一项是一个映射规则,规则的第一个成员是一个字符串,表示需要映射的字段名,第二个成员是一个数组,表示映射后的字段名,第三个成员是一个可选的格式掩码,用于格式化日期时间字段;也可以提供一个格式化函数(参数分别为当前值和当前字段名,返回格式化后的值)。如果明确地将格式掩码设为null,则原值映射而不进行格式化(适用于非日期时间字段)。例如:`[['timeRange', ['startTime', 'endTime'], 'YYYY-MM-DD']]``timeRange`应当是一个至少具有2个成员的数组类型的值。Form会将`timeRange`的值前两个值分别按照格式掩码`YYYY-MM-DD`格式化后映射到`startTime``endTime`字段上。每一项的第三个参数是一个可选的格式掩码,

docs/src/components/common-ui/vben-modal.md

+6-5
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,12 @@ const [Modal, modalApi] = useVbenModal({
137137

138138
除了上面的属性类型包含`slot`,还可以通过插槽来自定义弹窗的内容。
139139

140-
| 插槽名 | 描述 |
141-
| -------------- | ------------------- |
142-
| default | 默认插槽 - 弹窗内容 |
143-
| prepend-footer | 取消按钮左侧 |
144-
| append-footer | 取消按钮右侧 |
140+
| 插槽名 | 描述 |
141+
| -------------- | -------------------------------------------------- |
142+
| default | 默认插槽 - 弹窗内容 |
143+
| prepend-footer | 取消按钮左侧 |
144+
| center-footer | 取消按钮和确认按钮中间(不使用 footer 插槽时有效) |
145+
| append-footer | 确认按钮右侧 |
145146

146147
### modalApi
147148

packages/@core/ui-kit/form-ui/src/form-api.ts

+119
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,7 @@ export class FormApi {
295295
return true;
296296
});
297297
const filteredFields = fieldMergeFn(fields, form.values);
298+
this.handleStringToArrayFields(filteredFields);
298299
form.setValues(filteredFields, shouldValidate);
299300
}
300301

@@ -304,6 +305,7 @@ export class FormApi {
304305
const form = await this.getForm();
305306
await form.submitForm();
306307
const rawValues = toRaw(await this.getValues());
308+
this.handleArrayToStringFields(rawValues);
307309
await this.state?.handleSubmit?.(rawValues);
308310

309311
return rawValues;
@@ -392,10 +394,53 @@ export class FormApi {
392394
return this.form;
393395
}
394396

397+
private handleArrayToStringFields = (originValues: Record<string, any>) => {
398+
const arrayToStringFields = this.state?.arrayToStringFields;
399+
if (!arrayToStringFields || !Array.isArray(arrayToStringFields)) {
400+
return;
401+
}
402+
403+
const processFields = (fields: string[], separator: string = ',') => {
404+
this.processFields(fields, separator, originValues, (value, sep) =>
405+
Array.isArray(value) ? value.join(sep) : value,
406+
);
407+
};
408+
409+
// 处理简单数组格式 ['field1', 'field2', ';'] 或 ['field1', 'field2']
410+
if (arrayToStringFields.every((item) => typeof item === 'string')) {
411+
const lastItem =
412+
arrayToStringFields[arrayToStringFields.length - 1] || '';
413+
const fields =
414+
lastItem.length === 1
415+
? arrayToStringFields.slice(0, -1)
416+
: arrayToStringFields;
417+
const separator = lastItem.length === 1 ? lastItem : ',';
418+
processFields(fields, separator);
419+
return;
420+
}
421+
422+
// 处理嵌套数组格式 [['field1'], ';']
423+
arrayToStringFields.forEach((fieldConfig) => {
424+
if (Array.isArray(fieldConfig)) {
425+
const [fields, separator = ','] = fieldConfig;
426+
// 根据类型定义,fields 应该始终是字符串数组
427+
if (!Array.isArray(fields)) {
428+
console.warn(
429+
`Invalid field configuration: fields should be an array of strings, got ${typeof fields}`,
430+
);
431+
return;
432+
}
433+
processFields(fields, separator);
434+
}
435+
});
436+
};
437+
395438
private handleRangeTimeValue = (originValues: Record<string, any>) => {
396439
const values = { ...originValues };
397440
const fieldMappingTime = this.state?.fieldMappingTime;
398441

442+
this.handleStringToArrayFields(values);
443+
399444
if (!fieldMappingTime || !Array.isArray(fieldMappingTime)) {
400445
return values;
401446
}
@@ -441,6 +486,80 @@ export class FormApi {
441486
return values;
442487
};
443488

489+
private handleStringToArrayFields = (originValues: Record<string, any>) => {
490+
const arrayToStringFields = this.state?.arrayToStringFields;
491+
if (!arrayToStringFields || !Array.isArray(arrayToStringFields)) {
492+
return;
493+
}
494+
495+
const processFields = (fields: string[], separator: string = ',') => {
496+
this.processFields(fields, separator, originValues, (value, sep) => {
497+
if (typeof value !== 'string') {
498+
return value;
499+
}
500+
// 处理空字符串的情况
501+
if (value === '') {
502+
return [];
503+
}
504+
// 处理复杂分隔符的情况
505+
const escapedSeparator = sep.replaceAll(
506+
/[.*+?^${}()|[\]\\]/g,
507+
String.raw`\$&`,
508+
);
509+
return value.split(new RegExp(escapedSeparator));
510+
});
511+
};
512+
513+
// 处理简单数组格式 ['field1', 'field2', ';'] 或 ['field1', 'field2']
514+
if (arrayToStringFields.every((item) => typeof item === 'string')) {
515+
const lastItem =
516+
arrayToStringFields[arrayToStringFields.length - 1] || '';
517+
const fields =
518+
lastItem.length === 1
519+
? arrayToStringFields.slice(0, -1)
520+
: arrayToStringFields;
521+
const separator = lastItem.length === 1 ? lastItem : ',';
522+
processFields(fields, separator);
523+
return;
524+
}
525+
526+
// 处理嵌套数组格式 [['field1'], ';']
527+
arrayToStringFields.forEach((fieldConfig) => {
528+
if (Array.isArray(fieldConfig)) {
529+
const [fields, separator = ','] = fieldConfig;
530+
if (Array.isArray(fields)) {
531+
processFields(fields, separator);
532+
} else if (typeof originValues[fields] === 'string') {
533+
const value = originValues[fields];
534+
if (value === '') {
535+
originValues[fields] = [];
536+
} else {
537+
const escapedSeparator = separator.replaceAll(
538+
/[.*+?^${}()|[\]\\]/g,
539+
String.raw`\$&`,
540+
);
541+
originValues[fields] = value.split(new RegExp(escapedSeparator));
542+
}
543+
}
544+
}
545+
});
546+
};
547+
548+
private processFields = (
549+
fields: string[],
550+
separator: string,
551+
originValues: Record<string, any>,
552+
transformFn: (value: any, separator: string) => any,
553+
) => {
554+
fields.forEach((field) => {
555+
const value = originValues[field];
556+
if (value === undefined || value === null) {
557+
return;
558+
}
559+
originValues[field] = transformFn(value, separator);
560+
});
561+
};
562+
444563
private updateState() {
445564
const currentSchema = this.state?.schema ?? [];
446565
const prevSchema = this.prevState?.schema ?? [];

packages/@core/ui-kit/form-ui/src/types.ts

+29-1
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,12 @@ export type FieldMappingTime = [
232232
)?,
233233
][];
234234

235+
export type ArrayToStringFields = Array<
236+
| [string[], string?] // 嵌套数组格式,可选分隔符
237+
| string // 单个字段,使用默认分隔符
238+
| string[] // 简单数组格式,最后一个元素可以是分隔符
239+
>;
240+
235241
export interface FormSchema<
236242
T extends BaseFormComponentType = BaseFormComponentType,
237243
> extends FormCommonConfig {
@@ -266,6 +272,10 @@ export interface FormFieldProps extends FormSchema {
266272
export interface FormRenderProps<
267273
T extends BaseFormComponentType = BaseFormComponentType,
268274
> {
275+
/**
276+
* 表单字段数组映射字符串配置 默认使用","
277+
*/
278+
arrayToStringFields?: ArrayToStringFields;
269279
/**
270280
* 是否展开,在showCollapseButton=true下生效
271281
*/
@@ -296,6 +306,10 @@ export interface FormRenderProps<
296306
* 组件集合
297307
*/
298308
componentMap: Record<BaseFormComponentType, Component>;
309+
/**
310+
* 表单字段映射到时间格式
311+
*/
312+
fieldMappingTime?: FieldMappingTime;
299313
/**
300314
* 表单实例
301315
*/
@@ -308,10 +322,15 @@ export interface FormRenderProps<
308322
* 表单定义
309323
*/
310324
schema?: FormSchema<T>[];
325+
311326
/**
312327
* 是否显示展开/折叠
313328
*/
314329
showCollapseButton?: boolean;
330+
/**
331+
* 格式化日期
332+
*/
333+
315334
/**
316335
* 表单栅格布局
317336
* @default "grid-cols-1"
@@ -339,6 +358,11 @@ export interface VbenFormProps<
339358
* 表单操作区域class
340359
*/
341360
actionWrapperClass?: ClassType;
361+
/**
362+
* 表单字段数组映射字符串配置 默认使用","
363+
*/
364+
arrayToStringFields?: ArrayToStringFields;
365+
342366
/**
343367
* 表单字段映射
344368
*/
@@ -354,11 +378,15 @@ export interface VbenFormProps<
354378
/**
355379
* 表单值变化回调
356380
*/
357-
handleValuesChange?: (values: Record<string, any>) => void;
381+
handleValuesChange?: (
382+
values: Record<string, any>,
383+
fieldsChanged: string[],
384+
) => void;
358385
/**
359386
* 重置按钮参数
360387
*/
361388
resetButtonOptions?: ActionButtonOptions;
389+
362390
/**
363391
* 是否显示默认操作按钮
364392
* @default true

packages/@core/ui-kit/form-ui/src/vben-use-form.vue

+34-6
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
<script setup lang="ts">
2+
import type { Recordable } from '@vben-core/typings';
3+
24
import type { ExtendedFormApi, VbenFormProps } from './types';
35
46
// import { toRaw, watch } from 'vue';
57
import { nextTick, onMounted, watch } from 'vue';
6-
// import { isFunction } from '@vben-core/shared/utils';
78
89
import { useForwardPriorityValues } from '@vben-core/composables';
9-
import { cloneDeep } from '@vben-core/shared/utils';
10+
import { cloneDeep, get, isEqual, set } from '@vben-core/shared/utils';
1011
1112
import { useDebounceFn } from '@vueuse/core';
1213
@@ -61,16 +62,43 @@ function handleKeyDownEnter(event: KeyboardEvent) {
6162
}
6263
6364
const handleValuesChangeDebounced = useDebounceFn(async () => {
64-
forward.value.handleValuesChange?.(
65-
cloneDeep(await forward.value.formApi.getValues()),
66-
);
6765
state.value.submitOnChange && forward.value.formApi?.validateAndSubmitForm();
6866
}, 300);
6967
68+
const valuesCache: Recordable<any> = {};
69+
7070
onMounted(async () => {
7171
// 只在挂载后开始监听,form.values会有一个初始化的过程
7272
await nextTick();
73-
watch(() => form.values, handleValuesChangeDebounced, { deep: true });
73+
watch(
74+
() => form.values,
75+
(newVal) => {
76+
if (forward.value.handleValuesChange) {
77+
const fields = state.value.schema?.map((item) => {
78+
return item.fieldName;
79+
});
80+
81+
if (fields && fields.length > 0) {
82+
const changedFields: string[] = [];
83+
fields.forEach((field) => {
84+
const newFieldValue = get(newVal, field);
85+
const oldFieldValue = get(valuesCache, field);
86+
if (!isEqual(newFieldValue, oldFieldValue)) {
87+
changedFields.push(field);
88+
set(valuesCache, field, newFieldValue);
89+
}
90+
});
91+
92+
if (changedFields.length > 0) {
93+
// 调用handleValuesChange回调,传入所有表单值的深拷贝和变更的字段列表
94+
forward.value.handleValuesChange(cloneDeep(newVal), changedFields);
95+
}
96+
}
97+
}
98+
handleValuesChangeDebounced();
99+
},
100+
{ deep: true },
101+
);
74102
});
75103
</script>
76104

0 commit comments

Comments
 (0)