41 Commits

Author SHA1 Message Date
dap
7d8416890b docs: changelog 2025-04-16 21:53:29 +08:00
dap
2e2ffcd59e Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-04-16 21:33:11 +08:00
dap
2046bfa846 chore: 暂时锁定vite版本 会导致i18n插件打包失败 2025-04-16 19:57:07 +08:00
dap
0446adf778 refactor: 菜单图标更新 2025-04-16 17:38:11 +08:00
Netfan
f7a4d13a4c fix: fixed arguments of callbacks in formApi (#5970)
* 修复 `handleValuesChange` 传递的参数不是处理后的表单值的问题

* 修复 `handleReset` 未能传递正确参数的问题
2025-04-16 14:11:04 +08:00
dap
e587256425 update: placeholder update 2025-04-16 13:53:58 +08:00
Netfan
0936861da1 feat: pass fieldsChanged into the handleValuesChange callback function (#5968)
* fieldsChanged(已被改变值的字段名)将传入handleValuesChange回调函数
2025-04-16 11:29:01 +08:00
ming4762
3318d76bab perf: improve destroyOnClose for VbenModal (#5964) 2025-04-16 11:28:36 +08:00
LinaBell
8f3881eabf perf: beforeClose of drawer support promise (#5932)
* perf: the beforeClose function of drawer is consistent with that of modal

* refactor: drawer test update
2025-04-16 11:27:13 +08:00
zhouda1fu
5252480b09 fix: missing await in department form(#5967) 2025-04-16 11:22:59 +08:00
dap
f096dfc6e6 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-04-16 10:09:48 +08:00
Netfan
d18f56177c docs: update alert and apiComponent docs (#5961) 2025-04-15 20:52:23 +08:00
wyc001122
333998b518 fix: determine if scrollbar has been totally scrolled (#5934)
* 修复在系统屏幕缩放比例不为100%的情况下,滚动组件对是否已滚动到边界的判断可能不正确的问题
2025-04-15 20:51:38 +08:00
ming4762
3fb4fba1cb fix: modal closing animation (#5960) 2025-04-15 18:49:57 +08:00
ming4762
c7e6210c8d feat: modal&drawer support center-footer slot (#5956) 2025-04-15 16:04:44 +08:00
lztb
d864085c13 feat: vben-form添加arrayToStringFields属性 (#5957)
* feat: vben-form添加arrayToStringFields属性

* feat: 修改handleArrayToStringFields和handleStringToArrayFields中嵌套数组格式的处理不一致

---------

Co-authored-by: 米山 <17726957223@189.cn>
2025-04-15 16:03:20 +08:00
Netfan
fcdc1a1602 feat: add more expose methods for apiComponent (#5958)
* 为ApiComponent组件添加getOptions和getValue导出方法。
2025-04-15 15:32:30 +08:00
Netfan
bf7496f0d5 feat: add useAlertContext for Alert component (#5947)
* 新增Alert的子组件中获取弹窗上下文的能力
2025-04-15 00:00:05 +08:00
Netfan
9700150653 fix: table actions in fixed column (#5945) 2025-04-14 19:56:52 +08:00
Netfan
f0e9e55af2 feat: alert support customize footer (#5940)
* Alert组件支持自定义footer
2025-04-14 11:48:21 +08:00
Netfan
ff88274554 fix: long navigation menu can be scrolled (#5939)
* 修复超长的导航菜单无法纵向滚动的问题
2025-04-14 11:18:33 +08:00
ming4762
afce9dc5c0 perf: improve destroyOnClose for VbenModal (#5935)
* perf: 优化Vben Modal destroyOnClose,解决destroyOnClose=false,Modal依旧会被销毁的问题

影响范围(重要):destroyOnClose默认为true,这会导致所有的modal都会默认渲染到body
radix-vue Dialog组件默认会销毁挂载的组件,所以即使destroyOnClose=false,Modal依旧会被销毁的问题
对于一些大表单重复渲染导致卡顿,ApiComponent也会频繁的加载数据

* fix: modal closing animation

---------

Co-authored-by: Netfan <netfan@foxmail.com>
2025-04-13 23:02:07 +08:00
ming4762
b5700bd0b1 perf: improve autoSelect of ApiComponent (#5936)
* fix: 修复autoSelect不生效的问题,props.valueField已经被omit了

* feat: ApiComponent autoSelect支持使用函数,可以满足灵活性要求更高的场景
2025-04-13 20:03:18 +08:00
dap
e085083e42 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-04-12 22:28:23 +08:00
dap
a47910f650 refactor: 所有表格操作列宽度调整为'auto', 这样会根据子元素宽度适配(比如没有分配权限的情况) 2025-04-12 15:01:28 +08:00
Netfan
a8c4786311 feat: api-component support autoSelect prop (#5931)
* feat: api-component support autoSelect prop

* docs: add version requirement
2025-04-12 14:02:35 +08:00
Netfan
2971ccc0b7 docs: docs modal z-index fixed, update alert docs (#5930) 2025-04-12 13:41:40 +08:00
dap
4ead56eaf1 fix: onClosed 2025-04-12 10:44:53 +08:00
dap
4fad8d77de refactor: 角色管理 auto 2025-04-12 10:42:16 +08:00
dap
9db1087d32 update: 岗位 useBeforeCloseDiff 2025-04-12 10:38:14 +08:00
Netfan
4a2c7b313f fix: alert animation (#5927) 2025-04-12 10:37:47 +08:00
dap
0f5fc5f54c fix: onClosed 2025-04-12 10:34:44 +08:00
dap
76108e7b8f refactor: 宽度设置为auto(根据子元素宽度动态变化) 2025-04-12 10:31:43 +08:00
dap
6018817906 chore: version动态获取 2025-04-12 10:20:12 +08:00
dap
7e4bdf7bd6 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-04-12 09:56:51 +08:00
dap
32117574f6 chore: version update 2025-04-12 09:54:50 +08:00
dap
a48dfa1de2 fix: 新增dictType不显示 2025-04-12 09:44:27 +08:00
Netfan
36bf6fc149 fix: builtin color change throttled in preference drawer (#5924)
修复偏好设置中的自定义主题色拖动选择颜色时页面会明显卡顿的问题
2025-04-12 01:44:08 +08:00
Netfan
f46ec30995 fix: theme mode follow the system only auto (#5923)
* 修复主题在未设置为auto时,仍然会跟随系统主题变化的问题。
2025-04-12 01:16:57 +08:00
Netfan
9bd5a190c2 fix: alert action button focus, fixed #5921 (#5922)
* 修复Alert组件的按钮焦点切换问题
2025-04-12 00:59:56 +08:00
zhang
86da3cedc2 chore: 导出框架自带的组件,方便独立页面使用 (#5876) 2025-04-09 16:16:56 +08:00
80 changed files with 886 additions and 320 deletions

View File

@@ -1,3 +1,14 @@
# 1.3.2
**REFACTOR**
- 所有表格操作列宽度调整为'auto', 这样会根据子元素宽度适配(比如没有分配权限的情况)
- 菜单图标更新了一部分 sql同步更新
**OTHER**
- 暂时锁死vite依赖 i18n会报错
# 1.3.1
**REFACTOR**

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/web-antd",
"version": "1.3.0",
"version": "1.3.2",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

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

@@ -2,6 +2,11 @@ import type { RouteRecordStringComponent } from '@vben/types';
import { $t } from '@vben/locales';
const {
version,
// vite inject-metadata 插件注入的全局变量
} = __VBEN_ADMIN_METADATA__ || {};
/**
* 该文件放非后台返回的路由 比如个人中心 等需要跳转显示的页面
*/
@@ -134,8 +139,8 @@ export const localMenuList: RouteRecordStringComponent[] = [
icon: 'lucide:book-open-text',
keepAlive: true,
title: '更新记录',
badge: '1.3.0',
badgeVariants: '#CC0033',
badge: `当前: ${version}`,
badgeVariants: 'bg-primary',
},
},
],

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

@@ -77,7 +77,7 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
const { dictCode, dictType } = drawerApi.getData() as DrawerProps;
isUpdate.value = !!dictCode;
formApi.setFieldValue('dictType', dictType);
await formApi.setFieldValue('dictType', dictType);
if (dictCode && isUpdate.value) {
const record = await dictDetailInfo(dictCode);

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

@@ -8,6 +8,7 @@ import { addFullName, cloneDeep } from '@vben/utils';
import { useVbenForm } from '#/adapter/form';
import { postAdd, postInfo, postUpdate } from '#/api/system/post';
import { getDeptTree } from '#/api/system/user';
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
import { drawerSchema } from './data';
@@ -50,8 +51,16 @@ async function setupDeptSelect() {
]);
}
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
{
initializedGetter: defaultFormValueGetter(formApi),
currentGetter: defaultFormValueGetter(formApi),
},
);
const [BasicDrawer, drawerApi] = useVbenDrawer({
onCancel: handleCancel,
onBeforeClose,
onClosed: handleClosed,
onConfirm: handleConfirm,
async onOpenChange(isOpen) {
if (!isOpen) {
@@ -67,31 +76,33 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
const record = await postInfo(id);
await formApi.setValues(record);
}
await markInitialized();
drawerApi.drawerLoading(false);
},
});
async function handleConfirm() {
try {
drawerApi.drawerLoading(true);
drawerApi.lock(true);
const { valid } = await formApi.validate();
if (!valid) {
return;
}
const data = cloneDeep(await formApi.getValues());
await (isUpdate.value ? postUpdate(data) : postAdd(data));
resetInitialized();
emit('reload');
await handleCancel();
drawerApi.close();
} catch (error) {
console.error(error);
} finally {
drawerApi.drawerLoading(false);
drawerApi.lock(false);
}
}
async function handleCancel() {
drawerApi.close();
async function handleClosed() {
await formApi.resetForm();
resetInitialized();
}
</script>

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

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

View File

@@ -226,7 +226,11 @@ function handleAssignRole(record: Role) {
</MenuItem>
</Menu>
</template>
<a-button size="small" type="link">
<a-button
size="small"
type="link"
v-access:code="'system:role:edit'"
>
{{ $t('pages.common.more') }}
</a-button>
</Dropdown>

View File

@@ -52,7 +52,7 @@ const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
const [BasicModal, modalApi] = useVbenModal({
fullscreenButton: false,
onBeforeClose,
onCancel: handleClosed,
onClosed: handleClosed,
onConfirm: handleConfirm,
onOpenChange: async (isOpen) => {
if (!isOpen) {

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

@@ -87,7 +87,7 @@ export const columns: VxeGridProps['columns'] = [
slots: { default: 'action' },
title: '操作',
resizable: false,
width: 180,
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

@@ -75,7 +75,7 @@ const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
const [BasicDrawer, modalApi] = useVbenModal({
onBeforeClose,
onCancel: handleClosed,
onClosed: handleClosed,
onConfirm: handleConfirm,
async onOpenChange(isOpen) {
if (!isOpen) {

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: '请选择',
// 弹窗会设置body的pointer-events为none这回影响下拉框的点击事件
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

@@ -2,14 +2,16 @@ import type { DeepPartial } from '@vben-core/typings';
import type { InitialOptions, Preferences } from './types';
import { markRaw, reactive, readonly, watch } from 'vue';
import { StorageManager } from '@vben-core/shared/cache';
import { isMacOs, merge } from '@vben-core/shared/utils';
import {
breakpointsTailwind,
useBreakpoints,
useDebounceFn,
} from '@vueuse/core';
import { markRaw, reactive, readonly, watch } from 'vue';
import { defaultPreferences } from './config';
import { updateCSSVariables } from './update-css-variables';
@@ -37,106 +39,6 @@ class PreferenceManager {
);
}
/**
* 保存偏好设置
* @param {Preferences} preference - 需要保存的偏好设置
*/
private _savePreferences(preference: Preferences) {
this.cache?.setItem(STORAGE_KEY, preference);
this.cache?.setItem(STORAGE_KEY_LOCALE, preference.app.locale);
this.cache?.setItem(STORAGE_KEY_THEME, preference.theme.mode);
}
/**
* 处理更新的键值
* 根据更新的键值执行相应的操作。
* @param {DeepPartial<Preferences>} updates - 部分更新的偏好设置
*/
private handleUpdates(updates: DeepPartial<Preferences>) {
const themeUpdates = updates.theme || {};
const appUpdates = updates.app || {};
if (themeUpdates && Object.keys(themeUpdates).length > 0) {
updateCSSVariables(this.state);
}
if (
Reflect.has(appUpdates, 'colorGrayMode') ||
Reflect.has(appUpdates, 'colorWeakMode')
) {
this.updateColorMode(this.state);
}
}
private initPlatform() {
const dom = document.documentElement;
dom.dataset.platform = isMacOs() ? 'macOs' : 'window';
}
/**
* 从缓存中加载偏好设置。如果缓存中没有找到对应的偏好设置,则返回默认偏好设置。
*/
private loadCachedPreferences() {
return this.cache?.getItem<Preferences>(STORAGE_KEY);
}
/**
* 加载偏好设置
* @returns {Preferences} 加载的偏好设置
*/
private loadPreferences(): Preferences {
return this.loadCachedPreferences() || { ...defaultPreferences };
}
/**
* 监听状态和系统偏好设置的变化。
*/
private setupWatcher() {
if (this.isInitialized) {
return;
}
// 监听断点,判断是否移动端
const breakpoints = useBreakpoints(breakpointsTailwind);
const isMobile = breakpoints.smaller('md');
watch(
() => isMobile.value,
(val) => {
this.updatePreferences({
app: { isMobile: val },
});
},
{ immediate: true },
);
// 监听系统主题偏好设置变化
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', ({ matches: isDark }) => {
this.updatePreferences({
theme: { mode: isDark ? 'dark' : 'light' },
});
});
}
/**
* 更新页面颜色模式(灰色、色弱)
* @param preference
*/
private updateColorMode(preference: Preferences) {
if (preference.app) {
const { colorGrayMode, colorWeakMode } = preference.app;
const dom = document.documentElement;
const COLOR_WEAK = 'invert-mode';
const COLOR_GRAY = 'grayscale-mode';
colorWeakMode
? dom.classList.add(COLOR_WEAK)
: dom.classList.remove(COLOR_WEAK);
colorGrayMode
? dom.classList.add(COLOR_GRAY)
: dom.classList.remove(COLOR_GRAY);
}
}
clearCache() {
[STORAGE_KEY, STORAGE_KEY_LOCALE, STORAGE_KEY_THEME].forEach((key) => {
this.cache?.removeItem(key);
@@ -220,6 +122,113 @@ class PreferenceManager {
this.handleUpdates(updates);
this.savePreferences(this.state);
}
/**
* 保存偏好设置
* @param {Preferences} preference - 需要保存的偏好设置
*/
private _savePreferences(preference: Preferences) {
this.cache?.setItem(STORAGE_KEY, preference);
this.cache?.setItem(STORAGE_KEY_LOCALE, preference.app.locale);
this.cache?.setItem(STORAGE_KEY_THEME, preference.theme.mode);
}
/**
* 处理更新的键值
* 根据更新的键值执行相应的操作。
* @param {DeepPartial<Preferences>} updates - 部分更新的偏好设置
*/
private handleUpdates(updates: DeepPartial<Preferences>) {
const themeUpdates = updates.theme || {};
const appUpdates = updates.app || {};
if (themeUpdates && Object.keys(themeUpdates).length > 0) {
updateCSSVariables(this.state);
}
if (
Reflect.has(appUpdates, 'colorGrayMode') ||
Reflect.has(appUpdates, 'colorWeakMode')
) {
this.updateColorMode(this.state);
}
}
private initPlatform() {
const dom = document.documentElement;
dom.dataset.platform = isMacOs() ? 'macOs' : 'window';
}
/**
* 从缓存中加载偏好设置。如果缓存中没有找到对应的偏好设置,则返回默认偏好设置。
*/
private loadCachedPreferences() {
return this.cache?.getItem<Preferences>(STORAGE_KEY);
}
/**
* 加载偏好设置
* @returns {Preferences} 加载的偏好设置
*/
private loadPreferences(): Preferences {
return this.loadCachedPreferences() || { ...defaultPreferences };
}
/**
* 监听状态和系统偏好设置的变化。
*/
private setupWatcher() {
if (this.isInitialized) {
return;
}
// 监听断点,判断是否移动端
const breakpoints = useBreakpoints(breakpointsTailwind);
const isMobile = breakpoints.smaller('md');
watch(
() => isMobile.value,
(val) => {
this.updatePreferences({
app: { isMobile: val },
});
},
{ immediate: true },
);
// 监听系统主题偏好设置变化
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', ({ matches: isDark }) => {
// 如果偏好设置中主题模式为auto则跟随系统更新
if (this.state.theme.mode === 'auto') {
this.updatePreferences({
theme: { mode: isDark ? 'dark' : 'light' },
});
// 恢复为auto模式
this.updatePreferences({
theme: { mode: 'auto' },
});
}
});
}
/**
* 更新页面颜色模式(灰色、色弱)
* @param preference
*/
private updateColorMode(preference: Preferences) {
if (preference.app) {
const { colorGrayMode, colorWeakMode } = preference.app;
const dom = document.documentElement;
const COLOR_WEAK = 'invert-mode';
const COLOR_GRAY = 'grayscale-mode';
colorWeakMode
? dom.classList.add(COLOR_WEAK)
: dom.classList.remove(COLOR_WEAK);
colorGrayMode
? dom.classList.add(COLOR_GRAY)
: dom.classList.remove(COLOR_GRAY);
}
}
}
const preferencesManager = new PreferenceManager();

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,
},
)
"
@@ -138,7 +157,7 @@ async function handleOpenChange(val: boolean) {
<div class="flex items-center">
<component :is="getIconRender" class="mr-2" />
<span class="flex-auto">{{ $t(title) }}</span>
<AlertDialogCancel v-if="showCancel">
<AlertDialogCancel v-if="showCancel" as-child>
<VbenButton
variant="ghost"
size="icon"
@@ -152,22 +171,27 @@ 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}`">
<AlertDialogCancel v-if="showCancel" :disabled="loading">
<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"
:disabled="loading"
variant="ghost"
@click="handleCancel"
>
{{ cancelText || $t('cancel') }}
</component>
</AlertDialogCancel>
<AlertDialogAction>
<AlertDialogAction as-child>
<component
:is="components.PrimaryButton || VbenButton"
:loading="loading"

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

@@ -17,12 +17,15 @@ export * from '@vben-core/popup-ui';
// 给文档用
export {
VbenAvatar,
VbenButton,
VbenButtonGroup,
VbenCheckButtonGroup,
VbenCountToAnimator,
VbenFullScreen,
VbenInputPassword,
VbenLoading,
VbenLogo,
VbenPinInput,
VbenSpinner,
VbenTree,

View File

@@ -9,6 +9,8 @@ import { $t } from '@vben/locales';
import { BUILT_IN_THEME_PRESETS } from '@vben/preferences';
import { convertToHsl, TinyColor } from '@vben/utils';
import { useThrottleFn } from '@vueuse/core';
defineOptions({
name: 'PreferenceBuiltinTheme',
});
@@ -19,6 +21,15 @@ const colorInput = ref();
const modelValue = defineModel<BuiltinThemeType>({ default: 'default' });
const themeColorPrimary = defineModel<string>('themeColorPrimary');
const updateThemeColorPrimary = useThrottleFn(
(value: string) => {
themeColorPrimary.value = value;
},
300,
true,
true,
);
const inputValue = computed(() => {
return new TinyColor(themeColorPrimary.value || '').toHexString();
});
@@ -84,7 +95,7 @@ function handleSelect(theme: BuiltinThemePreset) {
function handleInputChange(e: Event) {
const target = e.target as HTMLInputElement;
themeColorPrimary.value = convertToHsl(target.value);
updateThemeColorPrimary(convertToHsl(target.value));
}
function selectColor() {

View File

@@ -1,8 +1,11 @@
import type { SetupVxeTable } from './types';
import { usePreferences } from '@vben/preferences';
import { useVbenForm } from '@vben-core/form-ui';
import { defineComponent, watch } from 'vue';
import { usePreferences } from '@vben/preferences';
import { useVbenForm } from '@vben-core/form-ui';
import {
VxeButton,
VxeCheckbox,
@@ -103,7 +106,7 @@ export function setupVbenVxeTable(setupOptions: SetupVxeTable) {
initVxeTable();
useTableForm = useVbenForm;
const preference = usePreferences();
const { isDark, locale } = usePreferences();
const localMap = {
'zh-CN': zhCN,
@@ -111,11 +114,11 @@ export function setupVbenVxeTable(setupOptions: SetupVxeTable) {
};
watch(
[() => preference.theme.value, () => preference.locale.value],
([theme, locale]) => {
VxeUI.setTheme(theme === 'dark' ? 'dark' : 'light');
VxeUI.setI18n(locale, localMap[locale]);
VxeUI.setLanguage(locale);
[() => isDark.value, () => locale.value],
([isDarkValue, localeValue]) => {
VxeUI.setTheme(isDarkValue ? 'dark' : 'light');
VxeUI.setI18n(localeValue, localMap[localeValue]);
VxeUI.setLanguage(localeValue);
},
{
immediate: true,

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('')}`);
},
// 垂直布局label和input在不同行值为vertical
// 水平布局label和input在同一行
@@ -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>
@@ -261,6 +263,9 @@ async function openPrompt() {
</template>
</Card>
<Card class="w-[300px]" title="轻量提示弹窗">
<template #extra>
<DocButton path="/components/common-ui/vben-alert" />
</template>
<p>通过快捷方法创建动态提示弹窗适合一些轻量的提示和确认输入等</p>
<template #actions>
<Button type="primary" @click="openAlert">Alert</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;