Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
This commit is contained in:
commit
f096dfc6e6
@ -12,6 +12,12 @@ Alert提供的功能与Modal类似,但只适用于简单应用场景。例如
|
|||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
::: tip 注意
|
||||||
|
|
||||||
|
Alert提供的快捷方法alert、confirm、prompt动态创建的弹窗在已打开的情况下,不支持HMR(热更新),代码变更后需要关闭这些弹窗后重新打开。
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
::: tip README
|
::: tip README
|
||||||
|
|
||||||
下方示例代码中的,存在一些主题色未适配、样式缺失的问题,这些问题只在文档内会出现,实际使用并不会有这些问题,可忽略,不必纠结。
|
下方示例代码中的,存在一些主题色未适配、样式缺失的问题,这些问题只在文档内会出现,实际使用并不会有这些问题,可忽略,不必纠结。
|
||||||
@ -32,6 +38,23 @@ Alert提供的功能与Modal类似,但只适用于简单应用场景。例如
|
|||||||
|
|
||||||
<DemoPreview dir="demos/vben-alert/prompt" />
|
<DemoPreview dir="demos/vben-alert/prompt" />
|
||||||
|
|
||||||
|
## useAlertContext
|
||||||
|
|
||||||
|
当弹窗的content、footer、icon使用自定义组件时,在这些组件中可以使用 `useAlertContext` 获取当前弹窗的上下文对象,用来主动控制弹窗。
|
||||||
|
|
||||||
|
::: tip 注意
|
||||||
|
|
||||||
|
`useAlertContext`只能用在setup或者函数式组件中。
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Methods
|
||||||
|
|
||||||
|
| 方法 | 描述 | 类型 | 版本要求 |
|
||||||
|
| --------- | ------------------ | -------- | -------- |
|
||||||
|
| doConfirm | 调用弹窗的确认操作 | ()=>void | >5.5.4 |
|
||||||
|
| doCancel | 调用弹窗的取消操作 | ()=>void | >5.5.4 |
|
||||||
|
|
||||||
## 类型说明
|
## 类型说明
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
@ -69,8 +92,14 @@ export type AlertProps = {
|
|||||||
contentClass?: string;
|
contentClass?: string;
|
||||||
/** 执行beforeClose回调期间,在内容区域显示一个loading遮罩*/
|
/** 执行beforeClose回调期间,在内容区域显示一个loading遮罩*/
|
||||||
contentMasking?: boolean;
|
contentMasking?: boolean;
|
||||||
|
/** 弹窗底部内容(与按钮在同一个容器中) */
|
||||||
|
footer?: Component | string;
|
||||||
/** 弹窗的图标(在标题的前面) */
|
/** 弹窗的图标(在标题的前面) */
|
||||||
icon?: Component | IconType;
|
icon?: Component | IconType;
|
||||||
|
/**
|
||||||
|
* 弹窗遮罩模糊效果
|
||||||
|
*/
|
||||||
|
overlayBlur?: number;
|
||||||
/** 是否显示取消按钮 */
|
/** 是否显示取消按钮 */
|
||||||
showCancel?: boolean;
|
showCancel?: boolean;
|
||||||
/** 弹窗标题 */
|
/** 弹窗标题 */
|
||||||
|
@ -151,16 +151,17 @@ function fetchApi(): Promise<Record<string, any>> {
|
|||||||
| options | 直接传入选项数据,也作为api返回空数据时的后备数据 | `OptionsItem[]` | - | - |
|
| options | 直接传入选项数据,也作为api返回空数据时的后备数据 | `OptionsItem[]` | - | - |
|
||||||
| visibleEvent | 触发重新请求数据的事件名 | `string` | - | - |
|
| visibleEvent | 触发重新请求数据的事件名 | `string` | - | - |
|
||||||
| loadingSlot | 目标组件的插槽名称,用来显示一个"加载中"的图标 | `string` | - | - |
|
| loadingSlot | 目标组件的插槽名称,用来显示一个"加载中"的图标 | `string` | - | - |
|
||||||
| autoSelect | 自动设置选项 | `'first' \| 'last' \| 'none'\| false` | `false` | >5.5.4 |
|
| autoSelect | 自动设置选项 | `'first' \| 'last' \| 'one'\| ((item: OptionsItem[]) => OptionsItem) \| false` | `false` | >5.5.4 |
|
||||||
|
|
||||||
#### autoSelect 自动设置选项
|
#### autoSelect 自动设置选项
|
||||||
|
|
||||||
如果当前值为undefined,在选项数据成功加载之后,自动从备选项中选择一个作为当前值。默认值为`false`,即不自动选择选项。注意:该属性不应用于多选组件。可选值有:
|
如果当前值为undefined,在选项数据成功加载之后,自动从备选项中选择一个作为当前值。默认值为`false`,即不自动选择选项。注意:该属性不应用于多选组件。可选值有:
|
||||||
|
|
||||||
- `first`:自动选择第一个选项
|
- `"first"`:自动选择第一个选项
|
||||||
- `last`:自动选择最后一个选项
|
- `"last"`:自动选择最后一个选项
|
||||||
- `one`:有且仅有一个选项时,自动选择它
|
- `"one"`:有且仅有一个选项时,自动选择它
|
||||||
- false:不自动选择选项
|
- `自定义函数`:自定义选择逻辑,函数的参数为options,返回值为选择的选项
|
||||||
|
- `false`:不自动选择选项
|
||||||
|
|
||||||
### Methods
|
### Methods
|
||||||
|
|
||||||
@ -168,3 +169,5 @@ function fetchApi(): Promise<Record<string, any>> {
|
|||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| getComponentRef | 获取被包装的组件的实例 | ()=>T | >5.5.4 |
|
| getComponentRef | 获取被包装的组件的实例 | ()=>T | >5.5.4 |
|
||||||
| updateParam | 设置接口请求参数(将与params属性合并) | (newParams: Record<string, any>)=>void | >5.5.4 |
|
| updateParam | 设置接口请求参数(将与params属性合并) | (newParams: Record<string, any>)=>void | >5.5.4 |
|
||||||
|
| getOptions | 获取已加载的选项数据 | ()=>OptionsItem[] | >5.5.4 |
|
||||||
|
| getValue | 获取当前值 | ()=>any | >5.5.4 |
|
||||||
|
@ -128,10 +128,11 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
|||||||
除了上面的属性类型包含`slot`,还可以通过插槽来自定义弹窗的内容。
|
除了上面的属性类型包含`slot`,还可以通过插槽来自定义弹窗的内容。
|
||||||
|
|
||||||
| 插槽名 | 描述 |
|
| 插槽名 | 描述 |
|
||||||
| -------------- | ------------------- |
|
| -------------- | -------------------------------------------------- |
|
||||||
| default | 默认插槽 - 弹窗内容 |
|
| default | 默认插槽 - 弹窗内容 |
|
||||||
| prepend-footer | 取消按钮左侧 |
|
| prepend-footer | 取消按钮左侧 |
|
||||||
| append-footer | 取消按钮右侧 |
|
| center-footer | 取消按钮和确认按钮中间(不使用 footer 插槽时有效) |
|
||||||
|
| append-footer | 确认按钮右侧 |
|
||||||
| close-icon | 关闭按钮图标 |
|
| close-icon | 关闭按钮图标 |
|
||||||
| extra | 额外内容(标题右侧) |
|
| extra | 额外内容(标题右侧) |
|
||||||
|
|
||||||
|
@ -60,7 +60,6 @@ Modal 内的内容一般业务中,会比较复杂,所以我们可以将 moda
|
|||||||
|
|
||||||
- `VbenModal` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenModal参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
|
- `VbenModal` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenModal参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
|
||||||
- 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenModal`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。
|
- 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenModal`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。
|
||||||
- 使用了`connectedComponent`参数时,可以配置`destroyOnClose`属性来决定当关闭弹窗时,是否要销毁`connectedComponent`组件(重新创建`connectedComponent`组件,这将会把其内部所有的变量、状态、数据等恢复到初始状态。)。
|
|
||||||
- 如果弹窗的默认行为不符合你的预期,可以在`src\bootstrap.ts`中修改`setDefaultModalProps`的参数来设置默认的属性,如默认隐藏全屏按钮,修改默认ZIndex等。
|
- 如果弹窗的默认行为不符合你的预期,可以在`src\bootstrap.ts`中修改`setDefaultModalProps`的参数来设置默认的属性,如默认隐藏全屏按钮,修改默认ZIndex等。
|
||||||
|
|
||||||
:::
|
:::
|
||||||
@ -84,7 +83,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` |
|
| appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` |
|
||||||
| connectedComponent | 连接另一个Modal组件 | `Component` | - |
|
| connectedComponent | 连接另一个Modal组件 | `Component` | - |
|
||||||
| destroyOnClose | 关闭时销毁`connectedComponent` | `boolean` | `false` |
|
| destroyOnClose | 关闭时销毁 | `boolean` | `false` |
|
||||||
| title | 标题 | `string\|slot` | - |
|
| title | 标题 | `string\|slot` | - |
|
||||||
| titleTooltip | 标题提示信息 | `string\|slot` | - |
|
| titleTooltip | 标题提示信息 | `string\|slot` | - |
|
||||||
| description | 描述信息 | `string\|slot` | - |
|
| description | 描述信息 | `string\|slot` | - |
|
||||||
@ -139,10 +138,11 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
除了上面的属性类型包含`slot`,还可以通过插槽来自定义弹窗的内容。
|
除了上面的属性类型包含`slot`,还可以通过插槽来自定义弹窗的内容。
|
||||||
|
|
||||||
| 插槽名 | 描述 |
|
| 插槽名 | 描述 |
|
||||||
| -------------- | ------------------- |
|
| -------------- | -------------------------------------------------- |
|
||||||
| default | 默认插槽 - 弹窗内容 |
|
| default | 默认插槽 - 弹窗内容 |
|
||||||
| prepend-footer | 取消按钮左侧 |
|
| prepend-footer | 取消按钮左侧 |
|
||||||
| append-footer | 取消按钮右侧 |
|
| center-footer | 取消按钮和确认按钮中间(不使用 footer 插槽时有效) |
|
||||||
|
| append-footer | 确认按钮右侧 |
|
||||||
|
|
||||||
### modalApi
|
### modalApi
|
||||||
|
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { h, ref } from 'vue';
|
||||||
|
|
||||||
import { alert, confirm, VbenButton } from '@vben/common-ui';
|
import { alert, confirm, VbenButton } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Checkbox, message } from 'ant-design-vue';
|
||||||
|
|
||||||
function showConfirm() {
|
function showConfirm() {
|
||||||
confirm('This is an alert message')
|
confirm('This is an alert message')
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@ -18,6 +22,34 @@ function showIconConfirm() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showfooterConfirm() {
|
||||||
|
const checked = ref(false);
|
||||||
|
confirm({
|
||||||
|
cancelText: '不要虾扯蛋',
|
||||||
|
confirmText: '是的,我们都是NPC',
|
||||||
|
content:
|
||||||
|
'刚才发生的事情,为什么我似乎早就经历过一般?\n我甚至能在事情发生过程中潜意识里预知到接下来会发生什么。\n\n听起来挺玄乎的,你有过这种感觉吗?',
|
||||||
|
footer: () =>
|
||||||
|
h(
|
||||||
|
Checkbox,
|
||||||
|
{
|
||||||
|
checked: checked.value,
|
||||||
|
class: 'flex-1',
|
||||||
|
'onUpdate:checked': (v) => (checked.value = v),
|
||||||
|
},
|
||||||
|
'不再提示',
|
||||||
|
),
|
||||||
|
icon: 'question',
|
||||||
|
title: '未解之谜',
|
||||||
|
}).then(() => {
|
||||||
|
if (checked.value) {
|
||||||
|
message.success('我不会再拿这个问题烦你了');
|
||||||
|
} else {
|
||||||
|
message.info('下次还要继续问你哟');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function showAsyncConfirm() {
|
function showAsyncConfirm() {
|
||||||
confirm({
|
confirm({
|
||||||
beforeClose({ isConfirm }) {
|
beforeClose({ isConfirm }) {
|
||||||
@ -37,6 +69,7 @@ function showAsyncConfirm() {
|
|||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<VbenButton @click="showConfirm">Confirm</VbenButton>
|
<VbenButton @click="showConfirm">Confirm</VbenButton>
|
||||||
<VbenButton @click="showIconConfirm">Confirm With Icon</VbenButton>
|
<VbenButton @click="showIconConfirm">Confirm With Icon</VbenButton>
|
||||||
|
<VbenButton @click="showfooterConfirm">Confirm With Footer</VbenButton>
|
||||||
<VbenButton @click="showAsyncConfirm">Async Confirm</VbenButton>
|
<VbenButton @click="showAsyncConfirm">Async Confirm</VbenButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { h } from 'vue';
|
import { h } from 'vue';
|
||||||
|
|
||||||
import { alert, prompt, VbenButton } from '@vben/common-ui';
|
import { alert, prompt, useAlertContext, VbenButton } from '@vben/common-ui';
|
||||||
|
|
||||||
import { Input, RadioGroup, Select } from 'ant-design-vue';
|
import { Input, RadioGroup, Select } from 'ant-design-vue';
|
||||||
import { BadgeJapaneseYen } from 'lucide-vue-next';
|
import { BadgeJapaneseYen } from 'lucide-vue-next';
|
||||||
@ -20,16 +20,30 @@ function showPrompt() {
|
|||||||
|
|
||||||
function showSlotsPrompt() {
|
function showSlotsPrompt() {
|
||||||
prompt({
|
prompt({
|
||||||
component: Input,
|
component: () => {
|
||||||
componentProps: {
|
// 获取弹窗上下文。注意:只能在setup或者函数式组件中调用
|
||||||
|
const { doConfirm } = useAlertContext();
|
||||||
|
return h(
|
||||||
|
Input,
|
||||||
|
{
|
||||||
|
onKeydown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
// 调用弹窗提供的确认方法
|
||||||
|
doConfirm();
|
||||||
|
}
|
||||||
|
},
|
||||||
placeholder: '请输入',
|
placeholder: '请输入',
|
||||||
prefix: '充值金额',
|
prefix: '充值金额:',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
},
|
},
|
||||||
componentSlots: {
|
{
|
||||||
addonAfter: () => h(BadgeJapaneseYen),
|
addonAfter: () => h(BadgeJapaneseYen),
|
||||||
},
|
},
|
||||||
content: '此弹窗演示了如何使用componentSlots传递自定义插槽',
|
);
|
||||||
|
},
|
||||||
|
content:
|
||||||
|
'此弹窗演示了如何使用自定义插槽,并且可以使用useAlertContext获取到弹窗的上下文。\n在输入框中按下回车键会触发确认操作。',
|
||||||
icon: 'question',
|
icon: 'question',
|
||||||
modelPropName: 'value',
|
modelPropName: 'value',
|
||||||
}).then((val) => {
|
}).then((val) => {
|
||||||
|
@ -295,6 +295,7 @@ export class FormApi {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
const filteredFields = fieldMergeFn(fields, form.values);
|
const filteredFields = fieldMergeFn(fields, form.values);
|
||||||
|
this.handleStringToArrayFields(filteredFields);
|
||||||
form.setValues(filteredFields, shouldValidate);
|
form.setValues(filteredFields, shouldValidate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -304,6 +305,7 @@ export class FormApi {
|
|||||||
const form = await this.getForm();
|
const form = await this.getForm();
|
||||||
await form.submitForm();
|
await form.submitForm();
|
||||||
const rawValues = toRaw(await this.getValues());
|
const rawValues = toRaw(await this.getValues());
|
||||||
|
this.handleArrayToStringFields(rawValues);
|
||||||
await this.state?.handleSubmit?.(rawValues);
|
await this.state?.handleSubmit?.(rawValues);
|
||||||
|
|
||||||
return rawValues;
|
return rawValues;
|
||||||
@ -392,10 +394,53 @@ export class FormApi {
|
|||||||
return this.form;
|
return this.form;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleArrayToStringFields = (originValues: Record<string, any>) => {
|
||||||
|
const arrayToStringFields = this.state?.arrayToStringFields;
|
||||||
|
if (!arrayToStringFields || !Array.isArray(arrayToStringFields)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const processFields = (fields: string[], separator: string = ',') => {
|
||||||
|
this.processFields(fields, separator, originValues, (value, sep) =>
|
||||||
|
Array.isArray(value) ? value.join(sep) : value,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理简单数组格式 ['field1', 'field2', ';'] 或 ['field1', 'field2']
|
||||||
|
if (arrayToStringFields.every((item) => typeof item === 'string')) {
|
||||||
|
const lastItem =
|
||||||
|
arrayToStringFields[arrayToStringFields.length - 1] || '';
|
||||||
|
const fields =
|
||||||
|
lastItem.length === 1
|
||||||
|
? arrayToStringFields.slice(0, -1)
|
||||||
|
: arrayToStringFields;
|
||||||
|
const separator = lastItem.length === 1 ? lastItem : ',';
|
||||||
|
processFields(fields, separator);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理嵌套数组格式 [['field1'], ';']
|
||||||
|
arrayToStringFields.forEach((fieldConfig) => {
|
||||||
|
if (Array.isArray(fieldConfig)) {
|
||||||
|
const [fields, separator = ','] = fieldConfig;
|
||||||
|
// 根据类型定义,fields 应该始终是字符串数组
|
||||||
|
if (!Array.isArray(fields)) {
|
||||||
|
console.warn(
|
||||||
|
`Invalid field configuration: fields should be an array of strings, got ${typeof fields}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
processFields(fields, separator);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
private handleRangeTimeValue = (originValues: Record<string, any>) => {
|
private handleRangeTimeValue = (originValues: Record<string, any>) => {
|
||||||
const values = { ...originValues };
|
const values = { ...originValues };
|
||||||
const fieldMappingTime = this.state?.fieldMappingTime;
|
const fieldMappingTime = this.state?.fieldMappingTime;
|
||||||
|
|
||||||
|
this.handleStringToArrayFields(values);
|
||||||
|
|
||||||
if (!fieldMappingTime || !Array.isArray(fieldMappingTime)) {
|
if (!fieldMappingTime || !Array.isArray(fieldMappingTime)) {
|
||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
@ -441,6 +486,80 @@ export class FormApi {
|
|||||||
return values;
|
return values;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private handleStringToArrayFields = (originValues: Record<string, any>) => {
|
||||||
|
const arrayToStringFields = this.state?.arrayToStringFields;
|
||||||
|
if (!arrayToStringFields || !Array.isArray(arrayToStringFields)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const processFields = (fields: string[], separator: string = ',') => {
|
||||||
|
this.processFields(fields, separator, originValues, (value, sep) => {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
// 处理空字符串的情况
|
||||||
|
if (value === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
// 处理复杂分隔符的情况
|
||||||
|
const escapedSeparator = sep.replaceAll(
|
||||||
|
/[.*+?^${}()|[\]\\]/g,
|
||||||
|
String.raw`\$&`,
|
||||||
|
);
|
||||||
|
return value.split(new RegExp(escapedSeparator));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理简单数组格式 ['field1', 'field2', ';'] 或 ['field1', 'field2']
|
||||||
|
if (arrayToStringFields.every((item) => typeof item === 'string')) {
|
||||||
|
const lastItem =
|
||||||
|
arrayToStringFields[arrayToStringFields.length - 1] || '';
|
||||||
|
const fields =
|
||||||
|
lastItem.length === 1
|
||||||
|
? arrayToStringFields.slice(0, -1)
|
||||||
|
: arrayToStringFields;
|
||||||
|
const separator = lastItem.length === 1 ? lastItem : ',';
|
||||||
|
processFields(fields, separator);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理嵌套数组格式 [['field1'], ';']
|
||||||
|
arrayToStringFields.forEach((fieldConfig) => {
|
||||||
|
if (Array.isArray(fieldConfig)) {
|
||||||
|
const [fields, separator = ','] = fieldConfig;
|
||||||
|
if (Array.isArray(fields)) {
|
||||||
|
processFields(fields, separator);
|
||||||
|
} else if (typeof originValues[fields] === 'string') {
|
||||||
|
const value = originValues[fields];
|
||||||
|
if (value === '') {
|
||||||
|
originValues[fields] = [];
|
||||||
|
} else {
|
||||||
|
const escapedSeparator = separator.replaceAll(
|
||||||
|
/[.*+?^${}()|[\]\\]/g,
|
||||||
|
String.raw`\$&`,
|
||||||
|
);
|
||||||
|
originValues[fields] = value.split(new RegExp(escapedSeparator));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private processFields = (
|
||||||
|
fields: string[],
|
||||||
|
separator: string,
|
||||||
|
originValues: Record<string, any>,
|
||||||
|
transformFn: (value: any, separator: string) => any,
|
||||||
|
) => {
|
||||||
|
fields.forEach((field) => {
|
||||||
|
const value = originValues[field];
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
originValues[field] = transformFn(value, separator);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
private updateState() {
|
private updateState() {
|
||||||
const currentSchema = this.state?.schema ?? [];
|
const currentSchema = this.state?.schema ?? [];
|
||||||
const prevSchema = this.prevState?.schema ?? [];
|
const prevSchema = this.prevState?.schema ?? [];
|
||||||
|
@ -232,6 +232,12 @@ export type FieldMappingTime = [
|
|||||||
)?,
|
)?,
|
||||||
][];
|
][];
|
||||||
|
|
||||||
|
export type ArrayToStringFields = Array<
|
||||||
|
| [string[], string?] // 嵌套数组格式,可选分隔符
|
||||||
|
| string // 单个字段,使用默认分隔符
|
||||||
|
| string[] // 简单数组格式,最后一个元素可以是分隔符
|
||||||
|
>;
|
||||||
|
|
||||||
export interface FormSchema<
|
export interface FormSchema<
|
||||||
T extends BaseFormComponentType = BaseFormComponentType,
|
T extends BaseFormComponentType = BaseFormComponentType,
|
||||||
> extends FormCommonConfig {
|
> extends FormCommonConfig {
|
||||||
@ -266,6 +272,10 @@ export interface FormFieldProps extends FormSchema {
|
|||||||
export interface FormRenderProps<
|
export interface FormRenderProps<
|
||||||
T extends BaseFormComponentType = BaseFormComponentType,
|
T extends BaseFormComponentType = BaseFormComponentType,
|
||||||
> {
|
> {
|
||||||
|
/**
|
||||||
|
* 表单字段数组映射字符串配置 默认使用","
|
||||||
|
*/
|
||||||
|
arrayToStringFields?: ArrayToStringFields;
|
||||||
/**
|
/**
|
||||||
* 是否展开,在showCollapseButton=true下生效
|
* 是否展开,在showCollapseButton=true下生效
|
||||||
*/
|
*/
|
||||||
@ -296,6 +306,10 @@ export interface FormRenderProps<
|
|||||||
* 组件集合
|
* 组件集合
|
||||||
*/
|
*/
|
||||||
componentMap: Record<BaseFormComponentType, Component>;
|
componentMap: Record<BaseFormComponentType, Component>;
|
||||||
|
/**
|
||||||
|
* 表单字段映射到时间格式
|
||||||
|
*/
|
||||||
|
fieldMappingTime?: FieldMappingTime;
|
||||||
/**
|
/**
|
||||||
* 表单实例
|
* 表单实例
|
||||||
*/
|
*/
|
||||||
@ -308,10 +322,15 @@ export interface FormRenderProps<
|
|||||||
* 表单定义
|
* 表单定义
|
||||||
*/
|
*/
|
||||||
schema?: FormSchema<T>[];
|
schema?: FormSchema<T>[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否显示展开/折叠
|
* 是否显示展开/折叠
|
||||||
*/
|
*/
|
||||||
showCollapseButton?: boolean;
|
showCollapseButton?: boolean;
|
||||||
|
/**
|
||||||
|
* 格式化日期
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 表单栅格布局
|
* 表单栅格布局
|
||||||
* @default "grid-cols-1"
|
* @default "grid-cols-1"
|
||||||
@ -339,6 +358,11 @@ export interface VbenFormProps<
|
|||||||
* 表单操作区域class
|
* 表单操作区域class
|
||||||
*/
|
*/
|
||||||
actionWrapperClass?: ClassType;
|
actionWrapperClass?: ClassType;
|
||||||
|
/**
|
||||||
|
* 表单字段数组映射字符串配置 默认使用","
|
||||||
|
*/
|
||||||
|
arrayToStringFields?: ArrayToStringFields;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 表单字段映射
|
* 表单字段映射
|
||||||
*/
|
*/
|
||||||
@ -359,6 +383,7 @@ export interface VbenFormProps<
|
|||||||
* 重置按钮参数
|
* 重置按钮参数
|
||||||
*/
|
*/
|
||||||
resetButtonOptions?: ActionButtonOptions;
|
resetButtonOptions?: ActionButtonOptions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否显示默认操作按钮
|
* 是否显示默认操作按钮
|
||||||
* @default true
|
* @default true
|
||||||
|
@ -208,6 +208,8 @@ onBeforeUnmount(() => {
|
|||||||
nsMenu.e('popup-container'),
|
nsMenu.e('popup-container'),
|
||||||
is(rootMenu.theme, true),
|
is(rootMenu.theme, true),
|
||||||
opened ? '' : 'hidden',
|
opened ? '' : 'hidden',
|
||||||
|
'overflow-auto',
|
||||||
|
'max-h-[calc(var(--radix-hover-card-content-available-height)-20px)]',
|
||||||
]"
|
]"
|
||||||
:content-props="contentProps"
|
:content-props="contentProps"
|
||||||
:open="true"
|
:open="true"
|
||||||
|
@ -7,7 +7,7 @@ import type { AlertProps, BeforeCloseScope, PromptProps } from './alert';
|
|||||||
import { h, nextTick, ref, render } from 'vue';
|
import { h, nextTick, ref, render } from 'vue';
|
||||||
|
|
||||||
import { useSimpleLocale } from '@vben-core/composables';
|
import { useSimpleLocale } from '@vben-core/composables';
|
||||||
import { Input } from '@vben-core/shadcn-ui';
|
import { Input, VbenRenderContent } from '@vben-core/shadcn-ui';
|
||||||
import { isFunction, isString } from '@vben-core/shared/utils';
|
import { isFunction, isString } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
import Alert from './alert.vue';
|
import Alert from './alert.vue';
|
||||||
@ -146,11 +146,7 @@ export async function vbenPrompt<T = any>(
|
|||||||
const inputComponentRef = ref<null | VNode>(null);
|
const inputComponentRef = ref<null | VNode>(null);
|
||||||
const staticContents: Component[] = [];
|
const staticContents: Component[] = [];
|
||||||
|
|
||||||
if (isString(content)) {
|
staticContents.push(h(VbenRenderContent, { content, renderBr: true }));
|
||||||
staticContents.push(h('span', content));
|
|
||||||
} else if (content) {
|
|
||||||
staticContents.push(content as Component);
|
|
||||||
}
|
|
||||||
|
|
||||||
const modelPropName = _modelPropName || 'modelValue';
|
const modelPropName = _modelPropName || 'modelValue';
|
||||||
const componentProps = { ..._componentProps };
|
const componentProps = { ..._componentProps };
|
||||||
|
@ -2,6 +2,8 @@ import type { Component, VNode, VNodeArrayChildren } from 'vue';
|
|||||||
|
|
||||||
import type { Recordable } from '@vben-core/typings';
|
import type { Recordable } from '@vben-core/typings';
|
||||||
|
|
||||||
|
import { createContext } from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
export type IconType = 'error' | 'info' | 'question' | 'success' | 'warning';
|
export type IconType = 'error' | 'info' | 'question' | 'success' | 'warning';
|
||||||
|
|
||||||
export type BeforeCloseScope = {
|
export type BeforeCloseScope = {
|
||||||
@ -34,6 +36,8 @@ export type AlertProps = {
|
|||||||
contentClass?: string;
|
contentClass?: string;
|
||||||
/** 执行beforeClose回调期间,在内容区域显示一个loading遮罩*/
|
/** 执行beforeClose回调期间,在内容区域显示一个loading遮罩*/
|
||||||
contentMasking?: boolean;
|
contentMasking?: boolean;
|
||||||
|
/** 弹窗底部内容(与按钮在同一个容器中) */
|
||||||
|
footer?: Component | string;
|
||||||
/** 弹窗的图标(在标题的前面) */
|
/** 弹窗的图标(在标题的前面) */
|
||||||
icon?: Component | IconType;
|
icon?: Component | IconType;
|
||||||
/**
|
/**
|
||||||
@ -68,3 +72,28 @@ export type PromptProps<T = any> = {
|
|||||||
/** 输入组件的值属性名 */
|
/** 输入组件的值属性名 */
|
||||||
modelPropName?: string;
|
modelPropName?: string;
|
||||||
} & Omit<AlertProps, 'beforeClose'>;
|
} & Omit<AlertProps, 'beforeClose'>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alert上下文
|
||||||
|
*/
|
||||||
|
export type AlertContext = {
|
||||||
|
/** 执行取消操作 */
|
||||||
|
doCancel: () => void;
|
||||||
|
/** 执行确认操作 */
|
||||||
|
doConfirm: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const [injectAlertContext, provideAlertContext] =
|
||||||
|
createContext<AlertContext>('VbenAlertContext');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取Alert上下文
|
||||||
|
* @returns AlertContext
|
||||||
|
*/
|
||||||
|
export function useAlertContext() {
|
||||||
|
const context = injectAlertContext();
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useAlertContext must be used within an AlertProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
@ -28,6 +28,8 @@ import {
|
|||||||
import { globalShareState } from '@vben-core/shared/global-state';
|
import { globalShareState } from '@vben-core/shared/global-state';
|
||||||
import { cn } from '@vben-core/shared/utils';
|
import { cn } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
|
import { provideAlertContext } from './alert';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<AlertProps>(), {
|
const props = withDefaults(defineProps<AlertProps>(), {
|
||||||
bordered: true,
|
bordered: true,
|
||||||
buttonAlign: 'end',
|
buttonAlign: 'end',
|
||||||
@ -87,6 +89,23 @@ const getIconRender = computed(() => {
|
|||||||
}
|
}
|
||||||
return iconRender;
|
return iconRender;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function doCancel() {
|
||||||
|
isConfirm.value = false;
|
||||||
|
handleOpenChange(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function doConfirm() {
|
||||||
|
isConfirm.value = true;
|
||||||
|
handleOpenChange(false);
|
||||||
|
emits('confirm');
|
||||||
|
}
|
||||||
|
|
||||||
|
provideAlertContext({
|
||||||
|
doCancel,
|
||||||
|
doConfirm,
|
||||||
|
});
|
||||||
|
|
||||||
function handleConfirm() {
|
function handleConfirm() {
|
||||||
isConfirm.value = true;
|
isConfirm.value = true;
|
||||||
emits('confirm');
|
emits('confirm');
|
||||||
@ -152,12 +171,16 @@ async function handleOpenChange(val: boolean) {
|
|||||||
</div>
|
</div>
|
||||||
</AlertDialogTitle>
|
</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
<div class="m-4 mb-6 min-h-[30px]">
|
<div class="m-4 min-h-[30px]">
|
||||||
<VbenRenderContent :content="content" render-br />
|
<VbenRenderContent :content="content" render-br />
|
||||||
</div>
|
</div>
|
||||||
<VbenLoading v-if="loading && contentMasking" :spinning="loading" />
|
<VbenLoading v-if="loading && contentMasking" :spinning="loading" />
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
<div class="flex justify-end gap-x-2" :class="`justify-${buttonAlign}`">
|
<div
|
||||||
|
class="flex items-center justify-end gap-x-2"
|
||||||
|
:class="`justify-${buttonAlign}`"
|
||||||
|
>
|
||||||
|
<VbenRenderContent :content="footer" />
|
||||||
<AlertDialogCancel v-if="showCancel" as-child>
|
<AlertDialogCancel v-if="showCancel" as-child>
|
||||||
<component
|
<component
|
||||||
:is="components.DefaultButton || VbenButton"
|
:is="components.DefaultButton || VbenButton"
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
export * from './alert';
|
export type {
|
||||||
|
AlertProps,
|
||||||
|
BeforeCloseScope,
|
||||||
|
IconType,
|
||||||
|
PromptProps,
|
||||||
|
} from './alert';
|
||||||
|
export { useAlertContext } from './alert';
|
||||||
export { default as Alert } from './alert.vue';
|
export { default as Alert } from './alert.vue';
|
||||||
export {
|
export {
|
||||||
vbenAlert as alert,
|
vbenAlert as alert,
|
||||||
|
@ -274,7 +274,7 @@ const getAppendTo = computed(() => {
|
|||||||
{{ cancelText || $t('cancel') }}
|
{{ cancelText || $t('cancel') }}
|
||||||
</slot>
|
</slot>
|
||||||
</component>
|
</component>
|
||||||
|
<slot name="center-footer"></slot>
|
||||||
<component
|
<component
|
||||||
:is="components.PrimaryButton || VbenButton"
|
:is="components.PrimaryButton || VbenButton"
|
||||||
v-if="showConfirmButton"
|
v-if="showConfirmButton"
|
||||||
|
@ -44,6 +44,7 @@ export class ModalApi {
|
|||||||
confirmDisabled: false,
|
confirmDisabled: false,
|
||||||
confirmLoading: false,
|
confirmLoading: false,
|
||||||
contentClass: '',
|
contentClass: '',
|
||||||
|
destroyOnClose: true,
|
||||||
draggable: false,
|
draggable: false,
|
||||||
footer: true,
|
footer: true,
|
||||||
footerClass: '',
|
footerClass: '',
|
||||||
|
@ -60,6 +60,10 @@ export interface ModalProps {
|
|||||||
* 弹窗描述
|
* 弹窗描述
|
||||||
*/
|
*/
|
||||||
description?: string;
|
description?: string;
|
||||||
|
/**
|
||||||
|
* 在关闭时销毁弹窗
|
||||||
|
*/
|
||||||
|
destroyOnClose?: boolean;
|
||||||
/**
|
/**
|
||||||
* 是否可拖拽
|
* 是否可拖拽
|
||||||
* @default false
|
* @default false
|
||||||
@ -153,10 +157,6 @@ export interface ModalApiOptions extends ModalState {
|
|||||||
* 独立的弹窗组件
|
* 独立的弹窗组件
|
||||||
*/
|
*/
|
||||||
connectedComponent?: Component;
|
connectedComponent?: Component;
|
||||||
/**
|
|
||||||
* 在关闭时销毁弹窗。仅在使用 connectedComponent 时有效
|
|
||||||
*/
|
|
||||||
destroyOnClose?: boolean;
|
|
||||||
/**
|
/**
|
||||||
* 关闭前的回调,返回 false 可以阻止关闭
|
* 关闭前的回调,返回 false 可以阻止关闭
|
||||||
* @returns
|
* @returns
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { ExtendedModalApi, ModalProps } from './modal';
|
import type { ExtendedModalApi, ModalProps } from './modal';
|
||||||
|
|
||||||
import { computed, nextTick, provide, ref, useId, watch } from 'vue';
|
import { computed, nextTick, provide, ref, unref, useId, watch } from 'vue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useIsMobile,
|
useIsMobile,
|
||||||
@ -34,6 +34,7 @@ interface Props extends ModalProps {
|
|||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
appendToMain: false,
|
appendToMain: false,
|
||||||
|
destroyOnClose: true,
|
||||||
modalApi: undefined,
|
modalApi: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -67,6 +68,7 @@ const {
|
|||||||
confirmText,
|
confirmText,
|
||||||
contentClass,
|
contentClass,
|
||||||
description,
|
description,
|
||||||
|
destroyOnClose,
|
||||||
draggable,
|
draggable,
|
||||||
footer: showFooter,
|
footer: showFooter,
|
||||||
footerClass,
|
footerClass,
|
||||||
@ -100,10 +102,15 @@ const { dragging, transform } = useModalDraggable(
|
|||||||
shouldDraggable,
|
shouldDraggable,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const firstOpened = ref(false);
|
||||||
|
const isClosed = ref(true);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => state?.value?.isOpen,
|
() => state?.value?.isOpen,
|
||||||
async (v) => {
|
async (v) => {
|
||||||
if (v) {
|
if (v) {
|
||||||
|
isClosed.value = false;
|
||||||
|
if (!firstOpened.value) firstOpened.value = true;
|
||||||
await nextTick();
|
await nextTick();
|
||||||
if (!contentRef.value) return;
|
if (!contentRef.value) return;
|
||||||
const innerContentRef = contentRef.value.getContentRef();
|
const innerContentRef = contentRef.value.getContentRef();
|
||||||
@ -113,6 +120,7 @@ watch(
|
|||||||
dialogRef.value.style.transform = `translate(${offsetX}px, ${offsetY}px)`;
|
dialogRef.value.style.transform = `translate(${offsetX}px, ${offsetY}px)`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@ -176,6 +184,15 @@ const getAppendTo = computed(() => {
|
|||||||
? `#${ELEMENT_ID_MAIN_CONTENT}>div:not(.absolute)>div`
|
? `#${ELEMENT_ID_MAIN_CONTENT}>div:not(.absolute)>div`
|
||||||
: undefined;
|
: undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getForceMount = computed(() => {
|
||||||
|
return !unref(destroyOnClose);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleClosed() {
|
||||||
|
isClosed.value = true;
|
||||||
|
props.modalApi?.onClosed();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<Dialog
|
<Dialog
|
||||||
@ -197,9 +214,11 @@ const getAppendTo = computed(() => {
|
|||||||
shouldFullscreen,
|
shouldFullscreen,
|
||||||
'top-1/2 !-translate-y-1/2': centered && !shouldFullscreen,
|
'top-1/2 !-translate-y-1/2': centered && !shouldFullscreen,
|
||||||
'duration-300': !dragging,
|
'duration-300': !dragging,
|
||||||
|
hidden: isClosed,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
:force-mount="getForceMount"
|
||||||
:modal="modal"
|
:modal="modal"
|
||||||
:open="state?.isOpen"
|
:open="state?.isOpen"
|
||||||
:show-close="closable"
|
:show-close="closable"
|
||||||
@ -207,7 +226,7 @@ const getAppendTo = computed(() => {
|
|||||||
:overlay-blur="overlayBlur"
|
:overlay-blur="overlayBlur"
|
||||||
close-class="top-3"
|
close-class="top-3"
|
||||||
@close-auto-focus="handleFocusOutside"
|
@close-auto-focus="handleFocusOutside"
|
||||||
@closed="() => modalApi?.onClosed()"
|
@closed="handleClosed"
|
||||||
:close-disabled="submitting"
|
:close-disabled="submitting"
|
||||||
@escape-key-down="escapeKeyDown"
|
@escape-key-down="escapeKeyDown"
|
||||||
@focus-outside="handleFocusOutside"
|
@focus-outside="handleFocusOutside"
|
||||||
@ -302,7 +321,7 @@ const getAppendTo = computed(() => {
|
|||||||
{{ cancelText || $t('cancel') }}
|
{{ cancelText || $t('cancel') }}
|
||||||
</slot>
|
</slot>
|
||||||
</component>
|
</component>
|
||||||
|
<slot name="center-footer"></slot>
|
||||||
<component
|
<component
|
||||||
:is="components.PrimaryButton || VbenButton"
|
:is="components.PrimaryButton || VbenButton"
|
||||||
v-if="showConfirmButton"
|
v-if="showConfirmButton"
|
||||||
|
@ -1,14 +1,6 @@
|
|||||||
import type { ExtendedModalApi, ModalApiOptions, ModalProps } from './modal';
|
import type { ExtendedModalApi, ModalApiOptions, ModalProps } from './modal';
|
||||||
|
|
||||||
import {
|
import { defineComponent, h, inject, nextTick, provide, reactive } from 'vue';
|
||||||
defineComponent,
|
|
||||||
h,
|
|
||||||
inject,
|
|
||||||
nextTick,
|
|
||||||
provide,
|
|
||||||
reactive,
|
|
||||||
ref,
|
|
||||||
} from 'vue';
|
|
||||||
|
|
||||||
import { useStore } from '@vben-core/shared/store';
|
import { useStore } from '@vben-core/shared/store';
|
||||||
|
|
||||||
@ -32,7 +24,6 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
|||||||
const { connectedComponent } = options;
|
const { connectedComponent } = options;
|
||||||
if (connectedComponent) {
|
if (connectedComponent) {
|
||||||
const extendedApi = reactive({});
|
const extendedApi = reactive({});
|
||||||
const isModalReady = ref(true);
|
|
||||||
const Modal = defineComponent(
|
const Modal = defineComponent(
|
||||||
(props: TParentModalProps, { attrs, slots }) => {
|
(props: TParentModalProps, { attrs, slots }) => {
|
||||||
provide(USER_MODAL_INJECT_KEY, {
|
provide(USER_MODAL_INJECT_KEY, {
|
||||||
@ -42,11 +33,6 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
|||||||
Object.setPrototypeOf(extendedApi, api);
|
Object.setPrototypeOf(extendedApi, api);
|
||||||
},
|
},
|
||||||
options,
|
options,
|
||||||
async reCreateModal() {
|
|
||||||
isModalReady.value = false;
|
|
||||||
await nextTick();
|
|
||||||
isModalReady.value = true;
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
checkProps(extendedApi as ExtendedModalApi, {
|
checkProps(extendedApi as ExtendedModalApi, {
|
||||||
...props,
|
...props,
|
||||||
@ -55,7 +41,7 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
|||||||
});
|
});
|
||||||
return () =>
|
return () =>
|
||||||
h(
|
h(
|
||||||
isModalReady.value ? connectedComponent : 'div',
|
connectedComponent,
|
||||||
{
|
{
|
||||||
...props,
|
...props,
|
||||||
...attrs,
|
...attrs,
|
||||||
@ -84,14 +70,6 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
|||||||
injectData.options?.onOpenChange?.(isOpen);
|
injectData.options?.onOpenChange?.(isOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onClosed = mergedOptions.onClosed;
|
|
||||||
|
|
||||||
mergedOptions.onClosed = () => {
|
|
||||||
onClosed?.();
|
|
||||||
if (mergedOptions.destroyOnClose) {
|
|
||||||
injectData.reCreateModal?.();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const api = new ModalApi(mergedOptions);
|
const api = new ModalApi(mergedOptions);
|
||||||
|
|
||||||
const extendedApi: ExtendedModalApi = api as never;
|
const extendedApi: ExtendedModalApi = api as never;
|
||||||
|
@ -31,12 +31,11 @@ export default defineComponent({
|
|||||||
if (props.renderBr && isString(props.content)) {
|
if (props.renderBr && isString(props.content)) {
|
||||||
const lines = props.content.split('\n');
|
const lines = props.content.split('\n');
|
||||||
const result = [];
|
const result = [];
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (const [i, line] of lines.entries()) {
|
||||||
const line = lines[i];
|
result.push(h('p', { key: i }, line));
|
||||||
result.push(h('span', { key: i }, line));
|
// if (i < lines.length - 1) {
|
||||||
if (i < lines.length - 1) {
|
// result.push(h('br'));
|
||||||
result.push(h('br'));
|
// }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
} else {
|
} else {
|
||||||
|
@ -39,6 +39,14 @@ const isAtRight = ref(false);
|
|||||||
const isAtBottom = ref(false);
|
const isAtBottom = ref(false);
|
||||||
const isAtLeft = ref(true);
|
const isAtLeft = ref(true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We have to check if the scroll amount is close enough to some threshold in order to
|
||||||
|
* more accurately calculate arrivedState. This is because scrollTop/scrollLeft are non-rounded
|
||||||
|
* numbers, while scrollHeight/scrollWidth and clientHeight/clientWidth are rounded.
|
||||||
|
* https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#determine_if_an_element_has_been_totally_scrolled
|
||||||
|
*/
|
||||||
|
const ARRIVED_STATE_THRESHOLD_PIXELS = 1;
|
||||||
|
|
||||||
const showShadowTop = computed(() => props.shadow && props.shadowTop);
|
const showShadowTop = computed(() => props.shadow && props.shadowTop);
|
||||||
const showShadowBottom = computed(() => props.shadow && props.shadowBottom);
|
const showShadowBottom = computed(() => props.shadow && props.shadowBottom);
|
||||||
const showShadowLeft = computed(() => props.shadow && props.shadowLeft);
|
const showShadowLeft = computed(() => props.shadow && props.shadowLeft);
|
||||||
@ -60,14 +68,18 @@ function handleScroll(event: Event) {
|
|||||||
const target = event.target as HTMLElement;
|
const target = event.target as HTMLElement;
|
||||||
const scrollTop = target?.scrollTop ?? 0;
|
const scrollTop = target?.scrollTop ?? 0;
|
||||||
const scrollLeft = target?.scrollLeft ?? 0;
|
const scrollLeft = target?.scrollLeft ?? 0;
|
||||||
const offsetHeight = target?.offsetHeight ?? 0;
|
const clientHeight = target?.clientHeight ?? 0;
|
||||||
const offsetWidth = target?.offsetWidth ?? 0;
|
const clientWidth = target?.clientWidth ?? 0;
|
||||||
const scrollHeight = target?.scrollHeight ?? 0;
|
const scrollHeight = target?.scrollHeight ?? 0;
|
||||||
const scrollWidth = target?.scrollWidth ?? 0;
|
const scrollWidth = target?.scrollWidth ?? 0;
|
||||||
isAtTop.value = scrollTop <= 0;
|
isAtTop.value = scrollTop <= 0;
|
||||||
isAtLeft.value = scrollLeft <= 0;
|
isAtLeft.value = scrollLeft <= 0;
|
||||||
isAtBottom.value = scrollTop + offsetHeight >= scrollHeight;
|
isAtBottom.value =
|
||||||
isAtRight.value = scrollLeft + offsetWidth >= scrollWidth;
|
Math.abs(scrollTop) + clientHeight >=
|
||||||
|
scrollHeight - ARRIVED_STATE_THRESHOLD_PIXELS;
|
||||||
|
isAtRight.value =
|
||||||
|
Math.abs(scrollLeft) + clientWidth >=
|
||||||
|
scrollWidth - ARRIVED_STATE_THRESHOLD_PIXELS;
|
||||||
|
|
||||||
emit('scrollAt', {
|
emit('scrollAt', {
|
||||||
bottom: isAtBottom.value,
|
bottom: isAtBottom.value,
|
||||||
|
@ -59,9 +59,15 @@ interface Props {
|
|||||||
* - `first`:自动选择第一个选项
|
* - `first`:自动选择第一个选项
|
||||||
* - `last`:自动选择最后一个选项
|
* - `last`:自动选择最后一个选项
|
||||||
* - `one`: 当请求的结果只有一个选项时,自动选择该选项
|
* - `one`: 当请求的结果只有一个选项时,自动选择该选项
|
||||||
|
* - 函数:自定义选择逻辑,函数的参数为请求的结果数组,返回值为选择的选项
|
||||||
* - false:不自动选择(默认)
|
* - false:不自动选择(默认)
|
||||||
*/
|
*/
|
||||||
autoSelect?: 'first' | 'last' | 'one' | false;
|
autoSelect?:
|
||||||
|
| 'first'
|
||||||
|
| 'last'
|
||||||
|
| 'one'
|
||||||
|
| ((item: OptionsItem[]) => OptionsItem)
|
||||||
|
| false;
|
||||||
}
|
}
|
||||||
|
|
||||||
defineOptions({ name: 'ApiComponent', inheritAttrs: false });
|
defineOptions({ name: 'ApiComponent', inheritAttrs: false });
|
||||||
@ -209,6 +215,9 @@ function emitChange() {
|
|||||||
unref(getOptions).length > 0
|
unref(getOptions).length > 0
|
||||||
) {
|
) {
|
||||||
let firstOption;
|
let firstOption;
|
||||||
|
if (isFunction(props.autoSelect)) {
|
||||||
|
firstOption = props.autoSelect(unref(getOptions));
|
||||||
|
} else {
|
||||||
switch (props.autoSelect) {
|
switch (props.autoSelect) {
|
||||||
case 'first': {
|
case 'first': {
|
||||||
firstOption = unref(getOptions)[0];
|
firstOption = unref(getOptions)[0];
|
||||||
@ -225,13 +234,18 @@ function emitChange() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (firstOption) modelValue.value = firstOption[props.valueField];
|
if (firstOption) modelValue.value = firstOption.value;
|
||||||
}
|
}
|
||||||
emit('optionsChange', unref(getOptions));
|
emit('optionsChange', unref(getOptions));
|
||||||
}
|
}
|
||||||
const componentRef = ref();
|
const componentRef = ref();
|
||||||
defineExpose({
|
defineExpose({
|
||||||
|
/** 获取options数据 */
|
||||||
|
getOptions: () => unref(getOptions),
|
||||||
|
/** 获取当前值 */
|
||||||
|
getValue: () => unref(modelValue),
|
||||||
/** 获取被包装的组件实例 */
|
/** 获取被包装的组件实例 */
|
||||||
getComponentRef: <T = any,>() => componentRef.value as T,
|
getComponentRef: <T = any,>() => componentRef.value as T,
|
||||||
/** 更新Api参数 */
|
/** 更新Api参数 */
|
||||||
|
@ -212,7 +212,12 @@ setupVbenVxeTable({
|
|||||||
Popconfirm,
|
Popconfirm,
|
||||||
{
|
{
|
||||||
getPopupContainer(el) {
|
getPopupContainer(el) {
|
||||||
return el.closest('tbody') || document.body;
|
return (
|
||||||
|
el
|
||||||
|
.closest('.vxe-table--viewport-wrapper')
|
||||||
|
?.querySelector('.vxe-table--main-wrapper')
|
||||||
|
?.querySelector('tbody') || document.body
|
||||||
|
);
|
||||||
},
|
},
|
||||||
placement: 'topLeft',
|
placement: 'topLeft',
|
||||||
title: $t('ui.actionTitle.delete', [attrs?.nameTitle || '']),
|
title: $t('ui.actionTitle.delete', [attrs?.nameTitle || '']),
|
||||||
|
@ -30,5 +30,6 @@ function lockDrawer() {
|
|||||||
<Button type="primary" @click="lockDrawer">锁定抽屉状态</Button>
|
<Button type="primary" @click="lockDrawer">锁定抽屉状态</Button>
|
||||||
<!-- <template #prepend-footer> slot </template> -->
|
<!-- <template #prepend-footer> slot </template> -->
|
||||||
<!-- <template #append-footer> prepend slot </template> -->
|
<!-- <template #append-footer> prepend slot </template> -->
|
||||||
|
<!-- <template #center-footer> center slot </template> -->
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</template>
|
</template>
|
||||||
|
@ -16,15 +16,18 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
},
|
},
|
||||||
onOpenChange(isOpen) {
|
onOpenChange(isOpen) {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
handleUpdate(10);
|
handleUpdate();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleUpdate(len: number) {
|
function handleUpdate(len?: number) {
|
||||||
modalApi.setState({ confirmDisabled: true, loading: true });
|
modalApi.setState({ confirmDisabled: true, loading: true });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
list.value = Array.from({ length: len }, (_v, k) => k + 1);
|
list.value = Array.from(
|
||||||
|
{ length: len ?? Math.floor(Math.random() * 10) + 1 },
|
||||||
|
(_v, k) => k + 1,
|
||||||
|
);
|
||||||
modalApi.setState({ confirmDisabled: false, loading: false });
|
modalApi.setState({ confirmDisabled: false, loading: false });
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
@ -40,7 +43,7 @@ function handleUpdate(len: number) {
|
|||||||
{{ item }}
|
{{ item }}
|
||||||
</div>
|
</div>
|
||||||
<template #prepend-footer>
|
<template #prepend-footer>
|
||||||
<Button type="link" @click="handleUpdate(6)">点击更新数据</Button>
|
<Button type="link" @click="handleUpdate()">点击更新数据</Button>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
@ -24,7 +24,7 @@ const value = ref();
|
|||||||
title="基础弹窗示例"
|
title="基础弹窗示例"
|
||||||
title-tooltip="标题提示内容"
|
title-tooltip="标题提示内容"
|
||||||
>
|
>
|
||||||
此弹窗指定在内容区域打开
|
此弹窗指定在内容区域打开,并且在关闭之后弹窗内容不会被销毁
|
||||||
<Input v-model="value" placeholder="KeepAlive测试" />
|
<Input v-model:value="value" placeholder="KeepAlive测试" />
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
@ -198,7 +198,7 @@ async function openPrompt() {
|
|||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card class="w-[300px]" title="指定容器">
|
<Card class="w-[300px]" title="指定容器+关闭后不销毁">
|
||||||
<p>在内容区域打开弹窗的示例</p>
|
<p>在内容区域打开弹窗的示例</p>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<Button type="primary" @click="openInContentModal">打开弹窗</Button>
|
<Button type="primary" @click="openInContentModal">打开弹窗</Button>
|
||||||
|
@ -11,7 +11,7 @@ export function getMenuTypeOptions() {
|
|||||||
value: 'catalog',
|
value: 'catalog',
|
||||||
},
|
},
|
||||||
{ color: 'default', label: $t('system.menu.typeMenu'), value: 'menu' },
|
{ color: 'default', label: $t('system.menu.typeMenu'), value: 'menu' },
|
||||||
{ color: 'error', label: $t('system.menu.typeButton'), value: 'button' },
|
{ color: 'error', label: $t('system.menu.typeButton'), value: 'action' },
|
||||||
{
|
{
|
||||||
color: 'success',
|
color: 'success',
|
||||||
label: $t('system.menu.typeEmbedded'),
|
label: $t('system.menu.typeEmbedded'),
|
||||||
|
@ -241,10 +241,10 @@ const schema: VbenFormSchema[] = [
|
|||||||
component: 'Input',
|
component: 'Input',
|
||||||
dependencies: {
|
dependencies: {
|
||||||
rules: (values) => {
|
rules: (values) => {
|
||||||
return values.type === 'button' ? 'required' : null;
|
return values.type === 'action' ? 'required' : null;
|
||||||
},
|
},
|
||||||
show: (values) => {
|
show: (values) => {
|
||||||
return ['button', 'catalog', 'embedded', 'menu'].includes(values.type);
|
return ['action', 'catalog', 'embedded', 'menu'].includes(values.type);
|
||||||
},
|
},
|
||||||
triggerFields: ['type'],
|
triggerFields: ['type'],
|
||||||
},
|
},
|
||||||
@ -277,7 +277,7 @@ const schema: VbenFormSchema[] = [
|
|||||||
},
|
},
|
||||||
dependencies: {
|
dependencies: {
|
||||||
show: (values) => {
|
show: (values) => {
|
||||||
return values.type !== 'button';
|
return values.type !== 'action';
|
||||||
},
|
},
|
||||||
triggerFields: ['type'],
|
triggerFields: ['type'],
|
||||||
},
|
},
|
||||||
@ -295,7 +295,7 @@ const schema: VbenFormSchema[] = [
|
|||||||
},
|
},
|
||||||
dependencies: {
|
dependencies: {
|
||||||
show: (values) => {
|
show: (values) => {
|
||||||
return values.type !== 'button';
|
return values.type !== 'action';
|
||||||
},
|
},
|
||||||
triggerFields: ['type'],
|
triggerFields: ['type'],
|
||||||
},
|
},
|
||||||
@ -314,7 +314,7 @@ const schema: VbenFormSchema[] = [
|
|||||||
},
|
},
|
||||||
dependencies: {
|
dependencies: {
|
||||||
show: (values) => {
|
show: (values) => {
|
||||||
return values.type !== 'button';
|
return values.type !== 'action';
|
||||||
},
|
},
|
||||||
triggerFields: ['type'],
|
triggerFields: ['type'],
|
||||||
},
|
},
|
||||||
@ -325,7 +325,7 @@ const schema: VbenFormSchema[] = [
|
|||||||
component: 'Divider',
|
component: 'Divider',
|
||||||
dependencies: {
|
dependencies: {
|
||||||
show: (values) => {
|
show: (values) => {
|
||||||
return !['button', 'link'].includes(values.type);
|
return !['action', 'link'].includes(values.type);
|
||||||
},
|
},
|
||||||
triggerFields: ['type'],
|
triggerFields: ['type'],
|
||||||
},
|
},
|
||||||
@ -372,7 +372,7 @@ const schema: VbenFormSchema[] = [
|
|||||||
component: 'Checkbox',
|
component: 'Checkbox',
|
||||||
dependencies: {
|
dependencies: {
|
||||||
show: (values) => {
|
show: (values) => {
|
||||||
return !['button'].includes(values.type);
|
return !['action'].includes(values.type);
|
||||||
},
|
},
|
||||||
triggerFields: ['type'],
|
triggerFields: ['type'],
|
||||||
},
|
},
|
||||||
@ -402,7 +402,7 @@ const schema: VbenFormSchema[] = [
|
|||||||
component: 'Checkbox',
|
component: 'Checkbox',
|
||||||
dependencies: {
|
dependencies: {
|
||||||
show: (values) => {
|
show: (values) => {
|
||||||
return !['button', 'link'].includes(values.type);
|
return !['action', 'link'].includes(values.type);
|
||||||
},
|
},
|
||||||
triggerFields: ['type'],
|
triggerFields: ['type'],
|
||||||
},
|
},
|
||||||
@ -417,7 +417,7 @@ const schema: VbenFormSchema[] = [
|
|||||||
component: 'Checkbox',
|
component: 'Checkbox',
|
||||||
dependencies: {
|
dependencies: {
|
||||||
show: (values) => {
|
show: (values) => {
|
||||||
return !['button', 'link'].includes(values.type);
|
return !['action', 'link'].includes(values.type);
|
||||||
},
|
},
|
||||||
triggerFields: ['type'],
|
triggerFields: ['type'],
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user