70 Commits

Author SHA1 Message Date
dap
656d53a059 chore: 锁定依赖 2025-07-22 10:33:02 +08:00
dap
190c8c586e Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-06-30 19:35:24 +08:00
dap
5767af5269 update: 注释 2025-06-30 19:34:11 +08:00
dap
d3f4b936fb fix: withDefaultPlaceholder中placeholder 在keepalive & 语言切换 & tab切换 显示不变的问题 2025-06-30 19:32:23 +08:00
chewenye
b78bc65ce7 feat: 组件json-viewer支持bigint数据显示 cwy (#6377)
Co-authored-by: 车文烨 <chewy@china-lehua.com>
2025-06-29 04:32:30 +08:00
Utopia
b1fb623113 feat: 为 auth layout 添加 slot: logo, 提升组件的灵活性和可复用性 (#6442) 2025-06-27 19:23:24 +08:00
Stephen Chang
de14908fd3 fix(icon-picker): 解决icon-picker组件切换分页后,关键词检索失效问题 (#6437)
当icon-picker组件切换分页后,在输入关键词检索,但是分页没有重置,导致检索结果异常
2025-06-27 19:21:23 +08:00
Li Kui
5c3972196a fix: Add $t import to login expired modal (#6429)
closes #6230
2025-06-27 19:20:25 +08:00
CG.gatspy
3230781538 feat: [vben-tree]增加数据disabled (#6343)
* feat: [vben-tree]增加数据disabled

* Update packages/@core/ui-kit/shadcn-ui/src/ui/tree/tree.vue

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: Jin Mao <50581550+jinmao88@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-06-27 19:09:30 +08:00
broBinChen
e7fd0e3b6a feat(hooks): useHoverToggle的入参refElement支持传入响应式数组 (#6333)
* feat(hooks): useHoverToggle的入参refElement支持传入响应式数组

* feat(hooks): 1、增加 useHoverToggle 中 refElement 参数关于传入响应式数组的注释说明。 2、修改 watch 监听深度,仅需浅层监听 refs 变化。 3、使用 effectScope 管理 useElementHover 实例,避免 refs 变化时事件监听器累积导致的内存泄漏问题

* feat(hooks): 在useHoverToggle中增强 updateHovers  的边界处理,优化watch方案,只监听元素数量变化而不是整个数组变化,避免过度依赖收集

---------

Co-authored-by: xiaobin <xiaobin_chen@fzzixun.com>
2025-06-27 19:08:41 +08:00
dap
fb64d9f87a feat: warm-flow iframe主题切换 2025-06-26 12:05:33 +08:00
dap
c4e3ff14b3 refactor: 移除param的正则 2025-06-26 11:01:22 +08:00
dap
04796449da refactor: 个人中心 账号绑定 样式/逻辑重构(回滚了 既要又要的问题) 2025-06-26 09:37:14 +08:00
dap
6ced4a44c8 refactor: 再调一次就回滚 2025-06-26 09:28:21 +08:00
dap
3ebf0ac7df feat: useVbenForm 增加 Cascader(级联选择器) 组件 2025-06-25 18:50:22 +08:00
dap
47ae02c571 refactor: Windows 10 or Windows Server 2016 太长了 分割一下 2025-06-25 11:50:11 +08:00
dap
f16bfe2cd0 refactor: 在线设备 breakpoint 2025-06-25 11:37:14 +08:00
dap
383756c0aa Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-06-25 11:35:17 +08:00
yuhengshen
2f7d1f009d fix: 全屏状态下弹窗圆角优化 (#6413) 2025-06-24 23:41:54 +08:00
yuhengshen
946f91f387 feat: optimize modal dragging range(#6414)
* 当弹窗指定了容器时,拖拽将被限制在容器范围内
2025-06-24 17:05:59 +08:00
dap
445e6011da chore: sse enable 2025-06-24 14:02:55 +08:00
dap
afc2a3de58 chore: sse close 2025-06-23 21:51:16 +08:00
dap
cb83bca12d refactor: 个人中心 在线设备 样式重构 2025-06-20 14:19:22 +08:00
dap
f7ae821dc2 chore: 锁定lefthook版本 自动升级会导致报错 提交不了代码 2025-06-20 11:34:51 +08:00
dap
b737fa940a refactor: vxe升级后一些过时用法 2025-06-20 11:34:06 +08:00
lghuahua
986eacae9a fix: improve request logic in api-component
* 修复某些情况下updateParam设置的参数可能不会提交给api的问题
* 修复在上一个请求尚未完成前如果params发生了变更,将不会再触发新的api请求
---------

Co-authored-by: Netfan <netfan@foxmail.com>
2025-06-20 08:42:45 +08:00
dap
ce6867994a refactor: 个人中心 账号绑定 样式/逻辑重构 2025-06-18 11:16:26 +08:00
dap
e10ddb421c refactor: 未设置邮箱 2025-06-18 10:49:46 +08:00
dap
af0bb9bd66 refactor: 账号超长显示 2025-06-18 10:45:30 +08:00
dap
aec123a834 refactor: 个人中心 下拉卡片 昵称超长省略显示 2025-06-18 10:38:55 +08:00
dap
c09c089265 refactor: 个人中心 账号绑定 样式/逻辑重构 2025-06-17 21:43:31 +08:00
Netfan
97b8e28a2b docs: fix delete request usage (#6389) 2025-06-17 08:52:59 +08:00
dap
4baa0aed8b refactor: 这是iframe页面! iframe页面! iframe页面! 不是我写的真服了 2025-06-16 11:17:02 +08:00
尘墨
b2d3cf10aa !42 perf: 优化顶部栏头像展开用户信息绑定
Merge pull request !42 from 尘墨/main
2025-06-13 07:22:39 +00:00
dap
63d2b38fd1 refactor: tinymce在modal下全屏 2025-06-12 14:58:15 +08:00
dap
78cd6677c3 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-06-11 21:01:42 +08:00
Netfan
c0962fec18 fix: auto close popup on deactivated (#6368)
* 修复挂载到内容区域的弹窗和抽屉被意外关闭的问题
2025-06-11 12:20:52 +08:00
dap
d38093ca7d refactor: Partial 2025-06-10 10:53:08 +08:00
dap
687c33ec29 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-06-10 10:51:00 +08:00
XiaoHetitu
8ba7bdf2bd fix(button): 为按钮添加type属性防止表单提交意外触发表单验证机制 (#6340)
在按钮组件中,按钮元素缺少type="button"属性可能导致在表单中意外提交。添加此属性以确保按钮行为符合预期。

Co-authored-by: yuanwj <ywj6792341@qq.com>
2025-06-08 17:56:24 +08:00
zyy
b015fbc9fc fix: [adapter] 表格配置类型报错 (#6327)
配置toolbarConfig中的search时会有类型报错
2025-06-08 17:53:55 +08:00
broBinChen
b69320c070 feat(hooks): support separate enter/leave delays in useHoverToggle (#6325)
Co-authored-by: xiaobin <xiaobin_chen@fzzixun.com>
2025-06-08 17:53:29 +08:00
zhang
dcccc213ce fix: requestClient.upload会将vbenform中value为undefined的值转为字符串undefined’提… (#6300)
* fix: requestClient.upload会将vbenform中value为undefined的值转为字符串undefined’提交给后台保存

* fix: requestClient.upload会将vbenform中value为undefined的值转为字符串'undefined’提交给后台保存
2025-06-08 17:51:16 +08:00
ali-pay
c0e601c020 fix: menu type is not 'button' (#6277)
Co-authored-by: Jin Mao <50581550+jinmao88@users.noreply.github.com>
2025-06-08 17:50:44 +08:00
RanMaoting
017ed1a9e1 types: 为useVbenVxeGrid添加泛型声明,使grid实例上能正确获取到行数据的类型 (#5653)
Co-authored-by: Jin Mao <50581550+jinmao88@users.noreply.github.com>
2025-06-08 17:43:02 +08:00
dap
598f371568 refactor: 代码生成 导入 hover样式 2025-06-08 12:53:05 +08:00
dap
ca2aadaf4a refactor: 租户套餐 新增 过滤租户相关菜单 2025-06-06 12:02:56 +08:00
dap
616db1c127 docs: changelog 2025-06-05 21:35:59 +08:00
dap
08de1a6f19 refactor: 代码生成 字典下拉加载改为每次都重新加载 2025-06-05 21:33:53 +08:00
dap
006370798b refactor: tinymce+表单 校验失败样式 2025-06-05 21:18:36 +08:00
dap
831700660c refactor: 角色去掉moredropdown 改为平铺 2025-06-04 21:53:11 +08:00
dap
a53b9382f5 refactor: 排序 默认值 2025-06-04 19:49:03 +08:00
dap
703586123a refactor: 字典接口抛出异常(为什么会抛出异常?)无限调用接口 兼容处理 2025-06-04 19:06:18 +08:00
dap
0295418f79 fix: 菜单管理 路由地址的必填项不生效 2025-06-04 17:53:09 +08:00
dap
14b0d9b50f Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-06-04 09:18:54 +08:00
vben
b9aef618fe chore: release 5.5.7 2025-06-04 05:33:06 +08:00
Netfan
4102cc2211 feat: improve vbenCheckButtonGroup (#6329)
* 按钮组支持单选清除和多选限制最大选项数

* 按钮组支持icon插槽来定制图标
2025-06-03 23:11:56 +08:00
chewenye
ea776aa710 types: 扩展user-dropdown组件的menus类型,支持iconify (#6283)
Co-authored-by: 车文烨 <chewy@china-lehua.com>
2025-06-03 06:07:06 +08:00
huanghezhen
feb96dc8ea fix: resolve onClosed method failure in connectedComponent of useVbenModal (#6309) 2025-06-02 08:16:48 +08:00
wyc001122
470fd43b49 fix: 修复使用useVbenVxeGrid配置hasEmptyText、hasEmptyRender不生效的问题 (#6310) 2025-06-02 08:16:26 +08:00
zhang
76d106e474 fix: When defaultHomePage is inconsistent with user.homePath, the pa… (#6299)
* fix:  When defaultHomePage is inconsistent with user.homePath, the page refresh route jump will be abnormal

* fix:  When defaultHomePage is inconsistent with user.homePath, the page refresh route jump will be abnormal
2025-06-02 08:07:06 +08:00
wyc001122
78c3c9da6f docs(settings): 完善'生产环境动态配置'步骤 (#6297) 2025-06-02 08:05:23 +08:00
dap
8b7d717b21 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-05-31 18:53:41 +08:00
Netfan
081d08a7f8 fix: alert width fixed in small screen (#6312) 2025-05-30 19:54:26 +08:00
dap
0da75418d0 refactor: 菜单管理 刷新接口后 记录展开行的情况 (改为vxe配置) 2025-05-30 17:51:06 +08:00
dap
55f0da3085 refactor: tree-search 2025-05-30 14:24:40 +08:00
dap
3849800388 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-05-30 09:20:33 +08:00
dap
f913955259 docs: changelog 2025-05-29 19:45:38 +08:00
dap
8d6ef40d3e refactor: 适配新工作流预览 由logicflow改为iframe 真服了 2025-05-29 19:30:57 +08:00
Netfan
96a10ca83f style: fix lint error (#6298) 2025-05-28 19:23:21 +08:00
111 changed files with 1006 additions and 1081 deletions

View File

@@ -235,12 +235,14 @@
"cSpell.words": [
"archiver",
"axios",
"Cascader",
"dotenv",
"isequal",
"jspm",
"napi",
"nolebase",
"rollup",
"tinymce",
"vitest"
]
}

View File

@@ -1,15 +1,34 @@
# 1.4.1
**FEATURES**
- Tinymce添加在antd原生表单/useVbenForm下的校验样式
- useVbenForm 增加 Cascader(级联选择器) 组件
**BUG FIX**
- 菜单管理 路由地址的必填项不生效
- withDefaultPlaceholder中placeholder 在keepalive & 语言切换 & tab切换 显示不变的问题
**REFACTOR**
- 字典接口抛出异常(为什么会抛出异常?)无限调用接口 兼容处理
- 代码生成 字典下拉加载 改为每次进入编辑页面都加载
- ~~个人中心 账号绑定 样式/逻辑重构~~(回滚了 既要又要的问题)
- ~~个人中心 下拉卡片 昵称超长省略显示~~(回滚了 既要又要的问题)
# 1.4.0
**FEATURES**
- 菜单管理(通用方法) 保存表格滚动/展开状态并执行回调 用于树表在执行 新增/编辑/删除等操作后 依然在当前位置(体验优化)
-
- 菜单管理 级联删除 删除菜单和children
**REFACTOR**
- 除个人中心外所有本地路由改为从后端返回(需要执行更新sql)
- 流程图预览改为logicflow预览而非图片
- 流程图预览改为logicflow预览而非图片 ...然后后端又更新了 又改成iframe了
- 菜单管理 新增角色校验(与后端权限保持一致) 只有superadmin可进行增删改
# 1.3.6

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/web-antd",
"version": "1.4.0",
"version": "1.4.1",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
@@ -27,7 +27,6 @@
},
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"@logicflow/core": "^2.0.13",
"@tinymce/tinymce-vue": "^6.0.1",
"@vben/access": "workspace:*",
"@vben/common-ui": "workspace:*",

View File

@@ -9,6 +9,7 @@ import type { BaseFormComponentType } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import {
computed,
defineAsyncComponent,
defineComponent,
getCurrentInstance,
@@ -39,6 +40,9 @@ const AutoComplete = defineAsyncComponent(
() => import('ant-design-vue/es/auto-complete'),
);
const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
const Cascader = defineAsyncComponent(
() => import('ant-design-vue/es/cascader'),
);
const Checkbox = defineAsyncComponent(
() => import('ant-design-vue/es/checkbox'),
);
@@ -90,10 +94,13 @@ const withDefaultPlaceholder = <T extends Component>(
name: component.name,
inheritAttrs: false,
setup: (props: any, { attrs, expose, slots }) => {
const placeholder =
props?.placeholder ||
attrs?.placeholder ||
$t(`ui.placeholder.${type}`);
// 改为placeholder 解决在keepalive & 语言切换 & tab切换 显示不变的问题
const computedPlaceholder = computed(
() =>
props?.placeholder ||
attrs?.placeholder ||
$t(`ui.placeholder.${type}`),
);
// 透传组件暴露的方法
const innerRef = ref();
@@ -112,7 +119,7 @@ const withDefaultPlaceholder = <T extends Component>(
component,
{
...componentProps,
placeholder,
placeholder: computedPlaceholder.value,
...props,
...attrs,
ref: innerRef,
@@ -128,6 +135,7 @@ export type ComponentType =
| 'ApiSelect'
| 'ApiTreeSelect'
| 'AutoComplete'
| 'Cascader'
| 'Checkbox'
| 'CheckboxGroup'
| 'DatePicker'
@@ -191,6 +199,7 @@ async function initComponentAdapter() {
},
),
AutoComplete,
Cascader: withDefaultPlaceholder(Cascader, 'select'),
Checkbox,
CheckboxGroup,
DatePicker,

View File

@@ -50,13 +50,15 @@ setupVbenVxeTable({
// 右上角工具栏
toolbarConfig: {
// 自定义列
custom: {
custom: true,
customOptions: {
icon: 'vxe-icon-setting',
},
// 最大化
zoom: true,
// 刷新
refresh: {
refresh: true,
refreshOptions: {
// 默认为reload 修改为在当前页刷新
code: 'query',
},

View File

@@ -7,4 +7,5 @@ export interface OnlineUser {
browser: string;
os: string;
loginTime: number;
deviceType: string;
}

View File

@@ -39,6 +39,11 @@ const { apiURL, clientId, enableEncrypt } = useAppConfig(
*/
let isLogoutProcessing = false;
/**
* 定义一个401专用异常 用于可能会用到的区分场景?
*/
export class UnauthorizedException extends Error {}
function createRequestClient(baseURL: string) {
const client = new RequestClient({
// 后端地址
@@ -228,7 +233,7 @@ function createRequestClient(baseURL: string) {
case 401: {
// 已经在登出过程中 不再执行
if (isLogoutProcessing) {
throw new Error(timeoutMsg);
throw new UnauthorizedException(timeoutMsg);
}
isLogoutProcessing = true;
const _msg = $t('http.loginTimeout');
@@ -238,7 +243,7 @@ function createRequestClient(baseURL: string) {
isLogoutProcessing = false;
});
// 不再执行下面逻辑
throw new Error(_msg);
throw new UnauthorizedException(_msg);
}
default: {
if (msg) {

View File

@@ -94,7 +94,7 @@ export function pageByCurrent(params?: PageQuery) {
*/
export function flowInfo(businessId: string) {
return requestClient.get<FlowInfoResponse>(
`/workflow/instance/flowImage/${businessId}`,
`/workflow/instance/flowHisTaskList/${businessId}`,
);
}

View File

@@ -36,11 +36,6 @@ export interface Flow {
}
export interface FlowInfoResponse {
image: string;
instanceId: string;
list: Flow[];
defChart: {
defJson: Record<string, any>;
nodeJsonList: Record<string, any>[];
skipJsonList: Record<string, any>[];
};
}

View File

@@ -202,13 +202,31 @@ const events = computed(() => {
</template>
<style lang="scss">
/***
由于modal/drawer的zIndex升级后为2000
这里会造成遮挡 修改为更高的zIndex
*/
// 展开层元素z-index
$dropdown-index: 2025;
@mixin tinymce-valid-fail($color) {
.app-tinymce {
// 最外层的tinymce容器
.tox-tinymce {
border-color: $color;
}
// focus样式
.tox .tox-edit-area::before {
border-color: $color;
border-right: none;
border-left: none;
}
}
}
.tox.tox-silver-sink.tox-tinymce-aux {
/** 该样式默认为1300的zIndex */
z-index: 2025;
z-index: $dropdown-index;
}
.tox-fullscreen .tox.tox-tinymce-aux {
z-index: $dropdown-index !important;
}
.app-tinymce {
@@ -218,5 +236,29 @@ const events = computed(() => {
.tox-promotion {
display: none;
}
/** 保持focus时与primary色一致 */
.tox .tox-edit-area::before {
border-color: hsl(var(--primary));
}
}
// antd原生表单 校验失败样式
.ant-form-item:has(.ant-form-item-explain-error) {
$error-color: #ff3860;
@include tinymce-valid-fail($error-color);
}
// useVbenForm 校验失败样式
.form-valid-error {
$error-color: hsl(var(--destructive));
@include tinymce-valid-fail($error-color);
}
// 全屏下样式处理 不去掉transform位置会异常
div[role='dialog']:has(.tox.tox-tinymce.tox-fullscreen) {
transform: none !important;
}
</style>

View File

@@ -138,8 +138,8 @@ watch(
:avatar
:menus
:text="userStore.userInfo?.realName"
description="ann.vben@gmail.com"
tag-text="Pro"
:description="userStore.userInfo?.email || '未设置邮箱'"
:tag-text="userStore.userInfo?.username"
@logout="handleLogout"
/>
</template>

View File

@@ -20,6 +20,9 @@ export const overridesPreferences = defineOverridesPreferences({
* 这里可以设置默认头像 url链接或vite导入的图片链接
*/
// defaultAvatar: '',
/**
* 在这里设置应用标题
*/
name: import.meta.env.VITE_APP_TITLE,
/**
* 不支持modal模式 需要改动的地方太多

View File

@@ -27,7 +27,7 @@ const NotFoundComponent = () => import('#/views/_core/fallback/not-found.vue');
* 在这里定义映射
*/
const routeMetaMapping: Record<string, Omit<RouteMeta, 'title'>> = {
'/system/role-auth/user/:roleId(\\d+)': {
'/system/role-auth/user/:roleId': {
activePath: '/system/role',
requireHomeRedirect: true,
},
@@ -37,7 +37,7 @@ const routeMetaMapping: Record<string, Omit<RouteMeta, 'title'>> = {
requireHomeRedirect: true,
},
'/tool/gen-edit/index/:tableId(\\d+)': {
'/tool/gen-edit/index/:tableId': {
activePath: '/tool/gen',
requireHomeRedirect: true,
},

View File

@@ -118,6 +118,7 @@ export const useAuthStore = defineStore('auth', () => {
roles,
userId: user.userId,
username: user.userName,
email: user.email ?? '',
};
userStore.setUserInfo(userInfo);
/**

View File

@@ -1,3 +1,4 @@
import { UnauthorizedException } from '#/api/request';
import { dictDataInfo } from '#/api/system/dict/dict-data';
import { useDictStore } from '#/store/dict';
@@ -27,9 +28,16 @@ function fetchAndCacheDictData<T>(
// 内部处理了push的逻辑 这里不用push
setDictInfo(dictName, resp, formatNumber);
})
.catch(() => {
// 401时 移除字典缓存 下次登录重新获取
dictRequestCache.delete(dictName);
.catch((error) => {
/**
* 需要判断是否为401抛出的特定异常 401清除缓存
* 其他error清除缓存会导致无限循环调用字典接口 则不做处理
*/
if (error instanceof UnauthorizedException) {
// 401时 移除字典缓存 下次登录重新获取
dictRequestCache.delete(dictName);
}
// 其他不做处理
})
.finally(() => {
// 移除请求状态缓存

View File

@@ -1,6 +1,6 @@
import type { Component, CSSProperties } from 'vue';
import { ref } from 'vue';
import { markRaw, ref } from 'vue';
import { DEFAULT_TENANT_ID } from '@vben/constants';
import {
@@ -69,32 +69,32 @@ export async function handleAuthBinding(source: string) {
*/
export const accountBindList: BindItem[] = [
{
avatar: GiteeIcon,
avatar: markRaw(GiteeIcon),
description: '绑定Gitee账号',
source: 'gitee',
title: 'Gitee',
style: { color: '#c71d23' },
},
{
avatar: GithubOAuthIcon,
avatar: markRaw(GithubOAuthIcon),
description: '绑定Github账号',
source: 'github',
title: 'Github',
},
{
avatar: SvgMaxKeyIcon,
avatar: markRaw(SvgMaxKeyIcon),
description: '绑定MaxKey账号',
source: 'maxkey',
title: 'MaxKey',
},
{
avatar: SvgTopiamIcon,
avatar: markRaw(SvgTopiamIcon),
description: '绑定topiam账号',
source: 'topiam',
title: 'Topiam',
},
{
avatar: SvgWechatIcon,
avatar: markRaw(SvgWechatIcon),
description: '绑定wechat账号',
source: 'wechat',
title: 'Wechat',

View File

@@ -1,174 +1,142 @@
<script setup lang="tsx">
import type { VxeGridProps } from '@vben/plugins/vxe-table';
import type { BindItem } from '../../oauth-common';
import { computed, ref, unref } from 'vue';
import type { SocialInfo } from '#/api/system/social/model';
import { useVbenVxeGrid } from '@vben/plugins/vxe-table';
import { onMounted, ref } from 'vue';
import { Alert, Avatar, Card, List, ListItem, Modal } from 'ant-design-vue';
import { Alert, Avatar, Card, Empty, Modal, Tooltip } from 'ant-design-vue';
import { authUnbinding } from '#/api';
import { socialList } from '#/api/system/social';
import { accountBindList, handleAuthBinding } from '../../oauth-common';
function buttonText(item: BindItem) {
return item.bound ? '已绑定' : '绑定';
interface BindItemWithInfo extends BindItem {
info?: SocialInfo;
bind?: boolean;
}
/**
* 已经绑定的平台
*/
const boundPlatformsList = ref<string[]>([]);
const bindList = computed<BindItem[]>(() => {
const list = [...accountBindList];
const bindList = ref<BindItemWithInfo[]>([]);
async function loadData() {
const resp = await socialList();
const list: BindItemWithInfo[] = [...accountBindList];
list.forEach((item) => {
item.bound = !!unref(boundPlatformsList).includes(item.source);
/**
* 平台转小写
*/
item.bound = resp
.map((social) => social.source.toLowerCase())
.includes(item.source.toLowerCase());
/**
* 添加info信息
*/
if (item.bound) {
item.info = resp.find(
(social) => social.source.toLowerCase() === item.source,
);
}
});
return list;
});
const gridOptions: VxeGridProps = {
columns: [
{
field: 'source',
title: '绑定平台',
},
{
slots: {
default: ({ row }) => {
return <Avatar src={row.avatar} />;
},
},
field: 'avatar',
title: '头像',
},
{
align: 'center',
field: 'userName',
title: '账号',
},
{
align: 'center',
slots: {
default: 'action',
},
title: '操作',
},
],
height: 220,
keepSource: true,
pagerConfig: {
enabled: false,
},
toolbarConfig: {
enabled: false,
},
proxyConfig: {
ajax: {
query: async () => {
const resp = await socialList();
/**
* 平台转小写
* 已经绑定的平台
*/
boundPlatformsList.value = resp.map((item) =>
item.source.toLowerCase(),
);
return {
rows: resp,
};
},
},
},
rowConfig: {
isCurrent: false,
keyField: 'id',
},
id: 'profile-bind-table',
};
const [BasicTable, tableApi] = useVbenVxeGrid({
gridOptions,
});
bindList.value = list;
}
onMounted(loadData);
/**
* 解绑账号
*/
function handleUnbind(record: Record<string, any>) {
function handleUnbind(record: BindItemWithInfo) {
if (!record.info) {
return;
}
Modal.confirm({
content: `确定解绑[${record.source}]平台的[${record.userName}]账号吗?`,
content: `确定解绑[${record.source}]平台的[${record.info.userName}]账号吗?`,
async onOk() {
await authUnbinding(record.id);
await tableApi.reload();
await authUnbinding(record.info!.id);
await loadData();
},
title: '提示',
type: 'warning',
});
}
const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE;
</script>
<template>
<div class="flex flex-col gap-[16px]">
<BasicTable>
<template #action="{ row }">
<a-button type="link" @click="handleUnbind(row)">解绑</a-button>
</template>
</BasicTable>
<div class="pb-3">
<List
:data-source="bindList"
:grid="{ gutter: 8, xs: 1, sm: 1, md: 2, lg: 3, xl: 3, xxl: 3 }"
<div class="flex flex-col gap-4 pb-4">
<div
v-if="bindList.length > 0"
class="grid grid-cols-1 gap-4 lg:grid-cols-2 2xl:grid-cols-3"
>
<Card
class="transition-shadow duration-300 hover:shadow-md"
v-for="item in bindList"
:key="item.source"
>
<template #renderItem="{ item }">
<ListItem>
<Card>
<div class="flex w-full items-center gap-4">
<component
:is="item.avatar"
v-if="item.avatar"
:style="item?.style ?? {}"
class="size-[40px]"
/>
<div class="flex flex-1 items-center justify-between">
<div class="flex flex-col">
<h4
class="mb-[4px] text-[14px] text-black/85 dark:text-white/85"
>
{{ item.title }}
</h4>
<span class="text-black/45 dark:text-white/45">
{{ item.description }}
</span>
</div>
<a-button
:disabled="item.bound"
size="small"
type="link"
@click="handleAuthBinding(item.source)"
>
{{ buttonText(item) }}
</a-button>
</div>
</div>
</Card>
</ListItem>
</template>
</List>
<Alert message="说明" type="info">
<template #description>
<p>
需要添加第三方账号在
<span class="font-bold">
apps\web-antd\src\views\_core\oauth-common.ts
</span>
中accountBindList按模板添加
</p>
</template>
</Alert>
<div class="flex w-full items-center gap-4">
<component
:is="item.avatar"
v-if="item.avatar"
:style="item?.style ?? {}"
class="size-[40px]"
/>
<div class="flex flex-1 items-center justify-between">
<div class="flex flex-col">
<h4 class="mb-[4px] text-[14px] text-black/85 dark:text-white/85">
{{ item.title }}
</h4>
<span class="text-black/45 dark:text-white/45">
<template v-if="!item.bound">
{{ item.description }}
</template>
<template v-if="item.bound && item.info">
<Tooltip>
<template #title>
<div class="flex flex-col items-center gap-2 p-2">
<Avatar :size="36" :src="item.info.avatar" />
<div>绑定时间: {{ item.info.createTime }}</div>
</div>
</template>
<div class="cursor-pointer">
已绑定: {{ item.info.nickName }}
</div>
</Tooltip>
</template>
</span>
</div>
<!-- TODO: 这里有优化空间? -->
<a-button
size="small"
:type="item.bound ? 'default' : 'link'"
@click="
item.bound ? handleUnbind(item) : handleAuthBinding(item.source)
"
>
{{ item.bound ? '取消绑定' : '绑定' }}
</a-button>
</div>
</div>
</Card>
</div>
<div
v-if="bindList.length === 0"
class="flex items-center justify-center rounded-lg border py-4"
>
<Empty :image="simpleImage" description="暂无可绑定的第三方账户" />
</div>
<Alert message="说明" type="info">
<template #description>
<p>
需要添加第三方账号在
<span class="font-bold">
apps\web-antd\src\views\_core\oauth-common.ts
</span>
中accountBindList按模板添加
</p>
</template>
</Alert>
</div>
</template>

View File

@@ -95,6 +95,7 @@ export const drawerSchema: FormSchemaGetter = () => [
fieldName: 'orderNum',
label: '显示排序',
rules: 'required',
defaultValue: 0,
},
{
component: 'Input',

View File

@@ -99,6 +99,7 @@ export const drawerSchema: FormSchemaGetter = () => [
fieldName: 'dictSort',
label: '显示排序',
rules: 'required',
defaultValue: 0,
},
{
component: 'Textarea',

View File

@@ -238,6 +238,7 @@ export const drawerSchema: FormSchemaGetter = () => [
if (model.isFrame !== '0') {
return z
.string({ message: '请输入路由地址' })
.min(1, '请输入路由地址')
.refine((val) => !val.startsWith('/'), {
message: '路由地址不需要带/',
});

View File

@@ -1,25 +0,0 @@
import type { MaybePromise } from '@vben/types';
import type { useVbenVxeGrid } from '#/adapter/vxe-table';
/**
* 保存表格滚动/展开状态并执行回调 用于树表在执行 新增/编辑/删除等操作后 依然在当前位置(体验优化)
*
* @param tableApi 表格api
* @param callback 回调
*/
export async function preserveTreeTableState(
tableApi: ReturnType<typeof useVbenVxeGrid>[1],
callback: () => MaybePromise<void>,
) {
// 保存当前状态
const scrollState = tableApi.grid.getScroll();
const expandRecords = tableApi.grid.getTreeExpandRecords();
// 执行回调
await callback();
// 恢复状态
tableApi.grid.setTreeExpand(expandRecords, true);
tableApi.grid.scrollTo(scrollState.scrollLeft, scrollState.scrollTop);
}

View File

@@ -17,7 +17,6 @@ import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { menuCascadeRemove, menuList, menuRemove } from '#/api/system/menu';
import { columns, querySchema } from './data';
import { preserveTreeTableState } from './helper';
import menuDrawer from './menu-drawer.vue';
/**
@@ -69,6 +68,8 @@ const gridOptions: VxeGridProps = {
rowField: 'menuId',
// 自动转换为tree 由vxe处理 无需手动转换
transform: true,
// 刷新接口后 记录展开行的情况
reserve: true,
},
id: 'system-menu-index',
};
@@ -118,17 +119,15 @@ async function handleEdit(record: Menu) {
*/
const cascadingDeletion = ref(false);
async function handleDelete(row: Menu) {
await preserveTreeTableState(tableApi, async () => {
if (cascadingDeletion.value) {
// 级联删除
const menuAndChildren: Menu[] = treeToList([row], { id: 'menuId' });
await menuCascadeRemove(menuAndChildren.map((item) => item.menuId));
} else {
// 单删除
await menuRemove([row.menuId]);
}
await tableApi.query();
});
if (cascadingDeletion.value) {
// 级联删除
const menuAndChildren: Menu[] = treeToList([row], { id: 'menuId' });
await menuCascadeRemove(menuAndChildren.map((item) => item.menuId));
} else {
// 单删除
await menuRemove([row.menuId]);
}
await tableApi.query();
}
function removeConfirmTitle(row: Menu) {
@@ -147,7 +146,7 @@ function removeConfirmTitle(row: Menu) {
* 编辑/添加成功后刷新表格
*/
async function afterEditOrAdd() {
await preserveTreeTableState(tableApi, () => tableApi.query());
tableApi.query();
}
/**

View File

@@ -111,6 +111,7 @@ export const drawerSchema: FormSchemaGetter = () => [
fieldName: 'postSort',
label: '岗位排序',
rules: 'required',
defaultValue: 0,
},
{
component: 'RadioGroup',

View File

@@ -127,6 +127,7 @@ export const drawerSchema: FormSchemaGetter = () => [
fieldName: 'roleSort',
label: '角色排序',
rules: 'required',
defaultValue: 0,
},
{
component: 'Select',

View File

@@ -11,14 +11,7 @@ import { useAccess } from '@vben/access';
import { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';
import { getVxePopupContainer } from '@vben/utils';
import {
Dropdown,
Menu,
MenuItem,
Modal,
Popconfirm,
Space,
} from 'ant-design-vue';
import { Modal, Popconfirm, Space } from 'ant-design-vue';
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
import {
@@ -200,6 +193,18 @@ function handleAssignRole(record: Role) {
>
{{ $t('pages.common.edit') }}
</ghost-button>
<ghost-button
v-access:code="['system:role:edit']"
@click.stop="handleAuthEdit(row)"
>
权限
</ghost-button>
<ghost-button
v-access:code="['system:role:edit']"
@click.stop="handleAssignRole(row)"
>
分配
</ghost-button>
<Popconfirm
:get-popup-container="getVxePopupContainer"
placement="left"
@@ -215,25 +220,6 @@ function handleAssignRole(record: Role) {
</ghost-button>
</Popconfirm>
</Space>
<Dropdown placement="bottomRight">
<template #overlay>
<Menu>
<MenuItem key="1" @click="handleAuthEdit(row)">
数据权限
</MenuItem>
<MenuItem key="2" @click="handleAssignRole(row)">
分配用户
</MenuItem>
</Menu>
</template>
<a-button
size="small"
type="link"
v-access:code="'system:role:edit'"
>
{{ $t('pages.common.more') }}
</a-button>
</Dropdown>
</template>
</template>
</BasicTable>

View File

@@ -10,7 +10,7 @@ import { cloneDeep, eachTree } from '@vben/utils';
import { omit } from 'lodash-es';
import { useVbenForm } from '#/adapter/form';
import { menuTreeSelect, tenantPackageMenuTreeSelect } from '#/api/system/menu';
import { tenantPackageMenuTreeSelect } from '#/api/system/menu';
import {
packageAdd,
packageInfo,
@@ -40,30 +40,18 @@ const [BasicForm, formApi] = useVbenForm({
const menuTree = ref<MenuOption[]>([]);
async function setupMenuTree(id?: number | string) {
if (id) {
const resp = await tenantPackageMenuTreeSelect(id);
const menus = resp.menus;
// i18n处理
eachTree(menus, (node) => {
node.label = $t(node.label);
});
// 设置菜单信息
menuTree.value = resp.menus;
// keys依赖于menu 需要先加载menu
await nextTick();
await formApi.setFieldValue('menuIds', resp.checkedKeys);
} else {
const resp = await menuTreeSelect();
// i18n处理
eachTree(resp, (node) => {
node.label = $t(node.label);
});
// 设置菜单信息
menuTree.value = resp;
// keys依赖于menu 需要先加载menu
await nextTick();
await formApi.setFieldValue('menuIds', []);
}
// 0为新增使用 获取除了`租户管理`的所有菜单
const resp = await tenantPackageMenuTreeSelect(id ?? 0);
const menus = resp.menus;
// i18n处理
eachTree(menus, (node) => {
node.label = $t(node.label);
});
// 设置菜单信息
menuTree.value = menus;
// keys依赖于menu 需要先加载menu
await nextTick();
await formApi.setFieldValue('menuIds', resp.checkedKeys);
}
async function customFormValueGetter() {

View File

@@ -80,6 +80,7 @@ onMounted(loadTree);
v-model:value="searchValue"
:placeholder="$t('pages.common.search')"
size="small"
allow-clear
>
<template #enterButton>
<a-button @click="handleReload">
@@ -102,9 +103,9 @@ onMounted(loadTree);
@select="$emit('select')"
>
<template #title="{ label }">
<span v-if="label.indexOf(searchValue) > -1">
<span v-if="label.includes(searchValue)">
{{ label.substring(0, label.indexOf(searchValue)) }}
<span style="color: #f50">{{ searchValue }}</span>
<span class="text-primary">{{ searchValue }}</span>
{{
label.substring(
label.indexOf(searchValue) + searchValue.length,

View File

@@ -4,9 +4,10 @@ import type { Ref } from 'vue';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { GenInfo } from '#/api/tool/gen/model';
import { inject } from 'vue';
import { inject, onMounted, reactive } from 'vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { dictOptionSelectList } from '#/api/system/dict/dict-type';
import { validRules, vxeTableColumns } from './gen-data';
@@ -15,8 +16,26 @@ import { validRules, vxeTableColumns } from './gen-data';
*/
const genInfoData = inject('genInfoData') as Ref<GenInfo['info']>;
const dictOptions = reactive<{ label: string; value: string }[]>([
{ label: '未设置', value: '' },
]);
/**
* 加载字典下拉数据
*/
onMounted(async () => {
const resp = await dictOptionSelectList();
const options = resp.map((dict) => ({
label: `${dict.dictName} | ${dict.dictType}`,
value: dict.dictType,
}));
dictOptions.push(...options);
});
const gridOptions: VxeGridProps = {
columns: vxeTableColumns,
columns: vxeTableColumns(dictOptions),
keepSource: true,
editConfig: { trigger: 'click', mode: 'cell', showStatus: true },
editRules: validRules,

View File

@@ -2,14 +2,10 @@ import type { Recordable } from '@vben/types';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { reactive } from 'vue';
import { getPopupContainer } from '@vben/utils';
import { Checkbox, Input, Select } from 'ant-design-vue';
import { dictOptionSelectList } from '#/api/system/dict/dict-type';
const JavaTypes: string[] = [
'Long',
'String',
@@ -45,24 +41,6 @@ const componentsOptions = [
{ label: '富文本', value: 'editor' },
];
const dictOptions = reactive<{ label: string; value: string }[]>([
{ label: '未设置', value: '' },
]);
/**
* 在这里初始化字典下拉框
*/
(async function init() {
const ret = await dictOptionSelectList();
ret.forEach((dict) => {
const option = {
label: `${dict.dictName} | ${dict.dictType}`,
value: dict.dictType,
};
dictOptions.push(option);
});
})();
function renderBooleanTag(row: Recordable<any>, field: string) {
const value = row[field] ? '是' : '否';
const className = row[field] ? 'text-green-500' : 'text-red-500';
@@ -78,7 +56,10 @@ export const validRules: VxeGridProps['editRules'] = {
javaField: [{ required: true, message: '请输入' }],
};
export const vxeTableColumns: VxeGridProps['columns'] = [
// 内部依赖的字典从外部通过函数传入
export const vxeTableColumns: (
dictOptions: { label: string; value: string }[],
) => VxeGridProps['columns'] = (dictOptions) => [
{
title: '序号',
type: 'seq',

View File

@@ -86,11 +86,13 @@ const gridOptions: VxeGridProps = {
},
},
rowConfig: {
keyField: 'tableId',
keyField: 'tableName',
},
toolbarConfig: {
enabled: false,
},
id: 'import-table-modal',
cellClassName: 'cursor-pointer',
};
const [BasicTable, tableApi] = useVbenVxeGrid({ formOptions, gridOptions });

View File

@@ -41,8 +41,8 @@ import {
import { renderDict } from '#/utils/render';
import { approvalModal, approvalRejectionModal, flowInterfereModal } from '.';
import FlowPreview from '../components/flow-preview/index.vue';
import ApprovalDetails from './approval-details.vue';
import FlowPreview from './flow-preview.vue';
import { approveWithReasonModal } from './helper';
import userSelectModal from './user-select-modal.vue';
@@ -443,10 +443,7 @@ async function handleCopy(text: string) {
/>
</TabPane>
<TabPane key="2" tab="审批流程图">
<FlowPreview
v-if="currentFlowInfo.defChart"
:data="currentFlowInfo.defChart.defJson"
/>
<FlowPreview :instance-id="currentFlowInfo.instanceId" />
</TabPane>
</Tabs>
</div>

View File

@@ -6,6 +6,7 @@ import { stringify } from '@vben/request';
import { useAccessStore } from '@vben/stores';
import { useEventListener } from '@vueuse/core';
import { Alert } from 'ant-design-vue';
defineOptions({ name: 'FlowDesigner' });
@@ -48,5 +49,13 @@ useEventListener('message', messageHandler);
</script>
<template>
<iframe :src="url" class="size-full"></iframe>
<div class="size-full">
<Alert
class="mx-4 my-2"
type="warning"
:show-icon="true"
message="这是iframe页面! iframe页面! iframe页面! 不是我写的真服了"
/>
<iframe :src="url" class="size-full"></iframe>
</div>
</template>

View File

@@ -0,0 +1,32 @@
<script setup lang="ts">
import { useAppConfig } from '@vben/hooks';
import { stringify } from '@vben/request';
import { useAccessStore } from '@vben/stores';
import { useWarmflowIframe } from './hook';
defineOptions({ name: 'FlowPreview' });
const props = defineProps<{ instanceId: string }>();
const { clientId } = useAppConfig(import.meta.env, import.meta.env.PROD);
const accessStore = useAccessStore();
const params = {
Authorization: `Bearer ${accessStore.accessToken}`,
id: props.instanceId,
clientid: clientId,
type: 'FlowChart',
};
/**
* iframe地址
*/
const url = `${import.meta.env.VITE_GLOB_API_URL}/warm-flow-ui/index.html?${stringify(params)}`;
const { iframeRef } = useWarmflowIframe();
</script>
<template>
<iframe ref="iframeRef" :src="url" class="h-[500px] w-full border"></iframe>
</template>

View File

@@ -1,121 +0,0 @@
<script setup lang="ts">
import type { ZoomParamType } from '@logicflow/core';
import { onMounted, shallowRef, useTemplateRef, watch } from 'vue';
import { usePreferences } from '@vben/preferences';
import {
MinusCircleOutlined,
PlusCircleOutlined,
ShrinkOutlined,
} from '@ant-design/icons-vue';
import LogicFlow from '@logicflow/core';
import Between from './model/between';
import End from './model/end';
import Parallel from './model/parallel';
import Serial from './model/serial';
import Skip from './model/skip';
import Start from './model/start';
import { json2LogicFlowJson } from './model/tool';
import '@logicflow/core/lib/style/index.css';
const props = withDefaults(defineProps<{ data?: object }>(), {
data: () => ({}),
});
const container = useTemplateRef('container');
const lf = shallowRef<LogicFlow | null>(null);
function zoomViewport(zoom: ZoomParamType) {
if (!lf.value) {
return;
}
lf.value.zoom(zoom);
// 将内容平移至画布中心
lf.value.translateCenter();
}
onMounted(async () => {
if (props.data && container.value) {
const data = json2LogicFlowJson(props.data);
lf.value = new LogicFlow({
container: container.value,
isSilentMode: true,
textEdit: false,
grid: {
size: 20,
type: 'dot',
config: {
color: '#ccc',
thickness: 1,
},
},
background: {
backgroundColor: '#fff',
},
});
lf.value.register(Start);
lf.value.register(Between);
lf.value.register(Serial);
lf.value.register(Parallel);
lf.value.register(End);
lf.value.register(Skip);
lf.value.render(data);
lf.value.translateCenter();
}
});
const { isDark } = usePreferences();
watch(isDark, (v) => {
if (!lf.value) {
return;
}
lf.value.graphModel.background = {
background: v ? '#333' : '#fff',
};
});
</script>
<template>
<div>
<div class="flex items-center justify-between py-2">
<div class="flex items-center gap-3">
<a-button @click="zoomViewport(1)">
<template #icon>
<ShrinkOutlined />
</template>
</a-button>
<a-button @click="zoomViewport(true)">
<template #icon>
<PlusCircleOutlined />
</template>
</a-button>
<a-button @click="zoomViewport(false)">
<template #icon>
<MinusCircleOutlined />
</template>
</a-button>
</div>
<div class="flex items-center gap-3 font-semibold">
<span class="rounded-md border border-[#000] px-2">未办理</span>
<span
class="rounded-md border border-dashed border-[#ffcd17] bg-[#fff8dc] px-2 dark:text-black"
>
待办理
</span>
<span
class="rounded-md border border-[#9dff00] bg-[#f0ffd9] px-2 dark:text-black"
>
已完成
</span>
</div>
</div>
<!-- 容器区域 -->
<div class="h-[500px] w-full border" ref="container"></div>
</div>
</template>

View File

@@ -1,21 +0,0 @@
import LogicFlow, { RectNode, RectNodeModel } from '@logicflow/core';
class BetweenModel extends RectNodeModel {
override getNodeStyle() {
return super.getNodeStyle();
}
override initNodeData(data: LogicFlow.NodeConfig) {
super.initNodeData(data);
this.width = 100;
this.height = 80;
this.radius = 5;
}
}
class BetweenView extends RectNode {}
export default {
type: 'between',
model: BetweenModel,
view: BetweenView,
};

View File

@@ -1,16 +0,0 @@
import LogicFlow, { CircleNode, CircleNodeModel } from '@logicflow/core';
class endModel extends CircleNodeModel {
override initNodeData(data: LogicFlow.NodeConfig) {
super.initNodeData(data);
this.r = 20;
}
}
class endView extends CircleNode {}
export default {
type: 'end',
model: endModel,
view: endView,
};

View File

@@ -1,59 +0,0 @@
import type { GraphModel } from '@logicflow/core';
import LogicFlow, { h, PolygonNode, PolygonNodeModel } from '@logicflow/core';
class ParallelModel extends PolygonNodeModel {
static extendKey = 'ParallelModel';
constructor(data: LogicFlow.NodeConfig, graphModel: GraphModel) {
if (!data.text) {
data.text = '';
}
if (data.text && typeof data.text === 'string') {
data.text = {
value: data.text,
x: data.x,
y: data.y + 40,
};
}
super(data, graphModel);
this.points = [
[25, 0],
[50, 25],
[25, 50],
[0, 25],
];
}
}
class ParallelView extends PolygonNode {
static extendKey = 'ParallelNode';
override getShape() {
const { model } = this.props;
const { x, y, width, height, points } = model;
const style = model.getNodeStyle();
return h(
'g',
{
transform: `matrix(1 0 0 1 ${x - width / 2} ${y - height / 2})`,
},
h('polygon', {
...style,
x,
y,
points,
}),
h('path', {
d: 'm 23,10 0,12.5 -12.5,0 0,5 12.5,0 0,12.5 5,0 0,-12.5 12.5,0 0,-5 -12.5,0 0,-12.5 -5,0 z',
...style,
}),
);
}
}
export default {
type: 'parallel',
view: ParallelView,
model: ParallelModel,
};

View File

@@ -1,59 +0,0 @@
import type { GraphModel } from '@logicflow/core';
import LogicFlow, { h, PolygonNode, PolygonNodeModel } from '@logicflow/core';
class SerialModel extends PolygonNodeModel {
static extendKey = 'SerialModel';
constructor(data: LogicFlow.NodeConfig, graphModel: GraphModel) {
if (!data.text) {
data.text = '';
}
if (data.text && typeof data.text === 'string') {
data.text = {
value: data.text,
x: data.x,
y: data.y + 40,
};
}
super(data, graphModel);
this.points = [
[25, 0],
[50, 25],
[25, 50],
[0, 25],
];
}
}
class SerialView extends PolygonNode {
static extendKey = 'SerialNode';
override getShape() {
const { model } = this.props;
const { x, y, width, height, points } = model;
const style = model.getNodeStyle();
return h(
'g',
{
transform: `matrix(1 0 0 1 ${x - width / 2} ${y - height / 2})`,
},
h('polygon', {
...style,
x,
y,
points,
}),
h('path', {
d: 'm 16,15 7.42857142857143,9.714285714285715 -7.42857142857143,9.714285714285715 3.428571428571429,0 5.714285714285715,-7.464228571428572 5.714285714285715,7.464228571428572 3.428571428571429,0 -7.42857142857143,-9.714285714285715 7.42857142857143,-9.714285714285715 -3.428571428571429,0 -5.714285714285715,7.464228571428572 -5.714285714285715,-7.464228571428572 -3.428571428571429,0 z',
...style,
}),
);
}
}
export default {
type: 'serial',
view: SerialView,
model: SerialModel,
};

View File

@@ -1,32 +0,0 @@
import { PolylineEdge, PolylineEdgeModel } from '@logicflow/core';
class SkipModel extends PolylineEdgeModel {
/**
* 重写此方法,使保存数据是能带上锚点数据。
*/
override getData() {
const data = super.getData();
data.sourceAnchorId = this.sourceAnchorId;
data.targetAnchorId = this.targetAnchorId;
return data;
}
override getEdgeStyle() {
const style = super.getEdgeStyle();
const { properties } = this;
if (properties.isActived) {
style.strokeDasharray = '4 4';
}
return style;
}
override setAttributes() {
this.offset = 20;
}
}
export default {
type: 'skip',
view: PolylineEdge,
model: SkipModel,
};

View File

@@ -1,16 +0,0 @@
import LogicFlow, { CircleNode, CircleNodeModel } from '@logicflow/core';
class StartModel extends CircleNodeModel {
override initNodeData(data: LogicFlow.NodeConfig) {
super.initNodeData(data);
this.r = 20;
}
}
class StartView extends CircleNode {}
export default {
type: 'start',
model: StartModel,
view: StartView,
};

View File

@@ -1,248 +0,0 @@
/* eslint-disable unicorn/no-array-reduce */
const NODE_TYPE_MAP = {
0: 'start',
1: 'between',
2: 'end',
3: 'serial',
4: 'parallel',
};
/**
* 将warm-flow的定义json数据转成LogicFlow支持的数据格式
* @param {*} json
* @returns LogicFlow的数据
*/
export const json2LogicFlowJson = (definition: any) => {
const graphData: any = {
nodes: [],
edges: [],
};
// 解析definition属性
graphData.flowCode = definition.flowCode;
graphData.flowName = definition.flowName;
graphData.version = definition.version;
graphData.fromCustom = definition.fromCustom;
graphData.fromPath = definition.fromPath;
// 解析节点
const allSkips = definition.nodeList.reduce((acc: any, node: any) => {
if (node.skipList && Array.isArray(node.skipList)) {
acc.push(...node.skipList);
}
return acc;
}, []);
const allNodes = definition.nodeList;
// 解析节点
if (allNodes.length > 0) {
for (let i = 0, len = allNodes.length; i < len; i++) {
const node = allNodes[i];
const lfNode: any = {
text: {},
properties: {},
};
// 处理节点
lfNode.type = (NODE_TYPE_MAP as any)[node.nodeType];
lfNode.id = node.nodeCode;
const coordinate = node.coordinate;
if (coordinate) {
const attr = coordinate.split('|');
const nodeXy = attr[0].split(',');
lfNode.x = Number.parseInt(nodeXy[0]);
lfNode.y = Number.parseInt(nodeXy[1]);
if (attr.length === 2) {
const textXy = attr[1].split(',');
lfNode.text.x = Number.parseInt(textXy[0]);
lfNode.text.y = Number.parseInt(textXy[1]);
}
}
lfNode.text.value = node.nodeName;
lfNode.properties.nodeRatio = node.nodeRatio.toString();
lfNode.properties.permissionFlag = node.permissionFlag;
lfNode.properties.anyNodeSkip = node.anyNodeSkip;
lfNode.properties.listenerType = node.listenerType;
lfNode.properties.listenerPath = node.listenerPath;
lfNode.properties.formCustom = node.formCustom;
lfNode.properties.formPath = node.formPath;
lfNode.properties.ext = {};
if (node.ext && typeof node.ext === 'string') {
try {
node.ext = JSON.parse(node.ext);
node.ext.forEach((e: any) => {
lfNode.properties.ext[e.code] = String(e.value).includes(',')
? e.value.split(',')
: String(e.value);
});
} catch (error) {
console.error('Error parsing JSON:', error);
}
}
lfNode.properties.style = {};
if (node.status === 2) {
lfNode.properties.style.fill = '#F0FFD9';
lfNode.properties.style.stroke = '#9DFF00';
}
if (node.status === 1) {
lfNode.properties.style.fill = '#FFF8DC';
lfNode.properties.style.stroke = '#FFCD17';
}
graphData.nodes.push(lfNode);
}
}
if (allSkips.length > 0) {
// 处理边
let skipEle = null;
let edge: any = {};
for (let j = 0, lenn = allSkips.length; j < lenn; j++) {
skipEle = allSkips[j];
edge = {
text: {},
properties: {},
};
edge.id = skipEle.id;
edge.type = 'skip';
edge.sourceNodeId = skipEle.nowNodeCode;
edge.targetNodeId = skipEle.nextNodeCode;
edge.text = { value: skipEle.skipName };
edge.properties.skipCondition = skipEle.skipCondition;
edge.properties.skipName = skipEle.skipName;
edge.properties.skipType = skipEle.skipType;
const expr = skipEle.expr;
if (expr) {
edge.properties.expr = skipEle.expr;
}
const coordinate = skipEle.coordinate;
if (coordinate) {
const coordinateXy = coordinate.split('|');
edge.pointsList = [];
coordinateXy[0].split(';').forEach((item: any) => {
const pointArr = item.split(',');
edge.pointsList.push({
x: Number.parseInt(pointArr[0]),
y: Number.parseInt(pointArr[1]),
});
});
edge.startPoint = edge.pointsList[0];
edge.endPoint = edge.pointsList[edge.pointsList.length - 1];
if (coordinateXy.length > 1) {
const textXy = coordinateXy[1].split(',');
edge.text.x = Number.parseInt(textXy[0]);
edge.text.y = Number.parseInt(textXy[1]);
}
}
graphData.edges.push(edge);
}
}
console.log(graphData);
return graphData;
};
/**
* 将LogicFlow的数据转成warm-flow的json定义文件
* @param {*} data(...definitionInfo,nodes,edges)
* @returns
*/
export const logicFlowJsonToWarmFlow = (data: any) => {
// 先构建成流程对象
const definition: any = {
nodeList: [],
};
/**
* 根据节点的类型值获取key
* @param {*} mapValue 节点类型映射
* @returns
*/
const getNodeTypeValue = (mapValue: any) => {
for (const key in NODE_TYPE_MAP) {
if ((NODE_TYPE_MAP as any)[key] === mapValue) {
return key;
}
}
};
/**
* 根据节点的编码,获取节点的类型
* @param {*} nodeCode 当前节点名称
* @returns
*/
const getNodeType = (nodeCode: any) => {
for (const node of definition.nodeList) {
if (nodeCode === node.nodeCode) {
return node.nodeType;
}
}
};
/**
* 拼接skip坐标
* @param {*} edge logicFlow的edge
* @returns
*/
const getCoordinate = (edge: any) => {
let coordinate = '';
for (let i = 0; i < edge.pointsList.length; i++) {
coordinate = `${
coordinate + Number.parseInt(edge.pointsList[i].x)
},${Number.parseInt(edge.pointsList[i].y)}`;
if (i !== edge.pointsList.length - 1) {
coordinate = `${coordinate};`;
}
}
if (edge.text) {
coordinate = `${coordinate}|${Number.parseInt(edge.text.x)},${Number.parseInt(edge.text.y)}`;
}
return coordinate;
};
// 流程定义
definition.id = data.id;
definition.flowCode = data.flowCode;
definition.flowName = data.flowName;
definition.version = data.version;
definition.fromCustom = data.fromCustom;
definition.fromPath = data.fromPath;
// 流程节点
data.nodes.forEach((anyNode: any) => {
const node: any = {};
node.nodeType = getNodeTypeValue(anyNode.type);
node.nodeCode = anyNode.id;
if (anyNode.text) {
node.nodeName = anyNode.text.value;
}
node.permissionFlag = anyNode.properties.permissionFlag;
node.nodeRatio = anyNode.properties.nodeRatio;
node.anyNodeSkip = anyNode.properties.anyNodeSkip;
node.listenerType = anyNode.properties.listenerType;
node.listenerPath = anyNode.properties.listenerPath;
node.formCustom = anyNode.properties.formCustom;
node.formPath = anyNode.properties.formPath;
node.ext = [];
for (const key in anyNode.properties.ext) {
if (Object.prototype.hasOwnProperty.call(anyNode.properties.ext, key)) {
const e = anyNode.properties.ext[key];
node.ext.push({ code: key, value: Array.isArray(e) ? e.join(',') : e });
}
}
node.ext = JSON.stringify(node.ext);
node.coordinate = `${anyNode.x},${anyNode.y}`;
if (anyNode.text && anyNode.text.x && anyNode.text.y) {
node.coordinate = `${node.coordinate}|${anyNode.text.x},${anyNode.text.y}`;
}
node.handlerType = anyNode.properties.handlerType;
node.handlerPath = anyNode.properties.handlerPath;
node.version = definition.version;
node.skipList = [];
data.edges.forEach((anyEdge: any) => {
if (anyEdge.sourceNodeId === anyNode.id) {
const skip: any = {};
skip.skipType = anyEdge.properties.skipType;
skip.skipCondition = anyEdge.properties.skipCondition;
skip.skipName = anyEdge?.text?.value || anyEdge.properties.skipName;
skip.nowNodeCode = anyEdge.sourceNodeId;
skip.nowNodeType = getNodeType(skip.nowNodeCode);
skip.nextNodeCode = anyEdge.targetNodeId;
skip.nextNodeType = getNodeType(skip.nextNodeCode);
skip.coordinate = getCoordinate(anyEdge);
node.skipList.push(skip);
}
});
definition.nodeList.push(node);
});
return JSON.stringify(definition);
};

View File

@@ -0,0 +1,37 @@
import { onMounted, useTemplateRef, watch } from 'vue';
import { usePreferences } from '@vben/preferences';
/**
* warmflow ref相关操作
* @returns hook
*/
export function useWarmflowIframe() {
const iframeRef = useTemplateRef<HTMLIFrameElement>('iframeRef');
const { isDark } = usePreferences();
onMounted(() => {
/**
* load只是iframe加载完 而非vue加载完
*/
iframeRef.value?.addEventListener('load', async () => {
/**
* TODO: 这里可以优化 因为拿不到内部vue的mount状态
*/
await new Promise((resolve) => setTimeout(resolve, 500));
const theme = isDark.value ? 'theme-dark' : 'theme-light';
iframeRef.value?.contentWindow?.postMessage({ type: theme });
});
});
// 监听主题切换 通知iframe切换
watch(isDark, (dark) => {
if (!iframeRef.value) {
return;
}
const theme = dark ? 'theme-dark' : 'theme-light';
iframeRef.value.contentWindow?.postMessage({ type: theme });
});
return { iframeRef };
}

View File

@@ -73,6 +73,7 @@ onMounted(loadTree);
v-model:value="searchValue"
:placeholder="$t('pages.common.search')"
size="small"
allow-clear
>
<template #enterButton>
<a-button @click="handleReload">
@@ -95,9 +96,9 @@ onMounted(loadTree);
@select="$emit('select')"
>
<template #title="{ label }">
<span v-if="label.indexOf(searchValue) > -1">
<span v-if="label.includes(searchValue)">
{{ label.substring(0, label.indexOf(searchValue)) }}
<span style="color: #f50">{{ searchValue }}</span>
<span class="text-primary">{{ searchValue }}</span>
{{
label.substring(
label.indexOf(searchValue) + searchValue.length,

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/docs",
"version": "5.5.6",
"version": "5.5.7",
"private": true,
"scripts": {
"build": "vitepress build",

View File

@@ -150,8 +150,8 @@ export async function saveUserApi(user: UserInfo) {
```ts
import { requestClient } from '#/api/request';
export async function deleteUserApi(user: UserInfo) {
return requestClient.delete<boolean>(`/user/${user.id}`, user);
export async function deleteUserApi(userId: number) {
return requestClient.delete<boolean>(`/user/${userId}`);
}
```

View File

@@ -21,7 +21,7 @@ The rules are consistent with [Vite Env Variables and Modes](https://vitejs.dev/
console.log(import.meta.env.VITE_PROT);
```
- Variables starting with `VITE_GLOB_*` will be added to the `_app.config.js` configuration file during packaging. :::
- Variables starting with `VITE_GLOB_*` will be added to the `_app.config.js` configuration file during packaging.
:::
@@ -138,6 +138,27 @@ To add a new dynamically modifiable configuration item, simply follow the steps
}
```
- In `packages/effects/hooks/src/use-app-config.ts`, add the corresponding configuration item, such as:
```ts
export function useAppConfig(
env: Record<string, any>,
isProduction: boolean,
): ApplicationConfig {
// In production environment, directly use the window._VBEN_ADMIN_PRO_APP_CONF_ global variable
const config = isProduction
? window._VBEN_ADMIN_PRO_APP_CONF_
: (env as VbenAdminProAppConfigRaw);
const { VITE_GLOB_API_URL, VITE_GLOB_OTHER_API_URL } = config; // [!code ++]
return {
apiURL: VITE_GLOB_API_URL,
otherApiURL: VITE_GLOB_OTHER_API_URL, // [!code ++]
};
}
```
At this point, you can use the `useAppConfig` method within the project to access the newly added configuration item.
```ts

View File

@@ -180,8 +180,8 @@ export async function saveUserApi(user: UserInfo) {
```ts
import { requestClient } from '#/api/request';
export async function deleteUserApi(user: UserInfo) {
return requestClient.delete<boolean>(`/user/${user.id}`, user);
export async function deleteUserApi(userId: number) {
return requestClient.delete<boolean>(`/user/${userId}`);
}
```

View File

@@ -21,7 +21,7 @@
console.log(import.meta.env.VITE_PROT);
```
- 以 `VITE_GLOB_*` 开头的的变量,在打包的时候,会被加入 `_app.config.js`配置文件当中. :::
- 以 `VITE_GLOB_*` 开头的的变量,在打包的时候,会被加入 `_app.config.js`配置文件当中.
:::
@@ -137,6 +137,27 @@ const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
}
```
- 在 `packages/effects/hooks/src/use-app-config.ts` 中,新增对应的配置项,如:
```ts
export function useAppConfig(
env: Record<string, any>,
isProduction: boolean,
): ApplicationConfig {
// 生产环境下,直接使用 window._VBEN_ADMIN_PRO_APP_CONF_ 全局变量
const config = isProduction
? window._VBEN_ADMIN_PRO_APP_CONF_
: (env as VbenAdminProAppConfigRaw);
const { VITE_GLOB_API_URL, VITE_GLOB_OTHER_API_URL } = config; // [!code ++]
return {
apiURL: VITE_GLOB_API_URL,
otherApiURL: VITE_GLOB_OTHER_API_URL, // [!code ++]
};
}
```
到这里,就可以在项目内使用 `useAppConfig`方法获取到新增的配置项了。
```ts

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/commitlint-config",
"version": "5.5.6",
"version": "5.5.7",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/stylelint-config",
"version": "5.5.6",
"version": "5.5.7",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/node-utils",
"version": "5.5.6",
"version": "5.5.7",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/tailwind-config",
"version": "5.5.6",
"version": "5.5.7",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/tsconfig",
"version": "5.5.6",
"version": "5.5.7",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

View File

@@ -1,6 +1,6 @@
{
"name": "vben-admin-monorepo",
"version": "5.5.6",
"version": "5.5.7",
"private": true,
"keywords": [
"monorepo",

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/design",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/icons",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/shared",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/typings",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -12,6 +12,10 @@ interface BasicUserInfo {
* 头像
*/
avatar: string;
/**
* 邮箱
*/
email: string;
/**
* 用户权限
*/

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/composables",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/preferences",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/form-ui",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/layout-ui",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/menu-ui",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -34,7 +34,6 @@ const props = withDefaults(defineProps<AlertProps>(), {
bordered: true,
buttonAlign: 'end',
centered: true,
containerClass: 'w-[520px]',
});
const emits = defineEmits(['closed', 'confirm', 'opened']);
const open = defineModel<boolean>('open', { default: false });
@@ -148,7 +147,7 @@ async function handleOpenChange(val: boolean) {
:class="
cn(
containerClass,
'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%]',
'left-0 right-0 mx-auto flex max-h-[80%] flex-col p-0 duration-300 sm:w-[520px] sm:max-w-[80%] sm:rounded-[var(--radius)]',
{
'border-border border': bordered,
'shadow-3xl': !bordered,

View File

@@ -1,7 +1,15 @@
<script lang="ts" setup>
import type { DrawerProps, ExtendedDrawerApi } from './drawer';
import { computed, provide, ref, unref, useId, watch } from 'vue';
import {
computed,
onDeactivated,
provide,
ref,
unref,
useId,
watch,
} from 'vue';
import {
useIsMobile,
@@ -94,6 +102,16 @@ const {
// },
// );
/**
* 在开启keepAlive情况下 直接通过浏览器按钮/手势等返回 不会关闭弹窗
*/
onDeactivated(() => {
// 如果弹窗没有被挂载到内容区域,则关闭弹窗
if (!appendToMain.value) {
props.drawerApi?.close();
}
});
function interactOutside(e: Event) {
if (!closeOnClickModal.value || submitting.value) {
e.preventDefault();

View File

@@ -9,7 +9,6 @@ import {
h,
inject,
nextTick,
onDeactivated,
provide,
reactive,
ref,
@@ -72,13 +71,6 @@ export function useVbenDrawer<
},
);
/**
* 在开启keepAlive情况下 直接通过浏览器按钮/手势等返回 不会关闭弹窗
*/
onDeactivated(() => {
(extendedApi as ExtendedDrawerApi)?.close?.();
});
return [Drawer, extendedApi as ExtendedDrawerApi] as const;
}

View File

@@ -1,7 +1,16 @@
<script lang="ts" setup>
import type { ExtendedModalApi, ModalProps } from './modal';
import { computed, nextTick, provide, ref, unref, useId, watch } from 'vue';
import {
computed,
nextTick,
onDeactivated,
provide,
ref,
unref,
useId,
watch,
} from 'vue';
import {
useIsMobile,
@@ -96,10 +105,17 @@ const shouldDraggable = computed(
() => draggable.value && !shouldFullscreen.value && header.value,
);
const getAppendTo = computed(() => {
return appendToMain.value
? `#${ELEMENT_ID_MAIN_CONTENT}>div:not(.absolute)>div`
: undefined;
});
const { dragging, transform } = useModalDraggable(
dialogRef,
headerRef,
shouldDraggable,
getAppendTo,
);
const firstOpened = ref(false);
@@ -135,6 +151,16 @@ watch(
// },
// );
/**
* 在开启keepAlive情况下 直接通过浏览器按钮/手势等返回 不会关闭弹窗
*/
onDeactivated(() => {
// 如果弹窗没有被挂载到内容区域,则关闭弹窗
if (!appendToMain.value) {
props.modalApi?.close();
}
});
function handleFullscreen() {
props.modalApi?.setState((prev) => {
// if (prev.fullscreen) {
@@ -179,11 +205,6 @@ function handleFocusOutside(e: Event) {
e.preventDefault();
e.stopPropagation();
}
const getAppendTo = computed(() => {
return appendToMain.value
? `#${ELEMENT_ID_MAIN_CONTENT}>div:not(.absolute)>div`
: undefined;
});
const getForceMount = computed(() => {
return !unref(destroyOnClose) && unref(firstOpened);
@@ -205,7 +226,8 @@ function handleClosed() {
:append-to="getAppendTo"
:class="
cn(
'left-0 right-0 top-[10vh] mx-auto flex max-h-[80%] w-[520px] flex-col p-0 sm:rounded-[var(--radius)]',
'left-0 right-0 top-[10vh] mx-auto flex max-h-[80%] w-[520px] flex-col p-0',
shouldFullscreen ? 'sm:rounded-none' : 'sm:rounded-[var(--radius)]',
modalClass,
{
'border-border border': bordered,

View File

@@ -12,6 +12,7 @@ export function useModalDraggable(
targetRef: Ref<HTMLElement | undefined>,
dragRef: Ref<HTMLElement | undefined>,
draggable: ComputedRef<boolean>,
containerSelector?: ComputedRef<string | undefined>,
) {
const transform = reactive({
offsetX: 0,
@@ -29,20 +30,36 @@ export function useModalDraggable(
}
const targetRect = targetRef.value.getBoundingClientRect();
const { offsetX, offsetY } = transform;
const targetLeft = targetRect.left;
const targetTop = targetRect.top;
const targetWidth = targetRect.width;
const targetHeight = targetRect.height;
const docElement = document.documentElement;
const clientWidth = docElement.clientWidth;
const clientHeight = docElement.clientHeight;
const minLeft = -targetLeft + offsetX;
const minTop = -targetTop + offsetY;
const maxLeft = clientWidth - targetLeft - targetWidth + offsetX;
const maxTop = clientHeight - targetTop - targetHeight + offsetY;
let containerRect: DOMRect | null = null;
if (containerSelector?.value) {
const container = document.querySelector(containerSelector.value);
if (container) {
containerRect = container.getBoundingClientRect();
}
}
let maxLeft, maxTop, minLeft, minTop;
if (containerRect) {
minLeft = containerRect.left - targetLeft + offsetX;
maxLeft = containerRect.right - targetLeft - targetWidth + offsetX;
minTop = containerRect.top - targetTop + offsetY;
maxTop = containerRect.bottom - targetTop - targetHeight + offsetY;
} else {
const docElement = document.documentElement;
const clientWidth = docElement.clientWidth;
const clientHeight = docElement.clientHeight;
minLeft = -targetLeft + offsetX;
minTop = -targetTop + offsetY;
maxLeft = clientWidth - targetLeft - targetWidth + offsetX;
maxTop = clientHeight - targetTop - targetHeight + offsetY;
}
const onMousemove = (e: MouseEvent) => {
let moveX = offsetX + e.clientX - downX;

View File

@@ -5,7 +5,6 @@ import {
h,
inject,
nextTick,
onDeactivated,
provide,
reactive,
ref,
@@ -71,13 +70,6 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
},
);
/**
* 在开启keepAlive情况下 直接通过浏览器按钮/手势等返回 不会关闭弹窗
*/
onDeactivated(() => {
(extendedApi as ExtendedModalApi)?.close?.();
});
return [Modal, extendedApi as ExtendedModalApi] as const;
}
@@ -94,8 +86,9 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
injectData.options?.onOpenChange?.(isOpen);
};
const onClosed = mergedOptions.onClosed;
mergedOptions.onClosed = () => {
options.onClosed?.();
onClosed?.();
if (mergedOptions.destroyOnClose) {
injectData.reCreateModal?.();
}
@@ -129,6 +122,7 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
},
);
injectData.extendApi?.(extendedApi);
return [Modal, extendedApi] as const;
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/shadcn-ui",
"version": "5.5.6",
"version": "5.5.7",
"#main": "./dist/index.mjs",
"#module": "./dist/index.mjs",
"homepage": "https://github.com/vbenjs/vue-vben-admin",

View File

@@ -29,14 +29,25 @@ export type ValueType = boolean | number | string;
export interface VbenButtonGroupProps
extends Pick<VbenButtonProps, 'disabled'> {
/** 单选模式下允许清除选中 */
allowClear?: boolean;
/** 值改变前的回调 */
beforeChange?: (
value: ValueType,
isChecked: boolean,
) => boolean | PromiseLike<boolean | undefined> | undefined;
/** 按钮样式 */
btnClass?: any;
/** 按钮间隔距离 */
gap?: number;
/** 多选模式下限制最多选择的数量。0表示不限制 */
maxCount?: number;
/** 是否允许多选 */
multiple?: boolean;
/** 选项 */
options?: { [key: string]: any; label: CustomRenderType; value: ValueType }[];
/** 显示图标 */
showIcon?: boolean;
/** 尺寸 */
size?: 'large' | 'middle' | 'small';
}

View File

@@ -19,6 +19,8 @@ const props = withDefaults(defineProps<VbenButtonGroupProps>(), {
multiple: false,
showIcon: true,
size: 'middle',
allowClear: false,
maxCount: 0,
});
const emit = defineEmits(['btnClick']);
const btnDefaultProps = computed(() => {
@@ -82,12 +84,22 @@ async function onBtnClick(value: ValueType) {
if (innerValue.value.includes(value)) {
innerValue.value = innerValue.value.filter((item) => item !== value);
} else {
if (props.maxCount > 0 && innerValue.value.length >= props.maxCount) {
innerValue.value = innerValue.value.slice(0, props.maxCount - 1);
}
innerValue.value.push(value);
}
modelValue.value = innerValue.value;
} else {
innerValue.value = [value];
modelValue.value = value;
if (props.allowClear && innerValue.value.includes(value)) {
innerValue.value = [];
modelValue.value = undefined;
emit('btnClick', undefined);
return;
} else {
innerValue.value = [value];
modelValue.value = value;
}
}
emit('btnClick', value);
}
@@ -110,14 +122,21 @@ async function onBtnClick(value: ValueType) {
v-bind="btnDefaultProps"
:variant="innerValue.includes(btn.value) ? 'default' : 'outline'"
@click="onBtnClick(btn.value)"
type="button"
>
<div class="icon-wrapper" v-if="props.showIcon">
<LoaderCircle
class="animate-spin"
v-if="loadingValues.includes(btn.value)"
/>
<CircleCheckBig v-else-if="innerValue.includes(btn.value)" />
<Circle v-else />
<slot
name="icon"
:loading="loadingValues.includes(btn.value)"
:checked="innerValue.includes(btn.value)"
>
<LoaderCircle
class="animate-spin"
v-if="loadingValues.includes(btn.value)"
/>
<CircleCheckBig v-else-if="innerValue.includes(btn.value)" />
<Circle v-else />
</slot>
</div>
<slot name="option" :label="btn.label" :value="btn.value" :data="btn">
<VbenRenderContent :content="btn.label" />

View File

@@ -80,7 +80,7 @@ defineExpose({
v-bind="forwarded"
:class="
cn(
'z-popup bg-background w-full p-6 shadow-lg outline-none sm:rounded-xl',
'z-popup bg-background 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',
{

View File

@@ -23,6 +23,7 @@ const props = withDefaults(defineProps<TreeProps>(), {
defaultExpandedKeys: () => [],
defaultExpandedLevel: 0,
disabled: false,
disabledField: 'disabled',
expanded: () => [],
iconField: 'icon',
labelField: 'label',
@@ -101,16 +102,37 @@ function updateTreeValue() {
if (val === undefined) {
treeValue.value = undefined;
} else {
treeValue.value = Array.isArray(val)
? val.map((v) => getItemByValue(v))
: getItemByValue(val);
if (Array.isArray(val)) {
const filteredValues = val.filter((v) => {
const item = getItemByValue(v);
return item && !get(item, props.disabledField);
});
treeValue.value = filteredValues.map((v) => getItemByValue(v));
if (filteredValues.length !== val.length) {
modelValue.value = filteredValues;
}
} else {
const item = getItemByValue(val);
if (item && !get(item, props.disabledField)) {
treeValue.value = item;
} else {
treeValue.value = undefined;
modelValue.value = undefined;
}
}
}
}
function updateModelValue(val: Arrayable<Recordable<any>>) {
modelValue.value = Array.isArray(val)
? val.map((v) => get(v, props.valueField))
: get(val, props.valueField);
if (Array.isArray(val)) {
const filteredVal = val.filter((v) => !get(v, props.disabledField));
modelValue.value = filteredVal.map((v) => get(v, props.valueField));
} else {
if (val && !get(val, props.disabledField)) {
modelValue.value = get(val, props.valueField);
}
}
}
function expandToLevel(level: number) {
@@ -149,10 +171,18 @@ function collapseAll() {
expanded.value = [];
}
function isNodeDisabled(item: FlattenedItem<Recordable<any>>) {
return props.disabled || get(item.value, props.disabledField);
}
function onToggle(item: FlattenedItem<Recordable<any>>) {
emits('expand', item);
}
function onSelect(item: FlattenedItem<Recordable<any>>, isSelected: boolean) {
if (isNodeDisabled(item)) {
return;
}
if (
!props.checkStrictly &&
props.multiple &&
@@ -224,28 +254,34 @@ defineExpose({
:class="
cn('cursor-pointer', getNodeClass?.(item), {
'data-[selected]:bg-accent': !multiple,
'cursor-not-allowed': disabled,
'cursor-not-allowed': isNodeDisabled(item),
})
"
v-bind="
Object.assign(item.bind, {
onfocus: disabled ? 'this.blur()' : undefined,
onfocus: isNodeDisabled(item) ? 'this.blur()' : undefined,
disabled: isNodeDisabled(item),
})
"
@select="
(event) => {
(event: any) => {
if (isNodeDisabled(item)) {
event.preventDefault();
event.stopPropagation();
return;
}
if (event.detail.originalEvent.type === 'click') {
event.preventDefault();
}
!disabled && onSelect(item, event.detail.isSelected);
onSelect(item, event.detail.isSelected);
}
"
@toggle="
(event) => {
(event: any) => {
if (event.detail.originalEvent.type === 'click') {
event.preventDefault();
}
!disabled && onToggle(item);
!isNodeDisabled(item) && onToggle(item);
}
"
class="tree-node focus:ring-grass8 my-0.5 flex items-center rounded px-2 py-1 outline-none focus:ring-2"
@@ -266,24 +302,32 @@ defineExpose({
</div>
<Checkbox
v-if="multiple"
:checked="isSelected"
:disabled="disabled"
:indeterminate="isIndeterminate"
:checked="isSelected && !isNodeDisabled(item)"
:disabled="isNodeDisabled(item)"
:indeterminate="isIndeterminate && !isNodeDisabled(item)"
@click="
() => {
!disabled && handleSelect();
// onSelect(item, !isSelected);
(event: MouseEvent) => {
if (isNodeDisabled(item)) {
event.preventDefault();
event.stopPropagation();
return;
}
handleSelect();
}
"
/>
<div
class="flex items-center gap-1 pl-2"
@click="
(_event) => {
// $event.stopPropagation();
// $event.preventDefault();
!disabled && handleSelect();
// onSelect(item, !isSelected);
(event: MouseEvent) => {
if (isNodeDisabled(item)) {
event.preventDefault();
event.stopPropagation();
return;
}
event.stopPropagation();
event.preventDefault();
handleSelect();
}
"
>

View File

@@ -22,6 +22,8 @@ export interface TreeProps {
defaultValue?: Arrayable<number | string>;
/** 禁用 */
disabled?: boolean;
/** 禁用字段名 */
disabledField?: string;
/** 自定义节点类名 */
getNodeClass?: (item: FlattenedItem<Recordable<any>>) => string;
iconField?: string;

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/tabs-ui",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/constants",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/access",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/common-ui",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
@@ -49,6 +49,7 @@
"@vueuse/core": "catalog:",
"@vueuse/integrations": "catalog:",
"codemirror": "6.0.1",
"json-bigint": "catalog:",
"qrcode": "catalog:",
"tippy.js": "catalog:",
"vditor": "3.10.9",

View File

@@ -3,11 +3,11 @@ import type { Component } from 'vue';
import type { AnyPromiseFunction } from '@vben/types';
import { computed, ref, unref, useAttrs, watch } from 'vue';
import { computed, nextTick, ref, unref, useAttrs, watch } from 'vue';
import { LoaderCircle } from '@vben/icons';
import { get, isEqual, isFunction } from '@vben-core/shared/utils';
import { cloneDeep, get, isEqual, isFunction } from '@vben-core/shared/utils';
import { objectOmit } from '@vueuse/core';
@@ -104,6 +104,8 @@ const refOptions = ref<OptionsItem[]>([]);
const loading = ref(false);
// 首次是否加载过了
const isFirstLoaded = ref(false);
// 标记是否有待处理的请求
const hasPendingRequest = ref(false);
const getOptions = computed(() => {
const { labelField, valueField, childrenField, numberToString } = props;
@@ -146,18 +148,26 @@ const bindProps = computed(() => {
});
async function fetchApi() {
let { api, beforeFetch, afterFetch, params, resultField } = props;
const { api, beforeFetch, afterFetch, resultField } = props;
if (!api || !isFunction(api) || loading.value) {
if (!api || !isFunction(api)) {
return;
}
// 如果正在加载,标记有待处理的请求并返回
if (loading.value) {
hasPendingRequest.value = true;
return;
}
refOptions.value = [];
try {
loading.value = true;
let finalParams = unref(mergedParams);
if (beforeFetch && isFunction(beforeFetch)) {
params = (await beforeFetch(params)) || params;
finalParams = (await beforeFetch(cloneDeep(finalParams))) || finalParams;
}
let res = await api(params);
let res = await api(finalParams);
if (afterFetch && isFunction(afterFetch)) {
res = (await afterFetch(res)) || res;
}
@@ -177,6 +187,13 @@ async function fetchApi() {
isFirstLoaded.value = false;
} finally {
loading.value = false;
// 如果有待处理的请求,立即触发新的请求
if (hasPendingRequest.value) {
hasPendingRequest.value = false;
// 使用 nextTick 确保状态更新完成后再触发新请求
await nextTick();
fetchApi();
}
}
}
@@ -190,7 +207,7 @@ async function handleFetchForVisible(visible: boolean) {
}
}
const params = computed(() => {
const mergedParams = computed(() => {
return {
...props.params,
...unref(innerParams),
@@ -198,7 +215,7 @@ const params = computed(() => {
});
watch(
params,
mergedParams,
(value, oldValue) => {
if (isEqual(value, oldValue)) {
return;

View File

@@ -76,6 +76,12 @@ const keyword = ref('');
const keywordDebounce = refDebounced(keyword, 300);
const innerIcons = ref<string[]>([]);
/* 当检索关键词变化时,重置分页 */
watch(keywordDebounce, () => {
currentPage.value = 1;
setCurrentPage(1);
});
watchDebounced(
() => props.prefix,
async (prefix) => {

View File

@@ -18,6 +18,9 @@ import { $t } from '@vben/locales';
import { isBoolean } from '@vben-core/shared/utils';
// @ts-ignore
import JsonBigint from 'json-bigint';
defineOptions({ name: 'JsonViewer' });
const props = withDefaults(defineProps<JsonViewerProps>(), {
@@ -68,6 +71,20 @@ function handleClick(event: MouseEvent) {
emit('click', event);
}
// 支持显示 bigint 数据,如较长的订单号
const jsonData = computed<Record<string, any>>(() => {
if (typeof props.value !== 'string') {
return props.value || {};
}
try {
return JsonBigint({ storeAsString: true }).parse(props.value);
} catch (error) {
console.error('JSON parse error:', error);
return {};
}
});
const bindProps = computed<Recordable<any>>(() => {
const copyable = {
copyText: $t('ui.jsonViewer.copy'),
@@ -79,6 +96,7 @@ const bindProps = computed<Recordable<any>>(() => {
return {
...props,
...attrs,
value: jsonData.value,
onCopied: (event: JsonViewerAction) => emit('copied', event),
onKeyclick: (key: string) => emit('keyClick', key),
onClick: (event: MouseEvent) => handleClick(event),

View File

@@ -3,6 +3,8 @@ import type { AuthenticationProps } from './types';
import { computed, watch } from 'vue';
import { $t } from '@vben/locales';
import { useVbenModal } from '@vben-core/popup-ui';
import { Slot, VbenAvatar } from '@vben-core/shadcn-ui';

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/hooks",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -2,48 +2,139 @@ import type { Arrayable, MaybeElementRef } from '@vueuse/core';
import type { Ref } from 'vue';
import { computed, onUnmounted, ref, unref, watch } from 'vue';
import { computed, effectScope, onUnmounted, ref, unref, watch } from 'vue';
import { isFunction } from '@vben/utils';
import { useElementHover } from '@vueuse/core';
interface HoverDelayOptions {
/** 鼠标进入延迟时间 */
enterDelay?: (() => number) | number;
/** 鼠标离开延迟时间 */
leaveDelay?: (() => number) | number;
}
const DEFAULT_LEAVE_DELAY = 500; // 鼠标离开延迟时间,默认为 500ms
const DEFAULT_ENTER_DELAY = 0; // 鼠标进入延迟时间,默认为 0立即响应
/**
* 监测鼠标是否在元素内部,如果在元素内部则返回 true否则返回 false
* @param refElement 所有需要检测的元素。如果提供了一个数组,那么鼠标在任何一个元素内部都会返回 true
* @param delay 延迟更新状态的时间
* @param refElement 所有需要检测的元素。支持单个元素、元素数组或响应式引用的元素数组。如果鼠标在任何一个元素内部都会返回 true
* @param delay 延迟更新状态的时间,可以是数字或包含进入/离开延迟的配置对象
* @returns 返回一个数组,第一个元素是一个 ref表示鼠标是否在元素内部第二个元素是一个控制器可以通过 enable 和 disable 方法来控制监听器的启用和禁用
*/
export function useHoverToggle(
refElement: Arrayable<MaybeElementRef>,
delay: (() => number) | number = 500,
refElement: Arrayable<MaybeElementRef> | Ref<HTMLElement[] | null>,
delay: (() => number) | HoverDelayOptions | number = DEFAULT_LEAVE_DELAY,
) {
const isHovers: Array<Ref<boolean>> = [];
const value = ref(false);
const timer = ref<ReturnType<typeof setTimeout> | undefined>();
const refs = Array.isArray(refElement) ? refElement : [refElement];
refs.forEach((refEle) => {
const eleRef = computed(() => {
const ele = unref(refEle);
return ele instanceof Element ? ele : (ele?.$el as Element);
});
const isHover = useElementHover(eleRef);
isHovers.push(isHover);
});
const isOutsideAll = computed(() => isHovers.every((v) => !v.value));
// 兼容旧版本API
const normalizedOptions: HoverDelayOptions =
typeof delay === 'number' || isFunction(delay)
? { enterDelay: DEFAULT_ENTER_DELAY, leaveDelay: delay }
: {
enterDelay: DEFAULT_ENTER_DELAY,
leaveDelay: DEFAULT_LEAVE_DELAY,
...delay,
};
function setValueDelay(val: boolean) {
timer.value && clearTimeout(timer.value);
timer.value = setTimeout(
() => {
value.value = val;
timer.value = undefined;
},
isFunction(delay) ? delay() : delay,
);
const value = ref(false);
const enterTimer = ref<ReturnType<typeof setTimeout> | undefined>();
const leaveTimer = ref<ReturnType<typeof setTimeout> | undefined>();
const hoverScopes = ref<ReturnType<typeof effectScope>[]>([]);
// 使用计算属性包装 refElement使其响应式变化
const refs = computed(() => {
const raw = unref(refElement);
if (raw === null) return [];
return Array.isArray(raw) ? raw : [raw];
});
// 存储所有 hover 状态
const isHovers = ref<Array<Ref<boolean>>>([]);
// 更新 hover 监听的函数
function updateHovers() {
// 停止并清理之前的作用域
hoverScopes.value.forEach((scope) => scope.stop());
hoverScopes.value = [];
isHovers.value = refs.value.map((refEle) => {
if (!refEle) {
return ref(false);
}
const eleRef = computed(() => {
const ele = unref(refEle);
return ele instanceof Element ? ele : (ele?.$el as Element);
});
// 为每个元素创建独立的作用域
const scope = effectScope();
const hoverRef = scope.run(() => useElementHover(eleRef)) || ref(false);
hoverScopes.value.push(scope);
return hoverRef;
});
}
const watcher = watch(
// 监听元素数量变化,避免过度执行
const elementsCount = computed(() => {
const raw = unref(refElement);
if (raw === null) return 0;
return Array.isArray(raw) ? raw.length : 1;
});
// 初始设置
updateHovers();
// 只在元素数量变化时重新设置监听器
const stopWatcher = watch(elementsCount, updateHovers, { deep: false });
const isOutsideAll = computed(() => isHovers.value.every((v) => !v.value));
function clearTimers() {
if (enterTimer.value) {
clearTimeout(enterTimer.value);
enterTimer.value = undefined;
}
if (leaveTimer.value) {
clearTimeout(leaveTimer.value);
leaveTimer.value = undefined;
}
}
function setValueDelay(val: boolean) {
clearTimers();
if (val) {
// 鼠标进入
const enterDelay = normalizedOptions.enterDelay ?? DEFAULT_ENTER_DELAY;
const delayTime = isFunction(enterDelay) ? enterDelay() : enterDelay;
if (delayTime <= 0) {
value.value = true;
} else {
enterTimer.value = setTimeout(() => {
value.value = true;
enterTimer.value = undefined;
}, delayTime);
}
} else {
// 鼠标离开
const leaveDelay = normalizedOptions.leaveDelay ?? DEFAULT_LEAVE_DELAY;
const delayTime = isFunction(leaveDelay) ? leaveDelay() : leaveDelay;
if (delayTime <= 0) {
value.value = false;
} else {
leaveTimer.value = setTimeout(() => {
value.value = false;
leaveTimer.value = undefined;
}, delayTime);
}
}
}
const hoverWatcher = watch(
isOutsideAll,
(val) => {
setValueDelay(!val);
@@ -53,15 +144,19 @@ export function useHoverToggle(
const controller = {
enable() {
watcher.resume();
hoverWatcher.resume();
},
disable() {
watcher.pause();
hoverWatcher.pause();
},
};
onUnmounted(() => {
timer.value && clearTimeout(timer.value);
clearTimers();
// 停止监听器
stopWatcher();
// 停止所有剩余的作用域
hoverScopes.value.forEach((scope) => scope.stop());
});
return [value, controller] as [typeof value, typeof controller];

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/layouts",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -62,21 +62,23 @@ const { authPanelCenter, authPanelLeft, authPanelRight, isDark } =
</template>
</AuthenticationFormView>
<!-- 头部 Logo 和应用名称 -->
<div
v-if="logo || appName"
class="absolute left-0 top-0 z-10 flex flex-1"
@click="clickLogo"
>
<slot name="logo">
<!-- 头部 Logo 和应用名称 -->
<div
class="text-foreground lg:text-foreground ml-4 mt-4 flex flex-1 items-center sm:left-6 sm:top-6"
v-if="logo || appName"
class="absolute left-0 top-0 z-10 flex flex-1"
@click="clickLogo"
>
<img v-if="logo" :alt="appName" :src="logo" class="mr-2" width="42" />
<p v-if="appName" class="m-0 text-xl font-medium">
{{ appName }}
</p>
<div
class="text-foreground lg:text-foreground ml-4 mt-4 flex flex-1 items-center sm:left-6 sm:top-6"
>
<img v-if="logo" :alt="appName" :src="logo" class="mr-2" width="42" />
<p v-if="appName" class="m-0 text-xl font-medium">
{{ appName }}
</p>
</div>
</div>
</div>
</slot>
<!-- 系统介绍 -->
<div v-if="!authPanelCenter" class="relative hidden w-0 flex-1 lg:block">

View File

@@ -46,7 +46,11 @@ interface Props {
/**
* 菜单数组
*/
menus?: Array<{ handler: AnyFunction; icon?: Component; text: string }>;
menus?: Array<{
handler: AnyFunction;
icon?: Component | Function | string;
text: string;
}>;
/**
* 标签文本
@@ -207,10 +211,20 @@ if (enableShortcutKey.value) {
v-if="tagText || text || $slots.tagText"
class="text-foreground mb-1 flex items-center text-sm font-medium"
>
{{ text }}
<div
class="max-w-[100px] overflow-hidden text-ellipsis break-keep"
:title="text"
>
{{ text }}
</div>
<slot name="tagText">
<Badge v-if="tagText" class="ml-2 text-green-400">
{{ tagText }}
<div
class="max-w-[50px] overflow-hidden text-ellipsis"
:title="tagText"
>
{{ tagText }}
</div>
</Badge>
</slot>
</div>

View File

@@ -1,8 +1,11 @@
import type { ExtendedFormApi } from '@vben-core/form-ui';
import type { VxeGridInstance } from 'vxe-table';
import type { ExtendedFormApi } from '@vben-core/form-ui';
import type { VxeGridProps } from './types';
import { toRaw } from 'vue';
import { Store } from '@vben-core/shared/store';
import {
bindMethods,
@@ -11,7 +14,6 @@ import {
mergeWithArrayOverride,
StateHandler,
} from '@vben-core/shared/utils';
import { toRaw } from 'vue';
function getDefaultState(): VxeGridProps {
return {
@@ -24,18 +26,18 @@ function getDefaultState(): VxeGridProps {
};
}
export class VxeGridApi {
private isMounted = false;
private stateHandler: StateHandler;
export class VxeGridApi<T extends Record<string, any> = any> {
public formApi = {} as ExtendedFormApi;
// private prevState: null | VxeGridProps = null;
public grid = {} as VxeGridInstance;
public grid = {} as VxeGridInstance<T>;
public state: null | VxeGridProps<T> = null;
public state: null | VxeGridProps = null;
public store: Store<VxeGridProps<T>>;
public store: Store<VxeGridProps>;
private isMounted = false;
private stateHandler: StateHandler;
constructor(options: VxeGridProps = {}) {
const storeState = { ...options };
@@ -97,8 +99,8 @@ export class VxeGridApi {
setState(
stateOrFn:
| ((prev: VxeGridProps) => Partial<VxeGridProps>)
| Partial<VxeGridProps>,
| ((prev: VxeGridProps<T>) => Partial<VxeGridProps<T>>)
| Partial<VxeGridProps<T>>,
) {
if (isFunction(stateOrFn)) {
this.store.setState((prev) => {

View File

@@ -9,7 +9,7 @@ import type { Ref } from 'vue';
import type { ClassType, DeepPartial } from '@vben/types';
import type { VbenFormProps } from '@vben-core/form-ui';
import type { BaseFormComponentType, VbenFormProps } from '@vben-core/form-ui';
import type { VxeGridApi } from './api';
@@ -35,7 +35,11 @@ export interface SeparatorOptions {
show?: boolean;
backgroundColor?: string;
}
export interface VxeGridProps {
export interface VxeGridProps<
T extends Record<string, any> = any,
D extends BaseFormComponentType = BaseFormComponentType,
> {
/**
* 标题
*/
@@ -55,15 +59,15 @@ export interface VxeGridProps {
/**
* vxe-grid 配置
*/
gridOptions?: DeepPartial<VxeTableGridOptions>;
gridOptions?: DeepPartial<VxeTableGridOptions<T>>;
/**
* vxe-grid 事件
*/
gridEvents?: Partial<VxeGridListeners>;
gridEvents?: Partial<VxeGridListeners<T>>;
/**
* 表单配置
*/
formOptions?: VbenFormProps;
formOptions?: VbenFormProps<D>;
/**
* 显示搜索表单
*/
@@ -74,9 +78,12 @@ export interface VxeGridProps {
separator?: boolean | SeparatorOptions;
}
export type ExtendedVxeGridApi = VxeGridApi & {
useStore: <T = NoInfer<VxeGridProps>>(
selector?: (state: NoInfer<VxeGridProps>) => T,
export type ExtendedVxeGridApi<
D extends Record<string, any> = any,
F extends BaseFormComponentType = BaseFormComponentType,
> = VxeGridApi<D> & {
useStore: <T = NoInfer<VxeGridProps<D, F>>>(
selector?: (state: NoInfer<VxeGridProps<any, any>>) => T,
) => Readonly<Ref<T>>;
};

View File

@@ -1,3 +1,5 @@
import type { BaseFormComponentType } from '@vben-core/form-ui';
import type { ExtendedVxeGridApi, VxeGridProps } from './types';
import { defineComponent, h, onBeforeUnmount } from 'vue';
@@ -7,16 +9,19 @@ import { useStore } from '@vben-core/shared/store';
import { VxeGridApi } from './api';
import VxeGrid from './use-vxe-grid.vue';
export function useVbenVxeGrid(options: VxeGridProps) {
export function useVbenVxeGrid<
T extends Record<string, any> = any,
D extends BaseFormComponentType = BaseFormComponentType,
>(options: VxeGridProps<T, D>) {
// const IS_REACTIVE = isReactive(options);
const api = new VxeGridApi(options);
const extendedApi: ExtendedVxeGridApi = api as ExtendedVxeGridApi;
const extendedApi: ExtendedVxeGridApi<T, D> = api as ExtendedVxeGridApi<T, D>;
extendedApi.useStore = (selector) => {
return useStore(api.store, selector);
};
const Grid = defineComponent(
(props: VxeGridProps, { attrs, slots }) => {
(props: VxeGridProps<T>, { attrs, slots }) => {
onBeforeUnmount(() => {
api.unmount();
});

View File

@@ -280,6 +280,15 @@ const delegatedFormSlots = computed(() => {
return resultSlots.map((key) => key.replace(FORM_SLOT_PREFIX, ''));
});
const showDefaultEmpty = computed(() => {
// 检查是否有原生的 VXE Table 空状态配置
const hasEmptyText = options.value.emptyText !== undefined;
const hasEmptyRender = options.value.emptyRender !== undefined;
// 如果有原生配置,就不显示默认的空状态
return !hasEmptyText && !hasEmptyRender;
});
async function init() {
await nextTick();
const globalGridConfig = VxeUI?.getConfig()?.grid ?? {};
@@ -464,7 +473,7 @@ onUnmounted(() => {
</slot>
</template>
<!-- 统一控状态 -->
<template #empty>
<template v-if="showDefaultEmpty" #empty>
<slot name="empty">
<EmptyIcon class="mx-auto" />
<div class="mt-2">{{ $t('common.noData') }}</div>

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/request",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,8 @@
import type { RequestClient } from '../request-client';
import type { RequestClientConfig } from '../types';
import { isUndefined } from '@vben/utils';
class FileUploader {
private client: RequestClient;
@@ -18,10 +20,10 @@ class FileUploader {
Object.entries(data).forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach((item, index) => {
formData.append(`${key}[${index}]`, item);
!isUndefined(item) && formData.append(`${key}[${index}]`, item);
});
} else {
formData.append(key, value);
!isUndefined(value) && formData.append(key, value);
}
});

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/icons",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/locales",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/preferences",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/stores",
"version": "5.5.6",
"version": "5.5.7",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -6,6 +6,10 @@ interface BasicUserInfo {
* 头像
*/
avatar: string;
/**
* 邮箱
*/
email: string;
/**
* 用户权限
*/

Some files were not shown because too many files have changed in this diff Show More