Pre Merge pull request !28 from 玲娜贝er/dev

This commit is contained in:
玲娜贝er 2025-04-16 13:50:58 +00:00 committed by Gitee
commit 7a5438574f
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
67 changed files with 702 additions and 192 deletions

View File

@ -1,3 +1,9 @@
# 1.3.2
**REFACTOR**
- 所有表格操作列宽度调整为'auto', 这样会根据子元素宽度适配(比如没有分配权限的情况)
# 1.3.1
**REFACTOR**

View File

@ -9,7 +9,6 @@ import type { BaseFormComponentType } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import {
computed,
defineAsyncComponent,
defineComponent,
getCurrentInstance,
@ -91,15 +90,10 @@ const withDefaultPlaceholder = <T extends Component>(
inheritAttrs: false,
name: component.name,
setup: (props: any, { attrs, expose, slots }) => {
/**
* 使computed updateSchema更新的placeholder无法显示()
*/
const placeholder = computed(
() =>
props?.placeholder ||
attrs?.placeholder ||
$t(`ui.placeholder.${type}`),
);
const placeholder =
props?.placeholder ||
attrs?.placeholder ||
$t(`ui.placeholder.${type}`);
// 透传组件暴露的方法
const innerRef = ref();
@ -118,7 +112,7 @@ const withDefaultPlaceholder = <T extends Component>(
component,
{
...componentProps,
placeholder: placeholder.value,
placeholder,
...props,
...attrs,
ref: innerRef,

View File

@ -51,7 +51,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right',
slots: { default: 'action' },
title: '操作',
width: 180,
resizable: false,
width: 'auto',
},
];

View File

@ -60,7 +60,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right',
slots: { default: 'action' },
title: '操作',
width: 180,
resizable: false,
width: 'auto',
},
];

View File

@ -79,6 +79,7 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right',
slots: { default: 'action' },
title: '操作',
width: 120,
resizable: false,
width: 'auto',
},
];

View File

@ -86,6 +86,7 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right',
slots: { default: 'action' },
title: '操作',
width: 120,
resizable: false,
width: 'auto',
},
];

View File

@ -95,7 +95,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right',
slots: { default: 'action' },
title: '操作',
width: 180,
resizable: false,
width: 'auto',
},
];

View File

@ -71,7 +71,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right',
slots: { default: 'action' },
title: '操作',
width: 180,
resizable: false,
width: 'auto',
},
];

View File

@ -39,11 +39,13 @@ export const columns: VxeGridProps['columns'] = [
{
field: 'orderNum',
title: '排序',
width: 180,
resizable: false,
width: 'auto',
},
{
field: 'status',
width: 180,
resizable: false,
width: 'auto',
title: '状态',
slots: {
default: ({ row }) => {

View File

@ -45,7 +45,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right',
slots: { default: 'action' },
title: '操作',
width: 180,
resizable: false,
width: 'auto',
},
];

View File

@ -39,7 +39,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right',
slots: { default: 'action' },
title: '操作',
width: 180,
resizable: false,
width: 'auto',
},
];

View File

@ -145,7 +145,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right',
slots: { default: 'action' },
title: '操作',
width: 200,
resizable: false,
width: 'auto',
},
];

View File

@ -69,7 +69,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right',
slots: { default: 'action' },
title: '操作',
width: 180,
resizable: false,
width: 'auto',
},
];

View File

@ -81,7 +81,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right',
slots: { default: 'action' },
title: '操作',
width: 180,
resizable: false,
width: 'auto',
},
];

View File

@ -69,7 +69,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right',
slots: { default: 'action' },
title: '操作',
width: 180,
resizable: false,
width: 'auto',
},
];

View File

@ -65,7 +65,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right',
slots: { default: 'action' },
title: '操作',
width: 180,
resizable: false,
width: 'auto',
},
];

View File

@ -37,6 +37,7 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right',
slots: { default: 'action' },
title: '操作',
width: 180,
resizable: false,
width: 'auto',
},
];

View File

@ -68,7 +68,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right',
slots: { default: 'action' },
title: '操作',
width: 200,
resizable: false,
width: 'auto',
},
];

View File

@ -29,7 +29,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right',
slots: { default: 'action' },
title: '操作',
width: 180,
resizable: false,
width: 'auto',
},
];

View File

@ -33,7 +33,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right',
slots: { default: 'action' },
title: '操作',
width: 200,
resizable: false,
width: 'auto',
},
];

View File

@ -91,7 +91,8 @@ export const columns: VxeGridProps['columns'] = [
fixed: 'right',
slots: { default: 'action' },
title: '操作',
width: 210,
resizable: false,
width: 'auto',
},
];

View File

@ -1,7 +1,9 @@
<script setup lang="ts">
import type { PropType } from 'vue';
import type { CategoryTree } from '#/api/workflow/category/model';
import { onMounted, type PropType, ref } from 'vue';
import { onMounted, ref } from 'vue';
import { SyncOutlined } from '@ant-design/icons-vue';
import { InputSearch, Skeleton, Tree } from 'ant-design-vue';

View File

@ -63,7 +63,7 @@ export const columns: VxeGridProps['columns'] = [
slots: { default: 'action' },
title: '操作',
resizable: false,
width: 200,
width: 'auto',
},
];

View File

@ -1,9 +1,11 @@
<script setup lang="ts">
import type { VxeGridProps } from '#/adapter/vxe-table';
import { Page, useVbenModal } from '@vben/common-ui';
import { Space } from 'ant-design-vue';
import { useVbenVxeGrid, type VxeGridProps } from '#/adapter/vxe-table';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { sseList } from './api';
import sendMsgModal from './send-msg-modal.vue';
@ -31,7 +33,8 @@ const gridOptions: VxeGridProps = {
fixed: 'right',
slots: { default: 'action' },
title: '操作',
width: 180,
resizable: false,
width: 'auto',
},
],
height: 'auto',

View File

@ -104,6 +104,11 @@
--vp-custom-block-tip-text: var(--vp-c-text-1);
--vp-custom-block-tip-bg: var(--vp-c-brand-soft);
--vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
/**
* modal zIndex
*/
--popup-z-index: 1000;
}
@media (min-width: 640px) {

View File

@ -12,6 +12,12 @@ Alert提供的功能与Modal类似但只适用于简单应用场景。例如
:::
::: tip 注意
Alert提供的快捷方法alert、confirm、prompt动态创建的弹窗在已打开的情况下不支持HMR热更新代码变更后需要关闭这些弹窗后重新打开。
:::
::: tip README
下方示例代码中的,存在一些主题色未适配、样式缺失的问题,这些问题只在文档内会出现,实际使用并不会有这些问题,可忽略,不必纠结。
@ -32,6 +38,23 @@ Alert提供的功能与Modal类似但只适用于简单应用场景。例如
<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
@ -69,8 +92,14 @@ export type AlertProps = {
contentClass?: string;
/** 执行beforeClose回调期间在内容区域显示一个loading遮罩*/
contentMasking?: boolean;
/** 弹窗底部内容(与按钮在同一个容器中) */
footer?: Component | string;
/** 弹窗的图标(在标题的前面) */
icon?: Component | IconType;
/**
* 弹窗遮罩模糊效果
*/
overlayBlur?: number;
/** 是否显示取消按钮 */
showCancel?: boolean;
/** 弹窗标题 */

View File

@ -131,26 +131,37 @@ function fetchApi(): Promise<Record<string, any>> {
### Props
| 属性名 | 描述 | 类型 | 默认值 |
| --- | --- | --- | --- |
| modelValue(v-model) | 当前值 | `any` | - |
| component | 欲包装的组件(以下称为目标组件) | `Component` | - |
| numberToString | 是否将value从数字转为string | `boolean` | `false` |
| api | 获取数据的函数 | `(arg?: any) => Promise<OptionsItem[] \| Record<string, any>>` | - |
| params | 传递给api的参数 | `Record<string, any>` | - |
| resultField | 从api返回的结果中提取options数组的字段名 | `string` | - |
| labelField | label字段名 | `string` | `label` |
| childrenField | 子级数据字段名,需要层级数据的组件可用 | `string` | `` |
| valueField | value字段名 | `string` | `value` |
| optionsPropName | 目标组件接收options数据的属性名称 | `string` | `options` |
| modelPropName | 目标组件的双向绑定属性名默认为modelValue。部分组件可能为value | `string` | `modelValue` |
| immediate | 是否立即调用api | `boolean` | `true` |
| alwaysLoad | 每次`visibleEvent`事件发生时都重新请求数据 | `boolean` | `false` |
| beforeFetch | 在api请求之前的回调函数 | `AnyPromiseFunction<any, any>` | - |
| afterFetch | 在api请求之后的回调函数 | `AnyPromiseFunction<any, any>` | - |
| options | 直接传入选项数据也作为api返回空数据时的后备数据 | `OptionsItem[]` | - |
| visibleEvent | 触发重新请求数据的事件名 | `string` | - |
| loadingSlot | 目标组件的插槽名称,用来显示一个"加载中"的图标 | `string` | - |
| 属性名 | 描述 | 类型 | 默认值 | 版本要求 |
| --- | --- | --- | --- | --- |
| modelValue(v-model) | 当前值 | `any` | - | - |
| component | 欲包装的组件(以下称为目标组件) | `Component` | - | - |
| numberToString | 是否将value从数字转为string | `boolean` | `false` | - |
| api | 获取数据的函数 | `(arg?: any) => Promise<OptionsItem[] \| Record<string, any>>` | - | - |
| params | 传递给api的参数 | `Record<string, any>` | - | - |
| resultField | 从api返回的结果中提取options数组的字段名 | `string` | - | - |
| labelField | label字段名 | `string` | `label` | - |
| childrenField | 子级数据字段名,需要层级数据的组件可用 | `string` | `` | - |
| valueField | value字段名 | `string` | `value` | - |
| optionsPropName | 目标组件接收options数据的属性名称 | `string` | `options` | - |
| modelPropName | 目标组件的双向绑定属性名默认为modelValue。部分组件可能为value | `string` | `modelValue` | - |
| immediate | 是否立即调用api | `boolean` | `true` | - |
| alwaysLoad | 每次`visibleEvent`事件发生时都重新请求数据 | `boolean` | `false` | - |
| beforeFetch | 在api请求之前的回调函数 | `AnyPromiseFunction<any, any>` | - | - |
| afterFetch | 在api请求之后的回调函数 | `AnyPromiseFunction<any, any>` | - | - |
| options | 直接传入选项数据也作为api返回空数据时的后备数据 | `OptionsItem[]` | - | - |
| visibleEvent | 触发重新请求数据的事件名 | `string` | - | - |
| loadingSlot | 目标组件的插槽名称,用来显示一个"加载中"的图标 | `string` | - | - |
| autoSelect | 自动设置选项 | `'first' \| 'last' \| 'one'\| ((item: OptionsItem[]) => OptionsItem) \| false` | `false` | >5.5.4 |
#### autoSelect 自动设置选项
如果当前值为undefined在选项数据成功加载之后自动从备选项中选择一个作为当前值。默认值为`false`,即不自动选择选项。注意:该属性不应用于多选组件。可选值有:
- `"first"`:自动选择第一个选项
- `"last"`:自动选择最后一个选项
- `"one"`:有且仅有一个选项时,自动选择它
- `自定义函数`自定义选择逻辑函数的参数为options返回值为选择的选项
- `false`:不自动选择选项
### Methods
@ -158,3 +169,5 @@ function fetchApi(): Promise<Record<string, any>> {
| --- | --- | --- | --- |
| getComponentRef | 获取被包装的组件的实例 | ()=>T | >5.5.4 |
| updateParam | 设置接口请求参数将与params属性合并 | (newParams: Record<string, any>)=>void | >5.5.4 |
| getOptions | 获取已加载的选项数据 | ()=>OptionsItem[] | >5.5.4 |
| getValue | 获取当前值 | ()=>any | >5.5.4 |

View File

@ -127,13 +127,14 @@ const [Drawer, drawerApi] = useVbenDrawer({
除了上面的属性类型包含`slot`,还可以通过插槽来自定义弹窗的内容。
| 插槽名 | 描述 |
| -------------- | ------------------- |
| default | 默认插槽 - 弹窗内容 |
| prepend-footer | 取消按钮左侧 |
| append-footer | 取消按钮右侧 |
| close-icon | 关闭按钮图标 |
| extra | 额外内容(标题右侧) |
| 插槽名 | 描述 |
| -------------- | -------------------------------------------------- |
| default | 默认插槽 - 弹窗内容 |
| prepend-footer | 取消按钮左侧 |
| center-footer | 取消按钮和确认按钮中间(不使用 footer 插槽时有效) |
| append-footer | 确认按钮右侧 |
| close-icon | 关闭按钮图标 |
| extra | 额外内容(标题右侧) |
### drawerApi

View File

@ -310,7 +310,7 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
| actionWrapperClass | 表单操作区域class | `any` | - |
| handleReset | 表单重置回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
| handleSubmit | 表单提交回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
| handleValuesChange | 表单值变化回调 | `(values: Record<string, any>,) => void` | - |
| handleValuesChange | 表单值变化回调 | `(values: Record<string, any>, fieldsChanged: string[]) => void` | - |
| actionButtonsReverse | 调换操作按钮位置 | `boolean` | `false` |
| resetButtonOptions | 重置按钮组件参数 | `ActionButtonOptions` | - |
| submitButtonOptions | 提交按钮组件参数 | `ActionButtonOptions` | - |
@ -325,6 +325,12 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
| submitOnChange | 字段值改变时提交表单(内部防抖,这个属性一般用于表格的搜索表单) | `boolean` | false |
| compact | 是否紧凑模式(忽略为校验信息所预留的空间) | `boolean` | false |
::: tip handleValuesChange
`handleValuesChange` 回调函数的第一个参数`values`装载了表单改变后的当前值对象,第二个参数`fieldsChanged`是一个数组包含了所有被改变的字段名。注意第二个参数仅在v5.5.4(不含)以上版本可用并且传递的是已在schema中定义的字段名。如果你使用了字段映射并且需要检查是哪些字段发生了变化的话请注意该参数并不会包含映射后的字段名。
:::
::: tip fieldMappingTime
此属性用于将表单内的数组值映射成 2 个字段它应当传入一个数组数组的每一项是一个映射规则规则的第一个成员是一个字符串表示需要映射的字段名第二个成员是一个数组表示映射后的字段名第三个成员是一个可选的格式掩码用于格式化日期时间字段也可以提供一个格式化函数参数分别为当前值和当前字段名返回格式化后的值。如果明确地将格式掩码设为null则原值映射而不进行格式化适用于非日期时间字段。例如`[['timeRange', ['startTime', 'endTime'], 'YYYY-MM-DD']]``timeRange`应当是一个至少具有2个成员的数组类型的值。Form会将`timeRange`的值前两个值分别按照格式掩码`YYYY-MM-DD`格式化后映射到`startTime`和`endTime`字段上。每一项的第三个参数是一个可选的格式掩码,

View File

@ -60,7 +60,6 @@ Modal 内的内容一般业务中,会比较复杂,所以我们可以将 moda
- `VbenModal` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenModal参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
- 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenModal`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。
- 使用了`connectedComponent`参数时,可以配置`destroyOnClose`属性来决定当关闭弹窗时,是否要销毁`connectedComponent`组件(重新创建`connectedComponent`组件,这将会把其内部所有的变量、状态、数据等恢复到初始状态。)。
- 如果弹窗的默认行为不符合你的预期,可以在`src\bootstrap.ts`中修改`setDefaultModalProps`的参数来设置默认的属性如默认隐藏全屏按钮修改默认ZIndex等。
:::
@ -84,7 +83,7 @@ const [Modal, modalApi] = useVbenModal({
| --- | --- | --- | --- |
| appendToMain | 是否挂载到内容区域默认挂载到body | `boolean` | `false` |
| connectedComponent | 连接另一个Modal组件 | `Component` | - |
| destroyOnClose | 关闭时销毁`connectedComponent` | `boolean` | `false` |
| destroyOnClose | 关闭时销毁 | `boolean` | `false` |
| title | 标题 | `string\|slot` | - |
| titleTooltip | 标题提示信息 | `string\|slot` | - |
| description | 描述信息 | `string\|slot` | - |
@ -138,11 +137,12 @@ const [Modal, modalApi] = useVbenModal({
除了上面的属性类型包含`slot`,还可以通过插槽来自定义弹窗的内容。
| 插槽名 | 描述 |
| -------------- | ------------------- |
| default | 默认插槽 - 弹窗内容 |
| prepend-footer | 取消按钮左侧 |
| append-footer | 取消按钮右侧 |
| 插槽名 | 描述 |
| -------------- | -------------------------------------------------- |
| default | 默认插槽 - 弹窗内容 |
| prepend-footer | 取消按钮左侧 |
| center-footer | 取消按钮和确认按钮中间(不使用 footer 插槽时有效) |
| append-footer | 确认按钮右侧 |
### modalApi

View File

@ -1,6 +1,10 @@
<script lang="ts" setup>
import { h, ref } from 'vue';
import { alert, confirm, VbenButton } from '@vben/common-ui';
import { Checkbox, message } from 'ant-design-vue';
function showConfirm() {
confirm('This is an alert message')
.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() {
confirm({
beforeClose({ isConfirm }) {
@ -37,6 +69,7 @@ function showAsyncConfirm() {
<div class="flex gap-4">
<VbenButton @click="showConfirm">Confirm</VbenButton>
<VbenButton @click="showIconConfirm">Confirm With Icon</VbenButton>
<VbenButton @click="showfooterConfirm">Confirm With Footer</VbenButton>
<VbenButton @click="showAsyncConfirm">Async Confirm</VbenButton>
</div>
</template>

View File

@ -1,9 +1,9 @@
<script lang="ts" setup>
import { h } from 'vue';
import { alert, prompt, VbenButton } from '@vben/common-ui';
import { alert, prompt, useAlertContext, VbenButton } from '@vben/common-ui';
import { Input, RadioGroup } from 'ant-design-vue';
import { Input, RadioGroup, Select } from 'ant-design-vue';
import { BadgeJapaneseYen } from 'lucide-vue-next';
function showPrompt() {
@ -18,18 +18,32 @@ function showPrompt() {
});
}
function showSelectPrompt() {
function showSlotsPrompt() {
prompt({
component: Input,
componentProps: {
placeholder: '请输入',
prefix: '充值金额',
type: 'number',
component: () => {
// setup
const { doConfirm } = useAlertContext();
return h(
Input,
{
onKeydown(e: KeyboardEvent) {
if (e.key === 'Enter') {
e.preventDefault();
//
doConfirm();
}
},
placeholder: '请输入',
prefix: '充值金额:',
type: 'number',
},
{
addonAfter: () => h(BadgeJapaneseYen),
},
);
},
componentSlots: {
addonAfter: () => h(BadgeJapaneseYen),
},
content: '此弹窗演示了如何使用componentSlots传递自定义插槽',
content:
'此弹窗演示了如何使用自定义插槽并且可以使用useAlertContext获取到弹窗的上下文。\n在输入框中按下回车键会触发确认操作。',
icon: 'question',
modelPropName: 'value',
}).then((val) => {
@ -37,6 +51,29 @@ function showSelectPrompt() {
});
}
function showSelectPrompt() {
prompt({
component: Select,
componentProps: {
options: [
{ label: 'Option A', value: 'Option A' },
{ label: 'Option B', value: 'Option B' },
{ label: 'Option C', value: 'Option C' },
],
placeholder: '请选择',
// bodypointer-eventsnone
popupClassName: 'pointer-events-auto',
},
content: '此弹窗演示了如何使用component传递自定义组件',
icon: 'question',
modelPropName: 'value',
}).then((val) => {
if (val) {
alert(`你选择了${val}`);
}
});
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
@ -44,7 +81,6 @@ function sleep(ms: number) {
function showAsyncPrompt() {
prompt({
async beforeClose(scope) {
console.log(scope);
if (scope.isConfirm) {
if (scope.value) {
// false
@ -75,6 +111,7 @@ function showAsyncPrompt() {
<template>
<div class="flex gap-4">
<VbenButton @click="showPrompt">Prompt</VbenButton>
<VbenButton @click="showSlotsPrompt"> Prompt With slots </VbenButton>
<VbenButton @click="showSelectPrompt">Prompt With Select</VbenButton>
<VbenButton @click="showAsyncPrompt">Prompt With Async</VbenButton>
</div>

View File

@ -62,7 +62,7 @@ async function handleReset(e: Event) {
e?.stopPropagation();
const props = unref(rootProps);
const values = toRaw(props.formApi?.getValues());
const values = toRaw(await props.formApi?.getValues());
if (isFunction(props.handleReset)) {
await props.handleReset?.(values);

View File

@ -295,6 +295,7 @@ export class FormApi {
return true;
});
const filteredFields = fieldMergeFn(fields, form.values);
this.handleStringToArrayFields(filteredFields);
form.setValues(filteredFields, shouldValidate);
}
@ -304,6 +305,7 @@ export class FormApi {
const form = await this.getForm();
await form.submitForm();
const rawValues = toRaw(await this.getValues());
this.handleArrayToStringFields(rawValues);
await this.state?.handleSubmit?.(rawValues);
return rawValues;
@ -392,10 +394,53 @@ export class FormApi {
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>) => {
const values = { ...originValues };
const fieldMappingTime = this.state?.fieldMappingTime;
this.handleStringToArrayFields(values);
if (!fieldMappingTime || !Array.isArray(fieldMappingTime)) {
return values;
}
@ -441,6 +486,80 @@ export class FormApi {
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() {
const currentSchema = this.state?.schema ?? [];
const prevSchema = this.prevState?.schema ?? [];

View File

@ -232,6 +232,12 @@ export type FieldMappingTime = [
)?,
][];
export type ArrayToStringFields = Array<
| [string[], string?] // 嵌套数组格式,可选分隔符
| string // 单个字段,使用默认分隔符
| string[] // 简单数组格式,最后一个元素可以是分隔符
>;
export interface FormSchema<
T extends BaseFormComponentType = BaseFormComponentType,
> extends FormCommonConfig {
@ -266,6 +272,10 @@ export interface FormFieldProps extends FormSchema {
export interface FormRenderProps<
T extends BaseFormComponentType = BaseFormComponentType,
> {
/**
* 使","
*/
arrayToStringFields?: ArrayToStringFields;
/**
* showCollapseButton=true下生效
*/
@ -296,6 +306,10 @@ export interface FormRenderProps<
*
*/
componentMap: Record<BaseFormComponentType, Component>;
/**
*
*/
fieldMappingTime?: FieldMappingTime;
/**
*
*/
@ -308,10 +322,15 @@ export interface FormRenderProps<
*
*/
schema?: FormSchema<T>[];
/**
* /
*/
showCollapseButton?: boolean;
/**
*
*/
/**
*
* @default "grid-cols-1"
@ -339,6 +358,11 @@ export interface VbenFormProps<
* class
*/
actionWrapperClass?: ClassType;
/**
* 使","
*/
arrayToStringFields?: ArrayToStringFields;
/**
*
*/
@ -354,11 +378,15 @@ export interface VbenFormProps<
/**
*
*/
handleValuesChange?: (values: Record<string, any>) => void;
handleValuesChange?: (
values: Record<string, any>,
fieldsChanged: string[],
) => void;
/**
*
*/
resetButtonOptions?: ActionButtonOptions;
/**
*
* @default true

View File

@ -1,12 +1,13 @@
<script setup lang="ts">
import type { Recordable } from '@vben-core/typings';
import type { ExtendedFormApi, VbenFormProps } from './types';
// import { toRaw, watch } from 'vue';
import { nextTick, onMounted, watch } from 'vue';
// import { isFunction } from '@vben-core/shared/utils';
import { useForwardPriorityValues } from '@vben-core/composables';
import { cloneDeep } from '@vben-core/shared/utils';
import { cloneDeep, get, isEqual, set } from '@vben-core/shared/utils';
import { useDebounceFn } from '@vueuse/core';
@ -61,16 +62,46 @@ function handleKeyDownEnter(event: KeyboardEvent) {
}
const handleValuesChangeDebounced = useDebounceFn(async () => {
forward.value.handleValuesChange?.(
cloneDeep(await forward.value.formApi.getValues()),
);
state.value.submitOnChange && forward.value.formApi?.validateAndSubmitForm();
}, 300);
const valuesCache: Recordable<any> = {};
onMounted(async () => {
// form.values
await nextTick();
watch(() => form.values, handleValuesChangeDebounced, { deep: true });
watch(
() => form.values,
async (newVal) => {
if (forward.value.handleValuesChange) {
const fields = state.value.schema?.map((item) => {
return item.fieldName;
});
if (fields && fields.length > 0) {
const changedFields: string[] = [];
fields.forEach((field) => {
const newFieldValue = get(newVal, field);
const oldFieldValue = get(valuesCache, field);
if (!isEqual(newFieldValue, oldFieldValue)) {
changedFields.push(field);
set(valuesCache, field, newFieldValue);
}
});
if (changedFields.length > 0) {
// handleValuesChange
forward.value.handleValuesChange(
cloneDeep(await forward.value.formApi.getValues()),
changedFields,
);
}
}
}
handleValuesChangeDebounced();
},
{ deep: true },
);
});
</script>

View File

@ -208,6 +208,8 @@ onBeforeUnmount(() => {
nsMenu.e('popup-container'),
is(rootMenu.theme, true),
opened ? '' : 'hidden',
'overflow-auto',
'max-h-[calc(var(--radix-hover-card-content-available-height)-20px)]',
]"
:content-props="contentProps"
:open="true"

View File

@ -7,7 +7,7 @@ import type { AlertProps, BeforeCloseScope, PromptProps } from './alert';
import { h, nextTick, ref, render } from 'vue';
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 Alert from './alert.vue';
@ -146,11 +146,7 @@ export async function vbenPrompt<T = any>(
const inputComponentRef = ref<null | VNode>(null);
const staticContents: Component[] = [];
if (isString(content)) {
staticContents.push(h('span', content));
} else if (content) {
staticContents.push(content as Component);
}
staticContents.push(h(VbenRenderContent, { content, renderBr: true }));
const modelPropName = _modelPropName || 'modelValue';
const componentProps = { ..._componentProps };

View File

@ -2,6 +2,8 @@ import type { Component, VNode, VNodeArrayChildren } from 'vue';
import type { Recordable } from '@vben-core/typings';
import { createContext } from '@vben-core/shadcn-ui';
export type IconType = 'error' | 'info' | 'question' | 'success' | 'warning';
export type BeforeCloseScope = {
@ -34,8 +36,14 @@ export type AlertProps = {
contentClass?: string;
/** 执行beforeClose回调期间在内容区域显示一个loading遮罩*/
contentMasking?: boolean;
/** 弹窗底部内容(与按钮在同一个容器中) */
footer?: Component | string;
/** 弹窗的图标(在标题的前面) */
icon?: Component | IconType;
/**
*
*/
overlayBlur?: number;
/** 是否显示取消按钮 */
showCancel?: boolean;
/** 弹窗标题 */
@ -64,3 +72,28 @@ export type PromptProps<T = any> = {
/** 输入组件的值属性名 */
modelPropName?: string;
} & 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;
}

View File

@ -3,7 +3,7 @@ import type { Component } from 'vue';
import type { AlertProps } from './alert';
import { computed, h, nextTick, ref, watch } from 'vue';
import { computed, h, nextTick, ref } from 'vue';
import { useSimpleLocale } from '@vben-core/composables';
import {
@ -28,6 +28,8 @@ import {
import { globalShareState } from '@vben-core/shared/global-state';
import { cn } from '@vben-core/shared/utils';
import { provideAlertContext } from './alert';
const props = withDefaults(defineProps<AlertProps>(), {
bordered: true,
buttonAlign: 'end',
@ -39,14 +41,12 @@ const open = defineModel<boolean>('open', { default: false });
const { $t } = useSimpleLocale();
const components = globalShareState.getComponents();
const isConfirm = ref(false);
watch(open, async (val) => {
await nextTick();
if (val) {
isConfirm.value = false;
} else {
emits('closed', isConfirm.value);
}
});
function onAlertClosed() {
emits('closed', isConfirm.value);
isConfirm.value = false;
}
const getIconRender = computed(() => {
let iconRender: Component | null = null;
if (props.icon) {
@ -89,6 +89,23 @@ const getIconRender = computed(() => {
}
return iconRender;
});
function doCancel() {
isConfirm.value = false;
handleOpenChange(false);
}
function doConfirm() {
isConfirm.value = true;
handleOpenChange(false);
emits('confirm');
}
provideAlertContext({
doCancel,
doConfirm,
});
function handleConfirm() {
isConfirm.value = true;
emits('confirm');
@ -100,6 +117,7 @@ function handleCancel() {
const loading = ref(false);
async function handleOpenChange(val: boolean) {
await nextTick();
if (!val && props.beforeClose) {
loading.value = true;
try {
@ -120,15 +138,16 @@ async function handleOpenChange(val: boolean) {
<AlertDialogContent
:open="open"
:centered="centered"
:overlay-blur="overlayBlur"
@opened="emits('opened')"
@closed="onAlertClosed"
:class="
cn(
containerClass,
'left-0 right-0 top-[10vh] mx-auto flex max-h-[80%] flex-col p-0 duration-300 sm:rounded-[var(--radius)] md:w-[520px] md:max-w-[80%]',
'left-0 right-0 mx-auto flex max-h-[80%] flex-col p-0 duration-300 sm:rounded-[var(--radius)] md:w-[520px] md:max-w-[80%]',
{
'border-border border': bordered,
'shadow-3xl': !bordered,
'top-1/2 !-translate-y-1/2': centered,
},
)
"
@ -152,12 +171,16 @@ async function handleOpenChange(val: boolean) {
</div>
</AlertDialogTitle>
<AlertDialogDescription>
<div class="m-4 mb-6 min-h-[30px]">
<div class="m-4 min-h-[30px]">
<VbenRenderContent :content="content" render-br />
</div>
<VbenLoading v-if="loading && contentMasking" :spinning="loading" />
</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>
<component
:is="components.DefaultButton || VbenButton"

View File

@ -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 {
vbenAlert as alert,

View File

@ -9,7 +9,11 @@ vi.mock('@vben-core/shared/store', () => {
return {
isFunction: (fn: any) => typeof fn === 'function',
Store: class {
get state() {
return this._state;
}
private _state: DrawerState;
private options: any;
constructor(initialState: DrawerState, options: any) {
@ -25,10 +29,6 @@ vi.mock('@vben-core/shared/store', () => {
this._state = fn(this._state);
this.options.onUpdate();
}
get state() {
return this._state;
}
},
};
});
@ -54,7 +54,6 @@ describe('drawerApi', () => {
});
it('should close the drawer if onBeforeClose allows it', () => {
drawerApi.open();
drawerApi.close();
expect(drawerApi.store.state.isOpen).toBe(false);
});

View File

@ -86,7 +86,8 @@ export class DrawerApi {
}
/**
*
*
* @description onBeforeClose onBeforeClose false
*/
async close() {
// 通过 onBeforeClose 钩子函数来判断是否允许关闭弹窗

View File

@ -274,7 +274,7 @@ const getAppendTo = computed(() => {
{{ cancelText || $t('cancel') }}
</slot>
</component>
<slot name="center-footer"></slot>
<component
:is="components.PrimaryButton || VbenButton"
v-if="showConfirmButton"

View File

@ -44,6 +44,7 @@ export class ModalApi {
confirmDisabled: false,
confirmLoading: false,
contentClass: '',
destroyOnClose: true,
draggable: false,
footer: true,
footerClass: '',

View File

@ -60,6 +60,10 @@ export interface ModalProps {
*
*/
description?: string;
/**
*
*/
destroyOnClose?: boolean;
/**
*
* @default false
@ -153,10 +157,6 @@ export interface ModalApiOptions extends ModalState {
*
*/
connectedComponent?: Component;
/**
* 使 connectedComponent
*/
destroyOnClose?: boolean;
/**
* false
* @returns

View File

@ -1,7 +1,7 @@
<script lang="ts" setup>
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 {
useIsMobile,
@ -34,6 +34,7 @@ interface Props extends ModalProps {
const props = withDefaults(defineProps<Props>(), {
appendToMain: false,
destroyOnClose: true,
modalApi: undefined,
});
@ -67,6 +68,7 @@ const {
confirmText,
contentClass,
description,
destroyOnClose,
draggable,
footer: showFooter,
footerClass,
@ -100,10 +102,15 @@ const { dragging, transform } = useModalDraggable(
shouldDraggable,
);
const firstOpened = ref(false);
const isClosed = ref(true);
watch(
() => state?.value?.isOpen,
async (v) => {
if (v) {
isClosed.value = false;
if (!firstOpened.value) firstOpened.value = true;
await nextTick();
if (!contentRef.value) return;
const innerContentRef = contentRef.value.getContentRef();
@ -113,6 +120,7 @@ watch(
dialogRef.value.style.transform = `translate(${offsetX}px, ${offsetY}px)`;
}
},
{ immediate: true },
);
watch(
@ -176,6 +184,15 @@ const getAppendTo = computed(() => {
? `#${ELEMENT_ID_MAIN_CONTENT}>div:not(.absolute)>div`
: undefined;
});
const getForceMount = computed(() => {
return !unref(destroyOnClose) && unref(firstOpened);
});
function handleClosed() {
isClosed.value = true;
props.modalApi?.onClosed();
}
</script>
<template>
<Dialog
@ -197,9 +214,11 @@ const getAppendTo = computed(() => {
shouldFullscreen,
'top-1/2 !-translate-y-1/2': centered && !shouldFullscreen,
'duration-300': !dragging,
hidden: isClosed,
},
)
"
:force-mount="getForceMount"
:modal="modal"
:open="state?.isOpen"
:show-close="closable"
@ -207,7 +226,7 @@ const getAppendTo = computed(() => {
:overlay-blur="overlayBlur"
close-class="top-3"
@close-auto-focus="handleFocusOutside"
@closed="() => modalApi?.onClosed()"
@closed="handleClosed"
:close-disabled="submitting"
@escape-key-down="escapeKeyDown"
@focus-outside="handleFocusOutside"
@ -302,7 +321,7 @@ const getAppendTo = computed(() => {
{{ cancelText || $t('cancel') }}
</slot>
</component>
<slot name="center-footer"></slot>
<component
:is="components.PrimaryButton || VbenButton"
v-if="showConfirmButton"

View File

@ -1,14 +1,6 @@
import type { ExtendedModalApi, ModalApiOptions, ModalProps } from './modal';
import {
defineComponent,
h,
inject,
nextTick,
provide,
reactive,
ref,
} from 'vue';
import { defineComponent, h, inject, nextTick, provide, reactive } from 'vue';
import { useStore } from '@vben-core/shared/store';
@ -32,7 +24,6 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
const { connectedComponent } = options;
if (connectedComponent) {
const extendedApi = reactive({});
const isModalReady = ref(true);
const Modal = defineComponent(
(props: TParentModalProps, { attrs, slots }) => {
provide(USER_MODAL_INJECT_KEY, {
@ -42,11 +33,6 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
Object.setPrototypeOf(extendedApi, api);
},
options,
async reCreateModal() {
isModalReady.value = false;
await nextTick();
isModalReady.value = true;
},
});
checkProps(extendedApi as ExtendedModalApi, {
...props,
@ -55,7 +41,7 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
});
return () =>
h(
isModalReady.value ? connectedComponent : 'div',
connectedComponent,
{
...props,
...attrs,
@ -84,14 +70,6 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
injectData.options?.onOpenChange?.(isOpen);
};
const onClosed = mergedOptions.onClosed;
mergedOptions.onClosed = () => {
onClosed?.();
if (mergedOptions.destroyOnClose) {
injectData.reCreateModal?.();
}
};
const api = new ModalApi(mergedOptions);
const extendedApi: ExtendedModalApi = api as never;

View File

@ -31,12 +31,11 @@ export default defineComponent({
if (props.renderBr && isString(props.content)) {
const lines = props.content.split('\n');
const result = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
result.push(h('span', { key: i }, line));
if (i < lines.length - 1) {
result.push(h('br'));
}
for (const [i, line] of lines.entries()) {
result.push(h('p', { key: i }, line));
// if (i < lines.length - 1) {
// result.push(h('br'));
// }
}
return result;
} else {

View File

@ -39,6 +39,14 @@ const isAtRight = ref(false);
const isAtBottom = ref(false);
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 showShadowBottom = computed(() => props.shadow && props.shadowBottom);
const showShadowLeft = computed(() => props.shadow && props.shadowLeft);
@ -60,14 +68,18 @@ function handleScroll(event: Event) {
const target = event.target as HTMLElement;
const scrollTop = target?.scrollTop ?? 0;
const scrollLeft = target?.scrollLeft ?? 0;
const offsetHeight = target?.offsetHeight ?? 0;
const offsetWidth = target?.offsetWidth ?? 0;
const clientHeight = target?.clientHeight ?? 0;
const clientWidth = target?.clientWidth ?? 0;
const scrollHeight = target?.scrollHeight ?? 0;
const scrollWidth = target?.scrollWidth ?? 0;
isAtTop.value = scrollTop <= 0;
isAtLeft.value = scrollLeft <= 0;
isAtBottom.value = scrollTop + offsetHeight >= scrollHeight;
isAtRight.value = scrollLeft + offsetWidth >= scrollWidth;
isAtBottom.value =
Math.abs(scrollTop) + clientHeight >=
scrollHeight - ARRIVED_STATE_THRESHOLD_PIXELS;
isAtRight.value =
Math.abs(scrollLeft) + clientWidth >=
scrollWidth - ARRIVED_STATE_THRESHOLD_PIXELS;
emit('scrollAt', {
bottom: isAtBottom.value,

View File

@ -61,7 +61,7 @@ defineExpose({
<template>
<AlertDialogPortal>
<Transition name="fade">
<Transition name="fade" appear>
<AlertDialogOverlay
v-if="open && modal"
:style="{
@ -80,7 +80,17 @@ defineExpose({
v-bind="forwarded"
:class="
cn(
'z-popup bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%] w-full p-6 shadow-lg outline-none sm:rounded-xl',
'z-popup bg-background w-full p-6 shadow-lg outline-none sm:rounded-xl',
'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
{
'data-[state=open]:slide-in-from-top-[48%] data-[state=closed]:slide-out-to-top-[48%]':
!centered,
'data-[state=open]:slide-in-from-top-[98%] data-[state=closed]:slide-out-to-top-[148%]':
centered,
'top-[10vh]': !centered,
'top-1/2 -translate-y-1/2': centered,
},
props.class,
)
"

View File

@ -54,6 +54,20 @@ interface Props {
visibleEvent?: string;
/** 组件的v-model属性名默认为modelValue。部分组件可能为value */
modelPropName?: string;
/**
* 自动选择
* - `first`自动选择第一个选项
* - `last`自动选择最后一个选项
* - `one`: 当请求的结果只有一个选项时自动选择该选项
* - 函数自定义选择逻辑函数的参数为请求的结果数组返回值为选择的选项
* - false不自动选择(默认)
*/
autoSelect?:
| 'first'
| 'last'
| 'one'
| ((item: OptionsItem[]) => OptionsItem)
| false;
}
defineOptions({ name: 'ApiComponent', inheritAttrs: false });
@ -74,6 +88,7 @@ const props = withDefaults(defineProps<Props>(), {
afterFetch: undefined,
modelPropName: 'modelValue',
api: undefined,
autoSelect: false,
options: () => [],
});
@ -81,7 +96,7 @@ const emit = defineEmits<{
optionsChange: [OptionsItem[]];
}>();
const modelValue = defineModel({ default: '' });
const modelValue = defineModel<any>({ default: undefined });
const attrs = useAttrs();
const innerParams = ref({});
@ -194,10 +209,43 @@ watch(
);
function emitChange() {
if (
modelValue.value === undefined &&
props.autoSelect &&
unref(getOptions).length > 0
) {
let firstOption;
if (isFunction(props.autoSelect)) {
firstOption = props.autoSelect(unref(getOptions));
} else {
switch (props.autoSelect) {
case 'first': {
firstOption = unref(getOptions)[0];
break;
}
case 'last': {
firstOption = unref(getOptions)[unref(getOptions).length - 1];
break;
}
case 'one': {
if (unref(getOptions).length === 1) {
firstOption = unref(getOptions)[0];
}
break;
}
}
}
if (firstOption) modelValue.value = firstOption.value;
}
emit('optionsChange', unref(getOptions));
}
const componentRef = ref();
defineExpose({
/** 获取options数据 */
getOptions: () => unref(getOptions),
/** 获取当前值 */
getValue: () => unref(modelValue),
/** 获取被包装的组件实例 */
getComponentRef: <T = any,>() => componentRef.value as T,
/** 更新Api参数 */

View File

@ -2,6 +2,8 @@ import { addIcon } from '@vben-core/icons';
import schedule from '@iconify/icons-akar-icons/schedule';
import settingOutline from '@iconify/icons-ant-design/setting-outlined';
import antdTool from '@iconify/icons-ant-design/tool-outlined';
import UserAntd from '@iconify/icons-ant-design/user-outlined';
import Operation from '@iconify/icons-arcticons/one-hand-operation';
import BaseLineHousesFill from '@iconify/icons-bi/houses-fill';
import BxPackage from '@iconify/icons-bx/package';
@ -39,11 +41,15 @@ import workflowOutline from '@iconify/icons-mdi/workflow-outline';
import DepartmentLine from '@iconify/icons-mingcute/department-line';
import profileLine from '@iconify/icons-mingcute/profile-line';
import UserDuotone from '@iconify/icons-ph/user-duotone';
import userList from '@iconify/icons-ph/user-list';
import users from '@iconify/icons-ph/users-light';
import insatnceLine from '@iconify/icons-ri/instance-line';
import todoLine from '@iconify/icons-ri/todo-line';
import Authy from '@iconify/icons-simple-icons/authy';
import FolderWithFilesOutline from '@iconify/icons-solar/folder-with-files-outline';
import monitorBoldDuotone from '@iconify/icons-solar/monitor-bold-duotone';
import monitorCameraOutlined from '@iconify/icons-solar/monitor-camera-outline';
import monitorPhoneOutlined from '@iconify/icons-solar/monitor-smartphone-outline';
import InterfaceLoginDialPadFingerPasswordDialPadDotFinger from '@iconify/icons-streamline/interface-login-dial-pad-finger-password-dial-pad-dot-finger';
import categoryPlus from '@iconify/icons-tabler/category-plus';
import code from '@iconify/icons-tabler/code';
@ -53,6 +59,7 @@ import code from '@iconify/icons-tabler/code';
*/
addIcon('eos-icons:system-group', SystemGroup);
addIcon('ph:user-duotone', UserDuotone);
addIcon('ant-design:user-outlined', UserAntd);
addIcon('eos-icons:role-binding-outlined', RoleBindingOutlined);
addIcon('ic:sharp-menu', MenuSharp);
addIcon('mingcute:department-line', DepartmentLine);
@ -68,15 +75,20 @@ addIcon(
);
addIcon('solar:folder-with-files-outline', FolderWithFilesOutline);
addIcon('simple-icons:authy', Authy);
addIcon('solar:monitor-smartphone-outline', monitorPhoneOutlined);
addIcon('ic:baseline-house', BaseLineHouse);
addIcon('ph:users-light', users);
addIcon('bi:houses-fill', BaseLineHousesFill);
addIcon('ph:user-list', userList);
addIcon('bx:package', BxPackage);
addIcon('solar:monitor-bold-duotone', monitorBoldDuotone);
addIcon('solar:monitor-camera-outline', monitorCameraOutlined);
addIcon('material-symbols:generating-tokens-outline', generatingTokensOutline);
addIcon('devicon:redis-wordmark', redisWordmark);
addIcon('devicon:spring-wordmark', springWordmark);
addIcon('akar-icons:schedule', schedule);
addIcon('mdi:tools', tools);
addIcon('ant-design:tool-outlined', antdTool);
addIcon('tabler:code', code);
addIcon('flat-color-icons:plus', plus);
addIcon('devicon:vscode', vscode);

View File

@ -0,0 +1,27 @@
<?xml version="1.0" standalone="no"?>
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="715.000000pt" height="697.000000pt" viewBox="0 0 715.000000 697.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,697.000000) scale(0.100000,-0.100000)"
fill="#1366ff" stroke="none">
<path d="M3430 6914 c-243 -21 -310 -28 -410 -45 -1406 -237 -2524 -1268
-2869 -2644 -76 -305 -104 -534 -104 -850 -1 -331 27 -558 104 -867 99 -394
260 -760 479 -1091 139 -209 251 -347 425 -526 298 -305 620 -538 992 -718 83
-40 157 -73 164 -73 6 0 54 -13 105 -30 447 -143 922 -37 1384 309 41 31 129
108 194 171 101 97 124 126 152 185 156 339 146 690 -32 1109 -86 201 -259
461 -433 650 -30 34 -107 96 -171 140 -245 167 -437 321 -543 434 -63 68 -161
219 -187 289 -28 75 -36 175 -20 244 18 78 72 187 120 242 41 46 42 48 35 100
-16 117 -178 620 -206 638 -6 3 -33 9 -60 12 -105 13 -183 69 -227 166 -21 45
-24 64 -20 121 11 155 135 260 292 248 183 -15 301 -192 237 -358 -12 -30 -21
-65 -21 -77 0 -31 82 -185 142 -266 139 -188 300 -276 484 -264 106 6 133 20
164 82 23 45 25 62 25 170 0 77 -7 150 -19 205 -25 116 -113 384 -137 414 -11
13 -41 32 -68 41 -147 49 -233 167 -233 321 0 131 72 243 195 300 78 36 190
34 272 -5 35 -16 78 -48 103 -75 103 -112 118 -244 45 -394 -33 -67 -30 -81
51 -246 182 -368 522 -768 1011 -1187 392 -337 649 -654 849 -1051 156 -310
241 -579 302 -963 10 -61 18 -197 21 -346 6 -260 -3 -395 -38 -593 -11 -60
-18 -110 -16 -112 8 -8 317 317 401 421 189 235 386 566 504 845 130 308 227
689 264 1040 18 173 15 547 -6 720 -132 1110 -735 2061 -1673 2640 -133 82
-374 205 -522 265 -297 121 -671 213 -991 245 -114 11 -438 20 -510 14z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -15,6 +15,7 @@ const SvgMaxKeyIcon = createIconifyIcon('svg:max-key');
const SvgTopiamIcon = createIconifyIcon('svg:topiam');
const SvgWechatIcon = createIconifyIcon('svg:wechat');
const SvgQQIcon = createIconifyIcon('svg:qq');
const SvgSnailJobIcon = createIconifyIcon('svg:snail-job');
export {
SvgAntdvLogoIcon,
@ -28,6 +29,7 @@ export {
SvgDownloadIcon,
SvgMaxKeyIcon,
SvgQQIcon,
SvgSnailJobIcon,
SvgTopiamIcon,
SvgWechatIcon,
};

View File

@ -212,7 +212,12 @@ setupVbenVxeTable({
Popconfirm,
{
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',
title: $t('ui.actionTitle.delete', [attrs?.nameTitle || '']),

View File

@ -30,5 +30,6 @@ function lockDrawer() {
<Button type="primary" @click="lockDrawer">锁定抽屉状态</Button>
<!-- <template #prepend-footer> slot </template> -->
<!-- <template #append-footer> prepend slot </template> -->
<!-- <template #center-footer> center slot </template> -->
</Drawer>
</template>

View File

@ -42,6 +42,9 @@ const [BaseForm, baseFormApi] = useVbenForm({
fieldMappingTime: [['rangePicker', ['startTime', 'endTime'], 'YYYY-MM-DD']],
//
handleSubmit: onSubmit,
handleValuesChange(_values, fieldsChanged) {
message.info(`表单以下字段发生变化:${fieldsChanged.join('')}`);
},
// labelinputvertical
// labelinput
@ -74,6 +77,7 @@ const [BaseForm, baseFormApi] = useVbenForm({
},
//
api: getAllMenusApi,
autoSelect: 'first',
},
//
fieldName: 'api',

View File

@ -16,15 +16,18 @@ const [Modal, modalApi] = useVbenModal({
},
onOpenChange(isOpen) {
if (isOpen) {
handleUpdate(10);
handleUpdate();
}
},
});
function handleUpdate(len: number) {
function handleUpdate(len?: number) {
modalApi.setState({ confirmDisabled: true, loading: true });
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 });
}, 2000);
}
@ -40,7 +43,7 @@ function handleUpdate(len: number) {
{{ item }}
</div>
<template #prepend-footer>
<Button type="link" @click="handleUpdate(6)">点击更新数据</Button>
<Button type="link" @click="handleUpdate()">点击更新数据</Button>
</template>
</Modal>
</template>

View File

@ -24,7 +24,7 @@ const value = ref();
title="基础弹窗示例"
title-tooltip="标题提示内容"
>
此弹窗指定在内容区域打开
<Input v-model="value" placeholder="KeepAlive测试" />
此弹窗指定在内容区域打开并且在关闭之后弹窗内容不会被销毁
<Input v-model:value="value" placeholder="KeepAlive测试" />
</Modal>
</template>

View File

@ -138,6 +138,7 @@ function openConfirm() {
}, 1000);
});
},
centered: false,
content: '这是一个确认弹窗',
icon: 'question',
})
@ -160,6 +161,7 @@ async function openPrompt() {
componentProps: { placeholder: '不能吃芝士...' },
content: '中午吃了什么?',
icon: 'question',
overlayBlur: 3,
})
.then((res) => {
message.success(`用户输入了:${res}`);
@ -196,7 +198,7 @@ async function openPrompt() {
</template>
</Card>
<Card class="w-[300px]" title="指定容器">
<Card class="w-[300px]" title="指定容器+关闭后不销毁">
<p>在内容区域打开弹窗的示例</p>
<template #actions>
<Button type="primary" @click="openInContentModal">打开弹窗</Button>

View File

@ -94,8 +94,9 @@ export function useColumns(
},
{
field: 'createTime',
resizable: false,
title: $t('system.dept.createTime'),
width: 180,
width: 'auto',
},
{
field: 'remark',

View File

@ -37,7 +37,7 @@ const [Modal, modalApi] = useVbenModal({
const { valid } = await formApi.validate();
if (valid) {
modalApi.lock();
const data = formApi.getValues();
const data = await formApi.getValues();
try {
await (formData.value?.id
? updateDept(formData.value.id, data)

View File

@ -11,7 +11,7 @@ export function getMenuTypeOptions() {
value: 'catalog',
},
{ 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',
label: $t('system.menu.typeEmbedded'),

View File

@ -241,10 +241,10 @@ const schema: VbenFormSchema[] = [
component: 'Input',
dependencies: {
rules: (values) => {
return values.type === 'button' ? 'required' : null;
return values.type === 'action' ? 'required' : null;
},
show: (values) => {
return ['button', 'catalog', 'embedded', 'menu'].includes(values.type);
return ['action', 'catalog', 'embedded', 'menu'].includes(values.type);
},
triggerFields: ['type'],
},
@ -277,7 +277,7 @@ const schema: VbenFormSchema[] = [
},
dependencies: {
show: (values) => {
return values.type !== 'button';
return values.type !== 'action';
},
triggerFields: ['type'],
},
@ -295,7 +295,7 @@ const schema: VbenFormSchema[] = [
},
dependencies: {
show: (values) => {
return values.type !== 'button';
return values.type !== 'action';
},
triggerFields: ['type'],
},
@ -314,7 +314,7 @@ const schema: VbenFormSchema[] = [
},
dependencies: {
show: (values) => {
return values.type !== 'button';
return values.type !== 'action';
},
triggerFields: ['type'],
},
@ -325,7 +325,7 @@ const schema: VbenFormSchema[] = [
component: 'Divider',
dependencies: {
show: (values) => {
return !['button', 'link'].includes(values.type);
return !['action', 'link'].includes(values.type);
},
triggerFields: ['type'],
},
@ -372,7 +372,7 @@ const schema: VbenFormSchema[] = [
component: 'Checkbox',
dependencies: {
show: (values) => {
return !['button'].includes(values.type);
return !['action'].includes(values.type);
},
triggerFields: ['type'],
},
@ -402,7 +402,7 @@ const schema: VbenFormSchema[] = [
component: 'Checkbox',
dependencies: {
show: (values) => {
return !['button', 'link'].includes(values.type);
return !['action', 'link'].includes(values.type);
},
triggerFields: ['type'],
},
@ -417,7 +417,7 @@ const schema: VbenFormSchema[] = [
component: 'Checkbox',
dependencies: {
show: (values) => {
return !['button', 'link'].includes(values.type);
return !['action', 'link'].includes(values.type);
},
triggerFields: ['type'],
},

View File

@ -167,7 +167,7 @@ catalog:
unbuild: ^3.5.0
unplugin-element-plus: ^0.9.1
vee-validate: ^4.15.0
vite: ^6.2.5
vite: 6.2.5
vite-plugin-compression: ^0.5.1
vite-plugin-dts: ^4.5.3
vite-plugin-html: ^3.2.2

View File

@ -1,16 +1,16 @@
UPDATE sys_menu SET icon = 'eos-icons:system-group' WHERE menu_id = 1;
UPDATE sys_menu SET icon = 'solar:monitor-bold-duotone' WHERE menu_id = 2;
UPDATE sys_menu SET icon = 'mdi:tools' WHERE menu_id = 3;
UPDATE sys_menu SET icon = 'solar:monitor-camera-outline' WHERE menu_id = 2;
UPDATE sys_menu SET icon = 'ant-design:tool-outlined' WHERE menu_id = 3;
UPDATE sys_menu SET icon = 'flat-color-icons:plus' WHERE menu_id = 4;
UPDATE sys_menu SET icon = 'devicon:vscode' WHERE menu_id = 5;
UPDATE sys_menu SET icon = 'ic:baseline-house' WHERE menu_id = 6;
UPDATE sys_menu SET icon = 'ph:user-duotone' WHERE menu_id = 100;
UPDATE sys_menu SET icon = 'ph:users-light' WHERE menu_id = 6;
UPDATE sys_menu SET icon = 'ant-design:user-outlined' WHERE menu_id = 100;
UPDATE sys_menu SET icon = 'eos-icons:role-binding-outlined' WHERE menu_id = 101;
UPDATE sys_menu SET icon = 'ic:sharp-menu' WHERE menu_id = 102;
UPDATE sys_menu SET icon = 'mingcute:department-line' WHERE menu_id = 103;
UPDATE sys_menu SET icon = 'icon-park-outline:appointment' WHERE menu_id = 104;
UPDATE sys_menu SET icon = 'fluent-mdl2:dictionary' WHERE menu_id = 105;
UPDATE sys_menu SET icon = 'icon-park-twotone:setting-two' WHERE menu_id = 106;
UPDATE sys_menu SET icon = 'ant-design:setting-outlined' WHERE menu_id = 106;
UPDATE sys_menu SET icon = 'fe:notice-push' WHERE menu_id = 107;
UPDATE sys_menu SET icon = 'material-symbols:logo-dev-outline' WHERE menu_id = 108;
UPDATE sys_menu SET icon = 'material-symbols:generating-tokens-outline' WHERE menu_id = 109;
@ -19,10 +19,10 @@ UPDATE sys_menu SET icon = 'fluent:form-new-24-regular' WHERE menu_id = 114;
UPDATE sys_menu SET icon = 'tabler:code' WHERE menu_id = 115;
UPDATE sys_menu SET icon = 'devicon:spring-wordmark' WHERE menu_id = 117;
UPDATE sys_menu SET icon = 'solar:folder-with-files-outline' WHERE menu_id = 118;
UPDATE sys_menu SET icon = 'akar-icons:schedule' WHERE menu_id = 120;
UPDATE sys_menu SET icon = 'bi:houses-fill' WHERE menu_id = 121;
UPDATE sys_menu SET icon = 'svg:snail-job' WHERE menu_id = 120;
UPDATE sys_menu SET icon = 'ph:user-list' WHERE menu_id = 121;
UPDATE sys_menu SET icon = 'bx:package' WHERE menu_id = 122;
UPDATE sys_menu SET icon = 'simple-icons:authy' WHERE menu_id = 123;
UPDATE sys_menu SET icon = 'solar:monitor-smartphone-outline' WHERE menu_id = 123;
UPDATE sys_menu SET icon = 'arcticons:one-hand-operation' WHERE menu_id = 500;
UPDATE sys_menu SET icon = 'streamline:interface-login-dial-pad-finger-password-dial-pad-dot-finger' WHERE menu_id = 501;