Compare commits

..

64 Commits

Author SHA1 Message Date
vben
cc6c9bf7a0 chore: release v5.5.6 2025-05-06 22:32:58 +08:00
Jin Mao
6b1aab9c67 fix: handle undefined children in generate-menus (#6117)
When children is undefined, use empty array as fallback to prevent potential runtime errors. This matches the behavior when hideChildrenInMenu is true.
2025-05-06 14:29:50 +08:00
LinaBell
8f4d3d418d fix: when keepAlive is enabled, returning directly through browser buttons/gestures will not close pop ups (#6113) 2025-05-06 14:02:23 +08:00
ming4762
3b3f8e4e44 fix: fix IconPicker props warning (#6108)
Invalid prop: type check failed for prop "onUpdate:value". Expected Function, got Array
2025-05-06 09:30:37 +08:00
vben
f94ca10adf chore: remove prepare script from package.json 2025-05-04 07:33:36 +08:00
vben
4471bc7a5d chore: update prepare script in package.json to remove lefthook installation 2025-05-04 00:05:29 +08:00
Vben
5689ac60ff feat(project): migrate from husky and lint-staged to lefthook (#6104) 2025-05-03 19:43:12 +08:00
Vben
045bc4e5ee feat: support smooth auto-scroll to active menu item (#6102) 2025-05-03 18:05:26 +08:00
Vben
17a18fc9ba chore: close eslint object sorting (#6101) 2025-05-03 16:06:36 +08:00
aonoa
41152d1722 refactor: modify the default homepage path loaded from the preference… (#6099)
* refactor: modify the default homepage path loaded from the preferences.ts

Signed-off-by: aonoa <1991849113@qq.com>

* refactor: modify the default homepage path loaded from the preferences.ts

Signed-off-by: aonoa <1991849113@qq.com>

---------

Signed-off-by: aonoa <1991849113@qq.com>
2025-05-03 16:03:08 +08:00
Netfan
f1af9f8f6e fix: add triggerClass binding to PopoverTrigger and update icon-picker styles (#6095)
* Popover支持设置trigger的样式
* 修正icon-picker的input值更新
2025-05-01 21:40:45 +08:00
Netfan
0517a7014f fix: add missing translation for preferences drawer (#6094) 2025-05-01 20:08:44 +08:00
Netfan
3e6d608a2f fix: destroyOnClose incorrect default value, fixed #6092 (#6093) 2025-05-01 14:09:37 +08:00
ming4762
5de954baa4 fix: fix LoginExpiredModal in some cases, message may be obscured (#6086) 2025-05-01 10:40:42 +08:00
Netfan
add1e61b6f fix: show validation message as tooltip in compact form (#6087)
* 紧凑模式表单的校验消息将显示为一个tooltip
2025-04-30 23:41:44 +08:00
Jin Mao
20c15f352f perf: page componet supports custom height offset for flexible content height … (#6081)
* perf: Page supports custom height offset for flexible content height control.

允许通过 height 属性调整页面内容高度计算。修改了 Page 组件以支持自定义高度偏移量,用于更灵活的内容高度控制。

* chore: typo

* perf(page): replace height with heightOffset for flexible content sizing

The `height` prop was replaced with `heightOffset` to better describe its purpose when used with `autoContentHeight`. The new prop allows custom offset values (in pixels) to adjust content area sizing, with clearer documentation.
2025-04-29 18:15:12 +08:00
Netfan
8aa7dabeff fix: calculation for collapsing search form is incorrect while initially hidden (#6068)
* 修复当默认隐藏搜索表单时,折叠位置的计算不正确的问题
2025-04-28 23:20:33 +08:00
vben
78c7c1589a chore: update readme.md 2025-04-28 23:11:34 +08:00
Vben
dd833ca56b chore: update dependencies and documentation, optimize build toolchain (#6060)
* chore: update packageManager version to pnpm@10.9.0 for compatibility improvements

* chore: Update dependent versions and configurations to improve compatibility and stability

- Update Node version to 22.1.0
- Updated pnpm version to 10.10.0
- Fixed syntax error in prettier command in lintstagedrc
- Update dependent versions in pnpm-lock.yaml to ensure consistency
- Update format and content in README documents to improve readability

* fix: lint error
2025-04-28 23:08:05 +08:00
vem
681c1dc267 fix: Update existing route index to prevent 404 on user switch (#6003)
Co-authored-by: tars-macmini <vem@qq.com>
2025-04-28 18:19:47 +08:00
Netfan
4545422ee0 fix: lock state will not change overflow style in drawer and modal (#6067)
* Modal和Drawer的锁定状态不再修改overflow样式
2025-04-28 17:02:54 +08:00
Gahotx
ca94ca906f fix: add rounded corners to project and quick nav items (#5296) 2025-04-27 22:50:42 +08:00
Vben
76de450c71 chore: update dependency version for improved stability and compatibility (#6023)
* chore: update dependency version for improved stability and compatibility

* fix: optimize clearPoints function in useCaptchaPoints hook to improve performance

* fix: make several props optional in various components for better flexibility
2025-04-27 22:06:49 +08:00
Trivikram Kamat
dd2b1ed580 fix: install corepack from npm (#5905)
* fix: install corepack from npm

* docs: install corepack from npm
2025-04-27 22:03:35 +08:00
ming4762
baec89f896 perf: resolve duplicate component names (#6039) 2025-04-27 22:02:38 +08:00
vben
7c7051a11e chore: release v5.5.5 2025-04-27 21:45:10 +08:00
Netfan
aa27a2f7a1 feat: encrypt the privacy data when it is persisted (#6056)
* 对私密数据持久化时执行加密
* 将锁屏密码合并到accessStore中进行加密
2025-04-27 20:59:10 +08:00
Jin Mao
9ee6d06d50 docs: add deepWiki doc link (#6057) 2025-04-27 20:54:07 +08:00
ming4762
0cc1cb5a7b perf: improve destroyOnClose for VbenDrawer&VbenModal (#6051)
* fix: fix that the default value of modal destroyOnClose does not take effect

* perf: improve destroyOnClose for VbenDrawer
2025-04-27 11:26:50 +08:00
Netfan
0a9fc4e02d fix: title of search button in vxeTable toolbar (#6046)
* 修改vxeTable工具栏里的搜索按钮的提示文案
2025-04-26 01:08:41 +08:00
Netfan
be840460d8 feat: vbenSelect support prop allowClear (#6043) 2025-04-25 23:37:03 +08:00
Netfan
cb45987fe2 docs: update example (#6036)
* 跟进后端菜单逻辑的修改,现已无需传递basicLayout布局
2025-04-25 11:44:47 +08:00
panda7
5ffd7db8e0 fix: the initial value echo for the check-button-group (#6029)
Co-authored-by: sqchen <9110848@qq.com>
2025-04-25 08:35:03 +08:00
Netfan
14377705e7 fix: alert confirm state in beforeClose callback (#6019) 2025-04-23 12:20:52 +08:00
pangyajun123
b985ff0584 fix: vxe-table theme token follow primary color (#6007) 2025-04-21 19:15:05 +08:00
wyc001122
b148b8ec92 fix: fix geader menu activation path (#5997)
Co-authored-by: 王泳超 <wangyongchao@testor.com.cn>
2025-04-19 14:35:33 +08:00
Netfan
79de6bcbf7 fix: alert send wrong confirm state to beforeClose (#5991)
* 修复alert在按下Esc或者点击遮罩关闭时,可能发送错误的isConfirm状态
2025-04-17 22:23:05 +08:00
Netfan
14bd6dd25d fix: destroyOnClose works within connectedComponent (#5989)
* 修复destroyOnClose没能销毁connectedComponent自身的问题
2025-04-17 20:25:49 +08:00
PIPEDREA_WZJ
7f269e0d69 Update tailwindcss.md (#5602)
tailwindcss最新的版本已经是v4.x,vben中使用的是3.x的tailwindcss。在未进行兼容前,会出现运行失败的问题
2025-04-17 14:01:39 +08:00
yuh
4baec83db5 feat: add examples: form-upload (#5955)
* feat: add examples: form-upload

* fix: upload: accept and label

* fix: upload: 设置表单值、图片预览
2025-04-17 14:00:46 +08:00
Netfan
f7a4d13a4c fix: fixed arguments of callbacks in formApi (#5970)
* 修复 `handleValuesChange` 传递的参数不是处理后的表单值的问题

* 修复 `handleReset` 未能传递正确参数的问题
2025-04-16 14:11:04 +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
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
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
Netfan
4a2c7b313f fix: alert animation (#5927) 2025-04-12 10:37:47 +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
62 changed files with 248 additions and 2396 deletions

1
.gitignore vendored
View File

@@ -5,7 +5,6 @@ dist-ssr
dist.zip
dist.tar
dist.war
dist-electron
.nitro
.output
*-dist.zip

3
.npmrc
View File

@@ -1,8 +1,5 @@
registry = "https://registry.npmmirror.com"
ELECTRON_MIRROR="https://npmmirror.com/mirrors/electron/"
public-hoist-pattern[]=lefthook
ELECTRON_MIRROR="https://npmmirror.com/mirrors/electron/"
public-hoist-pattern[]=husky
public-hoist-pattern[]=eslint
public-hoist-pattern[]=prettier
public-hoist-pattern[]=prettier-plugin-tailwindcss

View File

@@ -18,7 +18,7 @@ function setupCommonGuard(router: Router) {
// 记录已经加载的页面
const loadedPaths = new Set<string>();
router.beforeEach((to) => {
router.beforeEach(async (to) => {
to.meta.loaded = loadedPaths.has(to.path);
// 页面加载进度条

View File

@@ -18,7 +18,7 @@ function setupCommonGuard(router: Router) {
// 记录已经加载的页面
const loadedPaths = new Set<string>();
router.beforeEach((to) => {
router.beforeEach(async (to) => {
to.meta.loaded = loadedPaths.has(to.path);
// 页面加载进度条

View File

@@ -18,7 +18,7 @@ function setupCommonGuard(router: Router) {
// 记录已经加载的页面
const loadedPaths = new Set<string>();
router.beforeEach((to) => {
router.beforeEach(async (to) => {
to.meta.loaded = loadedPaths.has(to.path);
// 页面加载进度条

View File

@@ -60,29 +60,6 @@ VITE_INJECT_APP_LOADING=true
VITE_ARCHIVER=true
```
```bash [.env.production]
# Public Path for Resources, must start and end with /
VITE_BASE=/
# API URL
VITE_GLOB_API_URL=https://mock-napi.vben.pro/api
# Whether to enable compression, can be set to none, brotli, gzip
VITE_COMPRESS=gzip
# Whether to enable PWA
VITE_PWA=false
# vue-router mode
VITE_ROUTER_HISTORY=hash
# Whether to inject global loading
VITE_INJECT_APP_LOADING=true
# Whether to generate dist.zip after packaging
VITE_ARCHIVER=true
```
:::
## Dynamic Configuration in Production Environment
@@ -165,7 +142,6 @@ import { defineOverridesPreferences } from '@vben/preferences';
/**
* @description Project configuration file
* Only a part of the configuration in the project needs to be covered, and unnecessary configurations do not need to be covered. The default configuration will be automatically used
* !!! Please clear the cache after changing the configuration, otherwise it may not take effect
*/
export const overridesPreferences = defineOverridesPreferences({
// overrides
@@ -196,7 +172,7 @@ const defaultPreferences: Preferences = {
isMobile: false,
layout: 'sidebar-nav',
locale: 'zh-CN',
loginExpiredMode: 'page',
loginExpiredMode: 'modal',
name: 'Vben Admin',
preferencesButtonPosition: 'auto',
watermark: false,
@@ -215,16 +191,14 @@ const defaultPreferences: Preferences = {
enable: true,
icp: '',
icpLink: '',
settingShow: true,
},
footer: {
enable: false,
enable: true,
fixed: false,
},
header: {
enable: true,
hidden: false,
menuAlign: 'start',
mode: 'fixed',
},
logo: {
@@ -246,28 +220,23 @@ const defaultPreferences: Preferences = {
sidebar: {
autoActivateChild: false,
collapsed: false,
collapsedButton: true,
collapsedShowTitle: false,
enable: true,
expandOnHover: true,
extraCollapse: false,
fixedButton: true,
extraCollapse: true,
hidden: false,
width: 224,
width: 230,
},
tabbar: {
draggable: true,
enable: true,
height: 38,
height: 36,
keepAlive: true,
maxCount: 0,
middleClickToClose: false,
persist: true,
showIcon: true,
showMaximize: true,
showMore: true,
styleType: 'chrome',
wheelable: true,
},
theme: {
builtinType: 'default',
@@ -278,7 +247,7 @@ const defaultPreferences: Preferences = {
mode: 'dark',
radius: '0.5',
semiDarkHeader: false,
semiDarkSidebar: false,
semiDarkSidebar: true,
},
transition: {
enable: true,
@@ -292,9 +261,9 @@ const defaultPreferences: Preferences = {
languageToggle: true,
lockScreen: true,
notification: true,
refresh: true,
sidebarToggle: true,
themeToggle: true,
refresh: true,
},
};
```
@@ -376,8 +345,6 @@ interface CopyrightPreferences {
icp: string;
/** Link to the ICP */
icpLink: string;
/** Whether to show in settings panel */
settingShow?: boolean;
}
interface FooterPreferences {
@@ -392,8 +359,6 @@ interface HeaderPreferences {
enable: boolean;
/** Whether the header is hidden, css-hidden */
hidden: boolean;
/** Header menu alignment */
menuAlign: LayoutHeaderMenuAlignType;
/** Header display mode */
mode: LayoutHeaderModeType;
}
@@ -414,12 +379,8 @@ interface NavigationPreferences {
styleType: NavigationStyleType;
}
interface SidebarPreferences {
/** Automatically activate child menu when clicking on directory */
autoActivateChild: boolean;
/** Whether the sidebar is collapsed */
collapsed: boolean;
/** Whether the sidebar collapse button is visible */
collapsedButton: boolean;
/** Whether to show title when sidebar is collapsed */
collapsedShowTitle: boolean;
/** Whether the sidebar is visible */
@@ -428,8 +389,6 @@ interface SidebarPreferences {
expandOnHover: boolean;
/** Whether the sidebar extension area is collapsed */
extraCollapse: boolean;
/** Whether the sidebar fixed button is visible */
fixedButton: boolean;
/** Whether the sidebar is hidden - css */
hidden: boolean;
/** Sidebar width */
@@ -458,10 +417,6 @@ interface TabbarPreferences {
height: number;
/** Whether tab caching is enabled */
keepAlive: boolean;
/** Maximum number of tabs */
maxCount: number;
/** Whether to close tab when middle-clicked */
middleClickToClose: boolean;
/** Whether tabs are persistent */
persist: boolean;
/** Whether icons in multiple tabs are enabled */
@@ -472,8 +427,6 @@ interface TabbarPreferences {
showMore: boolean;
/** Tab style */
styleType: TabsStyleType;
/** Whether mouse wheel response is enabled */
wheelable: boolean;
}
interface ThemePreferences {
/** Built-in theme name */
@@ -561,6 +514,5 @@ interface Preferences {
- The `overridesPreferences` method only needs to override a part of the configurations in the project. There's no need to override configurations that are not needed; they will automatically use the default settings.
- Any configuration item can be overridden. You just need to override it within the `overridesPreferences` method. Do not modify the default configuration file.
- Please clear the cache after changing the configuration, otherwise it may not take effect.
:::

View File

@@ -339,10 +339,6 @@ interface RouteMeta {
| 'success'
| 'warning'
| string;
/**
* 路由的完整路径作为key默认true
*/
fullPathKey?: boolean;
/**
* 当前路由的子级在菜单中不展现
* @default false
@@ -506,13 +502,6 @@ interface RouteMeta {
用于配置页面的徽标颜色。
### fullPathKey
- 类型:`boolean`
- 默认值:`true`
是否将路由的完整路径作为tab key默认true
### activePath
- 类型:`string`
@@ -613,32 +602,3 @@ const { refresh } = useRefresh();
refresh();
</script>
```
## 标签页与路由控制
在某些场景下需要单个路由打开多个标签页或者修改路由的query不打开新的标签页
每个标签页Tab使用唯一的key标识设置Tab key有三种方式优先级由高到低
- 使用路由query参数pageKey
```vue
<script setup lang="ts">
import { useRouter } from 'vue-router';
// 跳转路由
const router = useRouter();
router.push({
path: 'path',
query: {
pageKey: 'key',
},
});
```
- 路由的完整路径作为key
`meta` 属性中的 `fullPathKey`不为false则使用路由`fullPath`作为key
- 路由的path作为key
`meta` 属性中的 `fullPathKey`为false则使用路由`path`作为key

View File

@@ -195,7 +195,7 @@ const defaultPreferences: Preferences = {
isMobile: false,
layout: 'sidebar-nav',
locale: 'zh-CN',
loginExpiredMode: 'page',
loginExpiredMode: 'modal',
name: 'Vben Admin',
preferencesButtonPosition: 'auto',
watermark: false,
@@ -214,16 +214,14 @@ const defaultPreferences: Preferences = {
enable: true,
icp: '',
icpLink: '',
settingShow: true,
},
footer: {
enable: false,
enable: true,
fixed: false,
},
header: {
enable: true,
hidden: false,
menuAlign: 'start',
mode: 'fixed',
},
logo: {
@@ -245,28 +243,23 @@ const defaultPreferences: Preferences = {
sidebar: {
autoActivateChild: false,
collapsed: false,
collapsedButton: true,
collapsedShowTitle: false,
enable: true,
expandOnHover: true,
extraCollapse: false,
fixedButton: true,
extraCollapse: true,
hidden: false,
width: 224,
width: 230,
},
tabbar: {
draggable: true,
enable: true,
height: 38,
height: 36,
keepAlive: true,
maxCount: 0,
middleClickToClose: false,
persist: true,
showIcon: true,
showMaximize: true,
showMore: true,
styleType: 'chrome',
wheelable: true,
},
theme: {
builtinType: 'default',
@@ -277,7 +270,7 @@ const defaultPreferences: Preferences = {
mode: 'dark',
radius: '0.5',
semiDarkHeader: false,
semiDarkSidebar: false,
semiDarkSidebar: true,
},
transition: {
enable: true,
@@ -376,8 +369,6 @@ interface CopyrightPreferences {
icp: string;
/** 备案号链接 */
icpLink: string;
/** 设置面板是否显示*/
settingShow?: boolean;
}
interface FooterPreferences {
@@ -392,8 +383,6 @@ interface HeaderPreferences {
enable: boolean;
/** 顶栏是否隐藏,css-隐藏 */
hidden: boolean;
/** 顶栏菜单位置 */
menuAlign: LayoutHeaderMenuAlignType;
/** header显示模式 */
mode: LayoutHeaderModeType;
}
@@ -415,12 +404,8 @@ interface NavigationPreferences {
}
interface SidebarPreferences {
/** 点击目录时自动激活子菜单 */
autoActivateChild: boolean;
/** 侧边栏是否折叠 */
collapsed: boolean;
/** 侧边栏折叠按钮是否可见 */
collapsedButton: boolean;
/** 侧边栏折叠时是否显示title */
collapsedShowTitle: boolean;
/** 侧边栏是否可见 */
@@ -429,8 +414,6 @@ interface SidebarPreferences {
expandOnHover: boolean;
/** 侧边栏扩展区域是否折叠 */
extraCollapse: boolean;
/** 侧边栏固定按钮是否可见 */
fixedButton: boolean;
/** 侧边栏是否隐藏 - css */
hidden: boolean;
/** 侧边栏宽度 */
@@ -459,10 +442,6 @@ interface TabbarPreferences {
height: number;
/** 开启标签页缓存功能 */
keepAlive: boolean;
/** 限制最大数量 */
maxCount: number;
/** 是否点击中键时关闭标签 */
middleClickToClose: boolean;
/** 是否持久化标签 */
persist: boolean;
/** 是否开启多标签页图标 */
@@ -473,8 +452,6 @@ interface TabbarPreferences {
showMore: boolean;
/** 标签页风格 */
styleType: TabsStyleType;
/** 是否开启鼠标滚轮响应 */
wheelable: boolean;
}
interface ThemePreferences {

View File

@@ -12,7 +12,7 @@
"license": "MIT",
"type": "module",
"scripts": {
"stub": "pnpm unbuild --stub"
"stub": "pnpm unbuild"
},
"files": [
"dist"

View File

@@ -1,9 +1,6 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/node.json",
"compilerOptions": {
"moduleResolution": "bundler"
},
"include": ["src"],
"exclude": ["node_modules"]
}

View File

@@ -47,15 +47,12 @@
"@vitejs/plugin-vue-jsx": "catalog:",
"dayjs": "catalog:",
"dotenv": "catalog:",
"electron": "catalog:",
"rollup": "catalog:",
"rollup-plugin-visualizer": "catalog:",
"sass": "catalog:",
"vite": "catalog:",
"vite-plugin-compression": "catalog:",
"vite-plugin-dts": "catalog:",
"vite-plugin-electron": "catalog:",
"vite-plugin-electron-renderer": "catalog:",
"vite-plugin-html": "catalog:",
"vite-plugin-lazy-import": "catalog:"
}

View File

@@ -1,40 +0,0 @@
import type { PluginOption } from 'vite';
import type { CommonPluginOptions } from '../typing';
import fs from 'node:fs';
import electron from 'vite-plugin-electron/simple';
export const viteElectronPlugin = (
options: CommonPluginOptions,
): PluginOption => {
fs.rmSync('dist-electron', { force: true, recursive: true });
const isServe = !options.isBuild;
const isBuild = options.isBuild;
const sourcemap = isServe || !!process.env.VSCODE_DEBUG;
return electron({
main: {
entry: 'electron/main.ts',
vite: {
build: {
minify: isBuild,
outDir: 'dist-electron/main',
sourcemap,
},
},
},
preload: {
input: 'electron/preload.ts',
vite: {
build: {
minify: isBuild,
outDir: 'dist-electron/preload',
sourcemap: sourcemap ? 'inline' : undefined, // #332
},
},
},
renderer: {},
});
};

View File

@@ -18,7 +18,6 @@ import { VitePWA } from 'vite-plugin-pwa';
import viteVueDevTools from 'vite-plugin-vue-devtools';
import { viteArchiverPlugin } from './archiver';
import { viteElectronPlugin } from './electron';
import { viteExtraAppConfigPlugin } from './extra-app-config';
import { viteImportMapPlugin } from './importmap';
import { viteInjectAppLoadingPlugin } from './inject-app-loading';
@@ -98,7 +97,6 @@ async function loadApplicationPlugins(
archiverPluginOptions,
compress,
compressTypes,
electron,
extraAppConfig,
html,
i18n,
@@ -215,10 +213,6 @@ async function loadApplicationPlugins(
return [await viteArchiverPlugin(archiverPluginOptions)];
},
},
{
condition: electron,
plugins: () => [viteElectronPlugin(commonOptions)],
},
]);
}

View File

@@ -3,6 +3,23 @@ import type { ConfigEnv, PluginOption, UserConfig } from 'vite';
import type { PluginOptions } from 'vite-plugin-dts';
import type { Options as PwaPluginOptions } from 'vite-plugin-pwa';
/**
* ImportMap 配置接口
* @description 用于配置模块导入映射,支持自定义导入路径和范围
* @example
* ```typescript
* {
* imports: {
* 'vue': 'https://unpkg.com/vue@3.2.47/dist/vue.esm-browser.js'
* },
* scopes: {
* 'https://site.com/': {
* 'vue': 'https://unpkg.com/vue@3.2.47/dist/vue.esm-browser.js'
* }
* }
* }
* ```
*/
interface IImportMap {
/** 模块导入映射 */
imports?: Record<string, string>;
@@ -185,8 +202,6 @@ interface ApplicationPluginOptions extends CommonPluginOptions {
* @description 可选的压缩类型
*/
compressTypes?: ('brotli' | 'gzip')[];
/** 启用electron */
electron?: boolean;
/**
* 是否抽离配置文件
* @default false

View File

@@ -45,7 +45,6 @@ export {
Menu,
Minimize,
Minimize2,
Minus,
MoonStar,
Palette,
PanelLeft,
@@ -59,8 +58,6 @@ export {
Settings,
Shrink,
Square,
SquareArrowDownLeft,
SquareArrowUpRight,
SquareCheckBig,
SquareMinus,
Sun,

View File

@@ -98,8 +98,6 @@
"@types/lodash.get": "catalog:",
"@types/lodash.isequal": "catalog:",
"@types/lodash.set": "catalog:",
"@types/nprogress": "catalog:",
"@vben-core/typings": "workspace:*",
"electron": "catalog:"
"@types/nprogress": "catalog:"
}
}

View File

@@ -28,11 +28,10 @@ function openWindow(url: string, options: OpenWindowOptions = {}): void {
* @param path
*/
function openRouteInNewWindow(path: string) {
// const { hash, origin } = location;
const { hash, origin } = location;
const fullPath = path.startsWith('/') ? path : `/${path}`;
// const url = `${origin}${hash ? '/#' : ''}${fullPath}`;
// openWindow(url, { target: '_blank' });
window.ipcRenderer.invoke('open-win', fullPath);
const url = `${origin}${hash ? '/#' : ''}${fullPath}`;
openWindow(url, { target: '_blank' });
}
export { openRouteInNewWindow, openWindow };

View File

@@ -1,9 +1,6 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/library.json",
"compilerOptions": {
"types": ["@vben-core/typings/electron"]
},
"include": ["src"],
"exclude": ["node_modules"]
}

View File

@@ -1,27 +0,0 @@
import type { IpcRendererEvent } from 'electron';
export type IpcRendererInvoke =
| 'app-close'
| 'app-maximize'
| 'app-minimize'
| 'is-maximized'
| 'open-win';
declare global {
interface Window {
ipcRenderer: {
invoke: (channel: IpcRendererInvoke, ...args: any[]) => Promise<any>;
off: (
channel: string,
listener: (event: IpcRendererEvent, ...args: any[]) => void,
) => void;
on: (
channel: string,
listener: (event: IpcRendererEvent, ...args: any[]) => void,
) => void;
send: (channel: string, data: any) => Promise<any>;
};
}
}
export {};

View File

@@ -27,9 +27,6 @@
},
"./vue-router": {
"types": "./vue-router.d.ts"
},
"./electron": {
"types": "./electron.d.ts"
}
},
"publishConfig": {

View File

@@ -1,8 +1,3 @@
import type { RouteLocationNormalized } from 'vue-router';
export interface TabDefinition extends RouteLocationNormalized {
/**
* 标签页的key
*/
key?: string;
}
export type TabDefinition = RouteLocationNormalized;

View File

@@ -43,10 +43,6 @@ interface RouteMeta {
| 'success'
| 'warning'
| string;
/**
* 路由的完整路径作为key默认true
*/
fullPathKey?: boolean;
/**
* 当前路由的子级在菜单中不展现
* @default false

View File

@@ -64,7 +64,7 @@ const logoStyle = computed((): CSSProperties => {
<header
:class="theme"
:style="style"
class="app-header border-border bg-header top-0 flex w-full flex-[0_0_auto] items-center border-b pl-2 transition-[margin-top] duration-200"
class="border-border bg-header top-0 flex w-full flex-[0_0_auto] items-center border-b pl-2 transition-[margin-top] duration-200"
>
<div v-if="slots.logo" :style="logoStyle">
<slot name="logo"></slot>
@@ -75,17 +75,3 @@ const logoStyle = computed((): CSSProperties => {
<slot></slot>
</header>
</template>
<style lang="scss" scoped>
.bg-header {
app-region: drag;
user-select: none;
:deep(.cursor-pointer),
:deep(.vben-sub-menu),
:deep(.vben-menu-item),
:deep(button),
:deep(a) {
app-region: no-drag;
}
}
</style>

View File

@@ -36,7 +36,7 @@ export interface VbenButtonGroupProps
btnClass?: any;
gap?: number;
multiple?: boolean;
options?: { [key: string]: any; label: CustomRenderType; value: ValueType }[];
options?: { label: CustomRenderType; value: ValueType }[];
showIcon?: boolean;
size?: 'large' | 'middle' | 'small';
}

View File

@@ -119,7 +119,7 @@ async function onBtnClick(value: ValueType) {
<CircleCheckBig v-else-if="innerValue.includes(btn.value)" />
<Circle v-else />
</div>
<slot name="option" :label="btn.label" :value="btn.value" :data="btn">
<slot name="option" :label="btn.label" :value="btn.value">
<VbenRenderContent :content="btn.label" />
</slot>
</Button>
@@ -127,9 +127,6 @@ async function onBtnClick(value: ValueType) {
</template>
<style lang="scss" scoped>
.vben-check-button-group {
display: flex;
flex-wrap: wrap;
&:deep(.size-large) button {
.icon-wrapper {
margin-right: 0.3rem;
@@ -162,16 +159,5 @@ async function onBtnClick(value: ValueType) {
}
}
}
&.no-gap > :deep(button):nth-of-type(1) {
border-right-width: 0;
}
&.no-gap {
:deep(button + button) {
margin-right: -1px;
border-left-width: 1px;
}
}
}
</style>

View File

@@ -224,20 +224,15 @@ defineExpose({
:class="
cn('cursor-pointer', getNodeClass?.(item), {
'data-[selected]:bg-accent': !multiple,
'cursor-not-allowed': disabled,
})
"
v-bind="
Object.assign(item.bind, {
onfocus: disabled ? 'this.blur()' : undefined,
})
"
v-bind="item.bind"
@select="
(event) => {
if (event.detail.originalEvent.type === 'click') {
event.preventDefault();
}
!disabled && onSelect(item, event.detail.isSelected);
onSelect(item, event.detail.isSelected);
}
"
@toggle="
@@ -245,7 +240,7 @@ defineExpose({
if (event.detail.originalEvent.type === 'click') {
event.preventDefault();
}
!disabled && onToggle(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"
@@ -267,11 +262,10 @@ defineExpose({
<Checkbox
v-if="multiple"
:checked="isSelected"
:disabled="disabled"
:indeterminate="isIndeterminate"
@click="
() => {
!disabled && handleSelect();
handleSelect();
// onSelect(item, !isSelected);
}
"
@@ -282,7 +276,7 @@ defineExpose({
(_event) => {
// $event.stopPropagation();
// $event.preventDefault();
!disabled && handleSelect();
handleSelect();
// onSelect(item, !isSelected);
}
"

View File

@@ -40,14 +40,14 @@ const style = computed(() => {
const tabsView = computed(() => {
return props.tabs.map((tab) => {
const { fullPath, meta, name, path, key } = tab || {};
const { fullPath, meta, name, path } = tab || {};
const { affixTab, icon, newTabTitle, tabClosable, title } = meta || {};
return {
affixTab: !!affixTab,
closable: Reflect.has(meta, 'tabClosable') ? !!tabClosable : true,
fullPath,
icon: icon as string,
key,
key: fullPath || path,
meta,
name,
path,

View File

@@ -47,14 +47,14 @@ const typeWithClass = computed(() => {
const tabsView = computed(() => {
return props.tabs.map((tab) => {
const { fullPath, meta, name, path, key } = tab || {};
const { fullPath, meta, name, path } = tab || {};
const { affixTab, icon, newTabTitle, tabClosable, title } = meta || {};
return {
affixTab: !!affixTab,
closable: Reflect.has(meta, 'tabClosable') ? !!tabClosable : true,
fullPath,
icon: icon as string,
key,
key: fullPath || path,
meta,
name,
path,

View File

@@ -1,4 +1,3 @@
import type { ComputedRef } from 'vue';
import type { RouteLocationNormalized } from 'vue-router';
import { useRoute, useRouter } from 'vue-router';
@@ -42,8 +41,8 @@ export function useTabs() {
await tabbarStore.toggleTabPin(tab || route);
}
async function refreshTab(name?: string) {
await tabbarStore.refresh(name || router);
async function refreshTab() {
await tabbarStore.refresh(router);
}
async function openTabInNewWindow(tab?: RouteLocationNormalized) {
@@ -54,24 +53,7 @@ export function useTabs() {
await tabbarStore.closeTabByKey(key, router);
}
/**
* 设置当前标签页的标题
*
* @description 支持设置静态标题字符串或动态计算标题
* @description 动态标题会在每次渲染时重新计算,适用于多语言或状态相关的标题
*
* @param title - 标题内容
* - 静态标题: 直接传入字符串
* - 动态标题: 传入 ComputedRef
*
* @example
* // 静态标题
* setTabTitle('标签页')
*
* // 动态标题(多语言)
* setTabTitle(computed(() => t('page.title')))
*/
async function setTabTitle(title: ComputedRef<string> | string) {
async function setTabTitle(title: string) {
tabbarStore.setUpdateTime();
await tabbarStore.setTabTitle(route, title);
}

View File

@@ -39,8 +39,5 @@
"@vueuse/core": "catalog:",
"vue": "catalog:",
"vue-router": "catalog:"
},
"devDependencies": {
"@vben-core/typings": "workspace:*"
}
}

View File

@@ -38,7 +38,7 @@ const { authPanelCenter, authPanelLeft, authPanelRight, isDark } =
<template>
<div
:class="[isDark ? 'dark' : '']"
:class="[isDark]"
class="flex min-h-full flex-1 select-none overflow-x-hidden"
>
<template v-if="toolbar">

View File

@@ -1,2 +1 @@
export { default as AuthPageLayout } from './authentication.vue';
export * from './types';

View File

@@ -6,9 +6,6 @@ import { computed } from 'vue';
import { preferences } from '@vben/preferences';
import {
AppClose,
AppMaxmize,
AppMinmize,
AuthenticationColorToggle,
AuthenticationLayoutToggle,
LanguageToggle,
@@ -31,8 +28,6 @@ const showColor = computed(() => props.toolbarList.includes('color'));
const showLayout = computed(() => props.toolbarList.includes('layout'));
const showLanguage = computed(() => props.toolbarList.includes('language'));
const showTheme = computed(() => props.toolbarList.includes('theme'));
const isElectron = window?.ipcRenderer !== undefined;
</script>
<template>
@@ -50,10 +45,5 @@ const isElectron = window?.ipcRenderer !== undefined;
<!-- Always show Language and Theme toggles -->
<LanguageToggle v-if="showLanguage && preferences.widget.languageToggle" />
<ThemeToggle v-if="showTheme && preferences.widget.themeToggle" />
<template v-if="isElectron">
<AppMinmize />
<AppMaxmize />
<AppClose />
</template>
</div>
</template>

View File

@@ -9,7 +9,7 @@ import { computed } from 'vue';
import { RouterView } from 'vue-router';
import { preferences, usePreferences } from '@vben/preferences';
import { getTabKey, storeToRefs, useTabbarStore } from '@vben/stores';
import { storeToRefs, useTabbarStore } from '@vben/stores';
import { IFrameRouterView } from '../../iframe';
@@ -115,13 +115,13 @@ function transformComponent(
:is="transformComponent(Component, route)"
v-if="renderRouteView"
v-show="!route.meta.iframeSrc"
:key="getTabKey(route)"
:key="route.fullPath"
/>
</KeepAlive>
<component
:is="Component"
v-else-if="renderRouteView"
:key="getTabKey(route)"
:key="route.fullPath"
/>
</Transition>
<template v-else>
@@ -134,13 +134,13 @@ function transformComponent(
:is="transformComponent(Component, route)"
v-if="renderRouteView"
v-show="!route.meta.iframeSrc"
:key="getTabKey(route)"
:key="route.fullPath"
/>
</KeepAlive>
<component
:is="Component"
v-else-if="renderRouteView"
:key="getTabKey(route)"
:key="route.fullPath"
/>
</template>
</RouterView>

View File

@@ -9,9 +9,6 @@ import { useAccessStore } from '@vben/stores';
import { VbenFullScreen, VbenIconButton } from '@vben-core/shadcn-ui';
import {
AppClose,
AppMaxmize,
AppMinmize,
GlobalSearch,
LanguageToggle,
PreferencesButton,
@@ -44,13 +41,6 @@ const { refresh } = useRefresh();
const rightSlots = computed(() => {
const list = [{ index: REFERENCE_VALUE + 100, name: 'user-dropdown' }];
if (window.ipcRenderer) {
list.push(
{ index: REFERENCE_VALUE + 110, name: 'app-minimize' },
{ index: REFERENCE_VALUE + 120, name: 'app-maximize' },
{ index: REFERENCE_VALUE + 120, name: 'app-close' },
);
}
if (preferences.widget.globalSearch) {
list.push({
index: REFERENCE_VALUE,
@@ -176,15 +166,6 @@ function clearPreferencesAndLogout() {
<template v-else-if="slot.name === 'fullscreen'">
<VbenFullScreen class="mr-1" />
</template>
<template v-else-if="slot.name === 'app-minimize'">
<AppMinmize class="mr-1" />
</template>
<template v-else-if="slot.name === 'app-maximize'">
<AppMaxmize class="mr-1" />
</template>
<template v-else-if="slot.name === 'app-close'">
<AppClose class="mr-1" />
</template>
</slot>
</template>
</div>

View File

@@ -140,10 +140,7 @@ function useMixedMenu() {
watch(
() => route.path,
(path) => {
const currentPath = route?.meta?.activePath ?? route?.meta?.link ?? path;
if (willOpenedByWindow(currentPath)) {
return;
}
const currentPath = (route?.meta?.activePath as string) ?? path;
calcSideMenus(currentPath);
if (rootMenuPath.value)
defaultSubMap.set(rootMenuPath.value, currentPath);

View File

@@ -30,7 +30,7 @@ const {
} = useTabbar();
const menus = computed(() => {
const tab = tabbarStore.getTabByKey(currentActive.value);
const tab = tabbarStore.getTabByPath(currentActive.value);
const menus = createContextMenus(tab);
return menus.map((item) => {
return {

View File

@@ -22,7 +22,7 @@ import {
X,
} from '@vben/icons';
import { $t, useI18n } from '@vben/locales';
import { getTabKey, useAccessStore, useTabbarStore } from '@vben/stores';
import { useAccessStore, useTabbarStore } from '@vben/stores';
import { filterTree } from '@vben/utils';
export function useTabbar() {
@@ -44,11 +44,8 @@ export function useTabbar() {
toggleTabPin,
} = useTabs();
/**
* 当前路径对应的tab的key
*/
const currentActive = computed(() => {
return getTabKey(route);
return route.fullPath;
});
const { locale } = useI18n();
@@ -76,8 +73,7 @@ export function useTabbar() {
// 点击tab,跳转路由
const handleClick = (key: string) => {
const { fullPath, path } = tabbarStore.getTabByKey(key);
router.push(fullPath || path);
router.push(key);
};
// 关闭tab
@@ -104,7 +100,7 @@ export function useTabbar() {
);
watch(
() => route.fullPath,
() => route.path,
() => {
const meta = route.matched?.[route.matched.length - 1]?.meta;
tabbarStore.addTab({
@@ -162,7 +158,7 @@ export function useTabbar() {
},
{
disabled: disabledRefresh,
handler: () => refreshTab(),
handler: refreshTab,
icon: RotateCw,
key: 'reload',
text: $t('preferences.tabbar.contextMenu.reload'),

View File

@@ -1,16 +0,0 @@
<script lang="ts" setup>
import { X } from '@vben/icons';
import { VbenIconButton } from '@vben-core/shadcn-ui';
function handleAppClose() {
window.ipcRenderer?.invoke('app-close');
}
</script>
<template>
<div class="flex-center mr-2 h-full" @click.stop="handleAppClose()">
<VbenIconButton class="bell-button text-foreground relative">
<X class="size-4" />
</VbenIconButton>
</div>
</template>

View File

@@ -1 +0,0 @@
export { default as AppClose } from './app-close.vue';

View File

@@ -1,30 +0,0 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { SquareArrowDownLeft, SquareArrowUpRight } from '@vben/icons';
import { VbenIconButton } from '@vben-core/shadcn-ui';
const isMaximized = ref(false);
if (window.ipcRenderer) {
onMounted(async () => {
isMaximized.value = await window.ipcRenderer.invoke('is-maximized');
window.ipcRenderer.on('maximize-changed', (_, maximized) => {
isMaximized.value = maximized;
});
});
}
function handleAppMaximize() {
window.ipcRenderer?.invoke('app-maximize');
}
</script>
<template>
<div class="flex-center mr-2 h-full" @click.stop="handleAppMaximize()">
<VbenIconButton class="bell-button text-foreground relative">
<SquareArrowDownLeft v-if="isMaximized" class="size-4" />
<SquareArrowUpRight v-else class="size-4" />
</VbenIconButton>
</div>
</template>

View File

@@ -1 +0,0 @@
export { default as AppMaxmize } from './app-maximize.vue';

View File

@@ -1,16 +0,0 @@
<script lang="ts" setup>
import { Minus } from '@vben/icons';
import { VbenIconButton } from '@vben-core/shadcn-ui';
function handleAppMinimize() {
window.ipcRenderer?.invoke('app-minimize');
}
</script>
<template>
<div class="flex-center mr-2 h-full" @click.stop="handleAppMinimize()">
<VbenIconButton class="bell-button text-foreground relative">
<Minus class="size-4" />
</VbenIconButton>
</div>
</template>

View File

@@ -1 +0,0 @@
export { default as AppMinmize } from './app-minimize.vue';

View File

@@ -1,6 +1,3 @@
export * from './app-close';
export * from './app-maximize';
export * from './app-minimize';
export { default as Breadcrumb } from './breadcrumb.vue';
export * from './check-updates';
export { default as AuthenticationColorToggle } from './color-toggle.vue';

View File

@@ -1,9 +1,6 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/web.json",
"compilerOptions": {
"types": ["@vben-core/typings/electron"]
},
"include": ["src"],
"exclude": ["node_modules"]
}

View File

@@ -22,13 +22,12 @@ describe('useAccessStore', () => {
const tab: any = {
fullPath: '/home',
meta: {},
key: '/home',
name: 'Home',
path: '/home',
};
const addNewTab = store.addTab(tab);
store.addTab(tab);
expect(store.tabs.length).toBe(1);
expect(store.tabs[0]).toEqual(addNewTab);
expect(store.tabs[0]).toEqual(tab);
});
it('adds a new tab if it does not exist', () => {
@@ -39,22 +38,20 @@ describe('useAccessStore', () => {
name: 'New',
path: '/new',
};
const addNewTab = store.addTab(newTab);
expect(store.tabs).toContainEqual(addNewTab);
store.addTab(newTab);
expect(store.tabs).toContainEqual(newTab);
});
it('updates an existing tab instead of adding a new one', () => {
const store = useTabbarStore();
const initialTab: any = {
fullPath: '/existing',
meta: {
fullPathKey: false,
},
meta: {},
name: 'Existing',
path: '/existing',
query: {},
};
store.addTab(initialTab);
store.tabs.push(initialTab);
const updatedTab = { ...initialTab, query: { id: '1' } };
store.addTab(updatedTab);
expect(store.tabs.length).toBe(1);
@@ -63,12 +60,9 @@ describe('useAccessStore', () => {
it('closes all tabs', async () => {
const store = useTabbarStore();
store.addTab({
fullPath: '/home',
meta: {},
name: 'Home',
path: '/home',
} as any);
store.tabs = [
{ fullPath: '/home', meta: {}, name: 'Home', path: '/home' },
] as any;
router.replace = vi.fn();
await store.closeAllTabs(router);
@@ -163,7 +157,7 @@ describe('useAccessStore', () => {
path: '/contact',
} as any);
await store._bulkCloseByKeys(['/home', '/contact']);
await store._bulkCloseByPaths(['/home', '/contact']);
expect(store.tabs).toHaveLength(1);
expect(store.tabs[0]?.name).toBe('About');
@@ -189,8 +183,9 @@ describe('useAccessStore', () => {
name: 'Contact',
path: '/contact',
};
const addTargetTab = store.addTab(targetTab);
await store.closeLeftTabs(addTargetTab);
store.addTab(targetTab);
await store.closeLeftTabs(targetTab);
expect(store.tabs).toHaveLength(1);
expect(store.tabs[0]?.name).toBe('Contact');
@@ -210,7 +205,7 @@ describe('useAccessStore', () => {
name: 'About',
path: '/about',
};
const addTargetTab = store.addTab(targetTab);
store.addTab(targetTab);
store.addTab({
fullPath: '/contact',
meta: {},
@@ -218,7 +213,7 @@ describe('useAccessStore', () => {
path: '/contact',
} as any);
await store.closeOtherTabs(addTargetTab);
await store.closeOtherTabs(targetTab);
expect(store.tabs).toHaveLength(1);
expect(store.tabs[0]?.name).toBe('About');
@@ -232,7 +227,7 @@ describe('useAccessStore', () => {
name: 'Home',
path: '/home',
};
const addTargetTab = store.addTab(targetTab);
store.addTab(targetTab);
store.addTab({
fullPath: '/about',
meta: {},
@@ -246,7 +241,7 @@ describe('useAccessStore', () => {
path: '/contact',
} as any);
await store.closeRightTabs(addTargetTab);
await store.closeRightTabs(targetTab);
expect(store.tabs).toHaveLength(1);
expect(store.tabs[0]?.name).toBe('Home');

View File

@@ -1,9 +1,4 @@
import type { ComputedRef } from 'vue';
import type {
RouteLocationNormalized,
Router,
RouteRecordNormalized,
} from 'vue-router';
import type { Router, RouteRecordNormalized } from 'vue-router';
import type { TabDefinition } from '@vben-core/typings';
@@ -57,23 +52,23 @@ export const useTabbarStore = defineStore('core-tabbar', {
/**
* Close tabs in bulk
*/
async _bulkCloseByKeys(keys: string[]) {
const keySet = new Set(keys);
this.tabs = this.tabs.filter(
(item) => !keySet.has(getTabKeyFromTab(item)),
);
async _bulkCloseByPaths(paths: string[]) {
this.tabs = this.tabs.filter((item) => {
return !paths.includes(getTabPath(item));
});
await this.updateCacheTabs();
this.updateCacheTabs();
},
/**
* @zh_CN 关闭标签页
* @param tab
*/
_close(tab: TabDefinition) {
const { fullPath } = tab;
if (isAffixTab(tab)) {
return;
}
const index = this.tabs.findIndex((item) => equalTab(item, tab));
const index = this.tabs.findIndex((item) => item.fullPath === fullPath);
index !== -1 && this.tabs.splice(index, 1);
},
/**
@@ -106,17 +101,14 @@ export const useTabbarStore = defineStore('core-tabbar', {
* @zh_CN 添加标签页
* @param routeTab
*/
addTab(routeTab: TabDefinition): TabDefinition {
let tab = cloneTab(routeTab);
if (!tab.key) {
tab.key = getTabKey(routeTab);
}
addTab(routeTab: TabDefinition) {
const tab = cloneTab(routeTab);
if (!isTabShown(tab)) {
return tab;
return;
}
const tabIndex = this.tabs.findIndex((item) => {
return equalTab(item, tab);
const tabIndex = this.tabs.findIndex((tab) => {
return getTabPath(tab) === getTabPath(routeTab);
});
if (tabIndex === -1) {
@@ -162,11 +154,10 @@ export const useTabbarStore = defineStore('core-tabbar', {
mergedTab.meta.newTabTitle = curMeta.newTabTitle;
}
}
tab = mergedTab;
this.tabs.splice(tabIndex, 1, mergedTab);
}
this.updateCacheTabs();
return tab;
},
/**
* @zh_CN 关闭所有标签页
@@ -182,63 +173,65 @@ export const useTabbarStore = defineStore('core-tabbar', {
* @param tab
*/
async closeLeftTabs(tab: TabDefinition) {
const index = this.tabs.findIndex((item) => equalTab(item, tab));
const index = this.tabs.findIndex(
(item) => getTabPath(item) === getTabPath(tab),
);
if (index < 1) {
return;
}
const leftTabs = this.tabs.slice(0, index);
const keys: string[] = [];
const paths: string[] = [];
for (const item of leftTabs) {
if (!isAffixTab(item)) {
keys.push(item.key as string);
paths.push(getTabPath(item));
}
}
await this._bulkCloseByKeys(keys);
await this._bulkCloseByPaths(paths);
},
/**
* @zh_CN 关闭其他标签页
* @param tab
*/
async closeOtherTabs(tab: TabDefinition) {
const closeKeys = this.tabs.map((item) => getTabKeyFromTab(item));
const closePaths = this.tabs.map((item) => getTabPath(item));
const keys: string[] = [];
const paths: string[] = [];
for (const key of closeKeys) {
if (key !== tab.key) {
const closeTab = this.tabs.find(
(item) => getTabKeyFromTab(item) === key,
);
for (const path of closePaths) {
if (path !== tab.fullPath) {
const closeTab = this.tabs.find((item) => getTabPath(item) === path);
if (!closeTab) {
continue;
}
if (!isAffixTab(closeTab)) {
keys.push(closeTab.key as string);
paths.push(getTabPath(closeTab));
}
}
}
await this._bulkCloseByKeys(keys);
await this._bulkCloseByPaths(paths);
},
/**
* @zh_CN 关闭右侧标签页
* @param tab
*/
async closeRightTabs(tab: TabDefinition) {
const index = this.tabs.findIndex((item) => equalTab(item, tab));
const index = this.tabs.findIndex(
(item) => getTabPath(item) === getTabPath(tab),
);
if (index !== -1 && index < this.tabs.length - 1) {
const rightTabs = this.tabs.slice(index + 1);
const keys: string[] = [];
const paths: string[] = [];
for (const item of rightTabs) {
if (!isAffixTab(item)) {
keys.push(item.key as string);
paths.push(getTabPath(item));
}
}
await this._bulkCloseByKeys(keys);
await this._bulkCloseByPaths(paths);
}
},
@@ -249,14 +242,15 @@ export const useTabbarStore = defineStore('core-tabbar', {
*/
async closeTab(tab: TabDefinition, router: Router) {
const { currentRoute } = router;
// 关闭不是激活选项卡
if (getTabKey(currentRoute.value) !== getTabKeyFromTab(tab)) {
if (getTabPath(currentRoute.value) !== getTabPath(tab)) {
this._close(tab);
this.updateCacheTabs();
return;
}
const index = this.getTabs.findIndex(
(item) => getTabKeyFromTab(item) === getTabKey(currentRoute.value),
(item) => getTabPath(item) === getTabPath(currentRoute.value),
);
const before = this.getTabs[index - 1];
@@ -283,7 +277,7 @@ export const useTabbarStore = defineStore('core-tabbar', {
async closeTabByKey(key: string, router: Router) {
const originKey = decodeURIComponent(key);
const index = this.tabs.findIndex(
(item) => getTabKeyFromTab(item) === originKey,
(item) => getTabPath(item) === originKey,
);
if (index === -1) {
return;
@@ -296,12 +290,12 @@ export const useTabbarStore = defineStore('core-tabbar', {
},
/**
* 根据tab的key获取tab
* @param key
* 根据路径获取标签页
* @param path
*/
getTabByKey(key: string) {
getTabByPath(path: string) {
return this.getTabs.find(
(item) => getTabKeyFromTab(item) === key,
(item) => getTabPath(item) === path,
) as TabDefinition;
},
/**
@@ -317,19 +311,22 @@ export const useTabbarStore = defineStore('core-tabbar', {
* @param tab
*/
async pinTab(tab: TabDefinition) {
const index = this.tabs.findIndex((item) => equalTab(item, tab));
if (index === -1) {
return;
const index = this.tabs.findIndex(
(item) => getTabPath(item) === getTabPath(tab),
);
if (index !== -1) {
const oldTab = this.tabs[index];
tab.meta.affixTab = true;
tab.meta.title = oldTab?.meta?.title as string;
// this.addTab(tab);
this.tabs.splice(index, 1, tab);
}
const oldTab = this.tabs[index];
tab.meta.affixTab = true;
tab.meta.title = oldTab?.meta?.title as string;
// this.addTab(tab);
this.tabs.splice(index, 1, tab);
// 过滤固定tabs后面更改affixTabOrder的值的话可能会有问题目前行464排序affixTabs没有设置值
const affixTabs = this.tabs.filter((tab) => isAffixTab(tab));
// 获得固定tabs的index
const newIndex = affixTabs.findIndex((item) => equalTab(item, tab));
const newIndex = affixTabs.findIndex(
(item) => getTabPath(item) === getTabPath(tab),
);
// 交换位置重新排序
await this.sortTabs(index, newIndex);
},
@@ -337,13 +334,7 @@ export const useTabbarStore = defineStore('core-tabbar', {
/**
* 刷新标签页
*/
async refresh(router: Router | string) {
// 如果是Router路由那么就根据当前路由刷新
// 如果是string字符串为路由名称则定向刷新指定标签页不能是当前路由名称否则不会刷新
if (typeof router === 'string') {
return await this.refreshByName(router);
}
async refresh(router: Router) {
const { currentRoute } = router;
const { name } = currentRoute.value;
@@ -358,15 +349,6 @@ export const useTabbarStore = defineStore('core-tabbar', {
stopProgress();
},
/**
* 根据路由名称刷新指定标签页
*/
async refreshByName(name: string) {
this.excludeCachedTabs.add(name);
await new Promise((resolve) => setTimeout(resolve, 200));
this.excludeCachedTabs.delete(name);
},
/**
* @zh_CN 重置标签页标题
*/
@@ -374,7 +356,9 @@ export const useTabbarStore = defineStore('core-tabbar', {
if (tab?.meta?.newTabTitle) {
return;
}
const findTab = this.tabs.find((item) => equalTab(item, tab));
const findTab = this.tabs.find(
(item) => getTabPath(item) === getTabPath(tab),
);
if (findTab) {
findTab.meta.newTabTitle = undefined;
await this.updateCacheTabs();
@@ -402,24 +386,13 @@ export const useTabbarStore = defineStore('core-tabbar', {
/**
* @zh_CN 设置标签页标题
*
* @zh_CN 支持设置静态标题字符串或计算属性作为动态标题
* @zh_CN 当标题为计算属性时,标题会随计算属性值变化而自动更新
* @zh_CN 适用于需要根据状态或多语言动态更新标题的场景
*
* @param {TabDefinition} tab - 标签页对象
* @param {ComputedRef<string> | string} title - 标题内容,支持静态字符串或计算属性
*
* @example
* // 设置静态标题
* setTabTitle(tab, '新标签页');
*
* @example
* // 设置动态标题
* setTabTitle(tab, computed(() => t('common.dashboard')));
* @param tab
* @param title
*/
async setTabTitle(tab: TabDefinition, title: ComputedRef<string> | string) {
const findTab = this.tabs.find((item) => equalTab(item, tab));
async setTabTitle(tab: TabDefinition, title: string) {
const findTab = this.tabs.find(
(item) => getTabPath(item) === getTabPath(tab),
);
if (findTab) {
findTab.meta.newTabTitle = title;
@@ -460,15 +433,17 @@ export const useTabbarStore = defineStore('core-tabbar', {
* @param tab
*/
async unpinTab(tab: TabDefinition) {
const index = this.tabs.findIndex((item) => equalTab(item, tab));
if (index === -1) {
return;
const index = this.tabs.findIndex(
(item) => getTabPath(item) === getTabPath(tab),
);
if (index !== -1) {
const oldTab = this.tabs[index];
tab.meta.affixTab = false;
tab.meta.title = oldTab?.meta?.title as string;
// this.addTab(tab);
this.tabs.splice(index, 1, tab);
}
const oldTab = this.tabs[index];
tab.meta.affixTab = false;
tab.meta.title = oldTab?.meta?.title as string;
// this.addTab(tab);
this.tabs.splice(index, 1, tab);
// 过滤固定tabs后面更改affixTabOrder的值的话可能会有问题目前行464排序affixTabs没有设置值
const affixTabs = this.tabs.filter((tab) => isAffixTab(tab));
// 获得固定tabs的index,使用固定tabs的下一个位置也就是活动tabs的第一个位置
@@ -601,49 +576,11 @@ function isTabShown(tab: TabDefinition) {
}
/**
* 从route获取tab页的key
* @zh_CN 获取标签页路径
* @param tab
*/
function getTabKey(tab: RouteLocationNormalized | RouteRecordNormalized) {
const {
fullPath,
path,
meta: { fullPathKey } = {},
query = {},
} = tab as RouteLocationNormalized;
// pageKey可能是数组查询参数重复时可能出现
const pageKey = Array.isArray(query.pageKey)
? query.pageKey[0]
: query.pageKey;
let rawKey;
if (pageKey) {
rawKey = pageKey;
} else {
rawKey = fullPathKey === false ? path : (fullPath ?? path);
}
try {
return decodeURIComponent(rawKey);
} catch {
return rawKey;
}
}
/**
* 从tab获取tab页的key
* 如果tab没有key,那么就从route获取key
* @param tab
*/
function getTabKeyFromTab(tab: TabDefinition): string {
return tab.key ?? getTabKey(tab);
}
/**
* 比较两个tab是否相等
* @param a
* @param b
*/
function equalTab(a: TabDefinition, b: TabDefinition) {
return getTabKeyFromTab(a) === getTabKeyFromTab(b);
function getTabPath(tab: RouteRecordNormalized | TabDefinition) {
return decodeURIComponent((tab as TabDefinition).fullPath || tab.path);
}
function routeToTab(route: RouteRecordNormalized) {
@@ -651,8 +588,5 @@ function routeToTab(route: RouteRecordNormalized) {
meta: route.meta,
name: route.name,
path: route.path,
key: getTabKey(route),
} as TabDefinition;
}
export { getTabKey };

View File

@@ -4,11 +4,5 @@ VITE_APP_TITLE=Vben Admin
# 应用命名空间用于缓存、store等功能的前缀确保隔离
VITE_APP_NAMESPACE=vben-web-play
# vue-router 的模式
VITE_ROUTER_HISTORY=hash
# 对store进行加密的密钥在将store持久化到localStorage时会使用该密钥进行加密
VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key
# vue-router 的模式
VITE_ROUTER_HISTORY=hash

View File

@@ -1,4 +1,4 @@
VITE_BASE=./
VITE_BASE=/
# 接口地址
VITE_GLOB_API_URL=https://mock-napi.vben.pro/api

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -1,192 +0,0 @@
import os from 'node:os';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
import {
app,
BrowserWindow,
globalShortcut,
ipcMain,
Menu,
shell,
} from 'electron';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
process.env.APP_ROOT = path.join(__dirname, '../..');
export const MAIN_DIST = path.join(process.env.APP_ROOT, 'dist-electron');
export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist');
export const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL;
process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL
? path.join(process.env.APP_ROOT, 'public')
: RENDERER_DIST;
// Disable GPU Acceleration for Windows 7
if (os.release().startsWith('6.1')) app.disableHardwareAcceleration();
// Set application name for Windows 10+ notifications
if (process.platform === 'win32') app.setAppUserModelId(app.getName());
if (!app.requestSingleInstanceLock()) {
app.quit();
}
let win: BrowserWindow | null = null;
const preload = path.join(__dirname, '../preload/preload.mjs');
const indexHtml = path.join(RENDERER_DIST, 'index.html');
async function createWindow() {
win = new BrowserWindow({
autoHideMenuBar: true,
frame: false,
height: 900,
icon: path.join(process.env.VITE_PUBLIC as string, 'favicon.ico'),
movable: true,
show: false,
title: 'Main window',
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
preload,
webSecurity: true,
},
width: 1440,
});
// 监听窗口准备好显示的事件
win.once('ready-to-show', () => {
win?.maximize(); // 最大化窗口
win?.show(); // 显示窗口
});
win.on('maximize', () => {
win?.webContents.send('maximize-changed', true);
});
win.on('unmaximize', () => {
win?.webContents.send('maximize-changed', false);
});
if (VITE_DEV_SERVER_URL) {
win.loadURL(VITE_DEV_SERVER_URL);
} else {
win.loadFile(indexHtml);
}
// Test actively push message to the Electron-Renderer
win.webContents.on('did-finish-load', () => {
win?.webContents.send('main-process-message', new Date().toLocaleString());
});
// Make all links open with the browser, not with the application
win.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith('https:')) shell.openExternal(url);
return { action: 'deny' };
});
// win.webContents.on('will-navigate', (event, url) => { }) #344
}
Menu.setApplicationMenu(null);
app
.whenReady()
.then(createWindow)
.then(() => {
// 禁用了菜单之后,默认的快捷键也会被禁用,这里重新注册部分常用快捷键
if (VITE_DEV_SERVER_URL) {
// 开发模式下监听快捷键来打开开发者工具
globalShortcut.register('CmdOrCtrl+Shift+I', () => {
BrowserWindow.getFocusedWindow()?.webContents.toggleDevTools();
});
}
// 监听快捷键来刷新页面
globalShortcut.registerAll(['CmdOrCtrl+R', 'CmdOrCtrl+F5'], () => {
BrowserWindow.getFocusedWindow()?.webContents.reload();
});
// 监听快捷键来强制刷新页面
globalShortcut.registerAll(
['CmdOrCtrl+Shift+R', 'CmdOrCtrl+Shift+F5'],
() => {
BrowserWindow.getFocusedWindow()?.webContents.reloadIgnoringCache();
},
);
});
app.on('window-all-closed', () => {
win = null;
if (process.platform !== 'darwin') app.quit();
});
app.on('will-quit', () => {
globalShortcut.unregisterAll();
});
app.on('second-instance', () => {
if (win) {
// Focus on the main window if the user tried to open another
if (win.isMinimized()) win.restore();
win.focus();
}
});
app.on('activate', () => {
const allWindows = BrowserWindow.getAllWindows();
if (allWindows.length > 0) {
allWindows[0].focus();
} else {
createWindow();
}
});
// New window example arg: new windows url
ipcMain.handle('open-win', (_, arg) => {
const childWindow = new BrowserWindow({
frame: false,
webPreferences: {
contextIsolation: true,
nodeIntegration: true,
preload,
webviewTag: true,
},
});
if (VITE_DEV_SERVER_URL) {
childWindow.loadURL(`${VITE_DEV_SERVER_URL}#${arg}`);
} else {
childWindow.loadFile(indexHtml, { hash: arg });
}
});
ipcMain.handle('app-minimize', (event) => {
const browserWindow = BrowserWindow.fromWebContents(event.sender);
if (browserWindow) {
browserWindow.minimize();
}
});
ipcMain.handle('app-maximize', (event) => {
const browserWindow = BrowserWindow.fromWebContents(event.sender);
if (browserWindow) {
if (browserWindow.isMaximized()) {
browserWindow.restore();
} else {
browserWindow.maximize();
}
}
});
ipcMain.handle('app-close', (event) => {
const browserWindow = BrowserWindow.fromWebContents(event.sender);
if (browserWindow) {
browserWindow.close();
}
});
ipcMain.handle('is-maximized', (event) => {
const browserWindow = BrowserWindow.fromWebContents(event.sender);
if (browserWindow) {
return browserWindow.isMaximized();
}
return false;
});

View File

@@ -1,113 +0,0 @@
import { contextBridge, ipcRenderer } from 'electron';
// --------- Expose some API to the Renderer process ---------
contextBridge.exposeInMainWorld('ipcRenderer', {
invoke(...args: Parameters<typeof ipcRenderer.invoke>) {
const [channel, ...omit] = args;
return ipcRenderer.invoke(channel, ...omit);
},
off(...args: Parameters<typeof ipcRenderer.off>) {
const [channel, ...omit] = args;
return ipcRenderer.off(channel, ...omit);
},
on(...args: Parameters<typeof ipcRenderer.on>) {
const [channel, listener] = args;
return ipcRenderer.on(channel, (event, ...args) =>
listener(event, ...args),
);
},
send(...args: Parameters<typeof ipcRenderer.send>) {
const [channel, ...omit] = args;
return ipcRenderer.send(channel, ...omit);
},
// You can expose other APTs you need here.
// ...
});
// --------- Preload scripts loading ---------
// function domReady(
// condition: DocumentReadyState[] = ['complete', 'interactive'],
// ) {
// return new Promise((resolve) => {
// if (condition.includes(document.readyState)) {
// resolve(true);
// } else {
// document.addEventListener('readystatechange', () => {
// if (condition.includes(document.readyState)) {
// resolve(true);
// }
// });
// }
// });
// }
// const safeDOM = {
// append(parent: HTMLElement, child: HTMLElement) {
// if (![...parent.children].includes(child)) {
// return parent.append(child);
// }
// },
// remove(parent: HTMLElement, child: HTMLElement) {
// if ([...parent.children].includes(child)) {
// return child.remove();
// }
// },
// };
// function useLoading() {
// const className = `loaders-css__square-spin`;
// const styleContent = `
// @keyframes square-spin {
// 25% { transform: perspective(100px) rotateX(180deg) rotateY(0); }
// 50% { transform: perspective(100px) rotateX(180deg) rotateY(180deg); }
// 75% { transform: perspective(100px) rotateX(0) rotateY(180deg); }
// 100% { transform: perspective(100px) rotateX(0) rotateY(0); }
// }
// .${className} > div {
// animation-fill-mode: both;
// width: 50px;
// height: 50px;
// background: #fff;
// animation: square-spin 3s 0s cubic-bezier(0.09, 0.57, 0.49, 0.9) infinite;
// }
// .app-loading-wrap {
// position: fixed;
// top: 0;
// left: 0;
// width: 100vw;
// height: 100vh;
// display: flex;
// align-items: center;
// justify-content: center;
// background: #282c34;
// z-index: 9;
// }
// `;
// const oStyle = document.createElement('style');
// const oDiv = document.createElement('div');
// oStyle.id = 'app-loading-style';
// oStyle.innerHTML = styleContent;
// oDiv.className = 'app-loading-wrap';
// oDiv.innerHTML = `<div class="${className}"><div></div></div>`;
// return {
// appendLoading() {
// safeDOM.append(document.head, oStyle);
// safeDOM.append(document.body, oDiv);
// },
// removeLoading() {
// safeDOM.remove(document.head, oStyle);
// safeDOM.remove(document.body, oDiv);
// },
// };
// }
// const { appendLoading, removeLoading } = useLoading();
// domReady().then(appendLoading);
// window.onmessage = (ev) => {
// ev.data.payload === 'removeLoading' && removeLoading();
// };
// setTimeout(removeLoading, 4999);

View File

@@ -16,61 +16,18 @@
},
"type": "module",
"scripts": {
"build": "pnpm vite build --mode production && electron-builder",
"build": "pnpm vite build --mode production",
"build:analyze": "pnpm vite build --mode analyze",
"dev": "cross-env ELECTRON_DISABLE_SECURITY_WARNINGS=true pnpm vite --mode development",
"dev": "pnpm vite --mode development",
"preview": "vite preview",
"typecheck": "vue-tsc --noEmit --skipLibCheck",
"test:e2e": "playwright test",
"test:e2e-ui": "playwright test --ui",
"test:e2e-codegen": "playwright codegen"
},
"main": "dist-electron/main/main.js",
"debug": {
"env": {
"VITE_DEV_SERVER_URL": "http://127.0.0.1:5555/"
}
},
"imports": {
"#/*": "./src/*"
},
"build": {
"productName": "VbenAdminPlayground",
"appId": "pro.vben.playground",
"copyright": "vben.pro © 2025",
"compression": "maximum",
"artifactName": "${productName}-v${version}-${platform}-${arch}.${ext}",
"asar": true,
"directories": {
"output": "dist-electron/release"
},
"files": [
"dist/**/*",
"dist-electron/**/*",
"package.json",
"!node_modules/**",
"!dist-electron/release/**"
],
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"perMachine": true,
"deleteAppDataOnUninstall": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"shortcutName": "VbenAdmin"
},
"win": {
"icon": "./electron/logo/logo_256.ico",
"target": "nsis"
},
"mac": {
"icon": "./electron/logo/logo_256.ico"
},
"linux": {
"icon": "./electron/logo/logo_256.ico"
}
},
"dependencies": {
"@tanstack/vue-query": "catalog:",
"@vben-core/menu-ui": "workspace:*",
@@ -94,10 +51,5 @@
"pinia": "catalog:",
"vue": "catalog:",
"vue-router": "catalog:"
},
"devDependencies": {
"cross-env": "catalog:",
"electron": "catalog:",
"electron-builder": "catalog:"
}
}

View File

@@ -208,34 +208,22 @@ setupVbenVxeTable({
}
function renderConfirm(opt: Recordable<any>) {
let viewportWrapper: HTMLElement | null = null;
return h(
Popconfirm,
{
/**
* 当popconfirm用在固定列中时将固定列作为弹窗的容器时可能会因为固定列较窄而无法容纳弹窗
* 将表格主体区域作为弹窗容器时又会因为固定列的层级较高而遮挡弹窗
* 将body或者表格视口区域作为弹窗容器时又会导致弹窗无法跟随表格滚动。
* 鉴于以上各种情况,一种折中的解决方案是弹出层展示时,禁止操作表格的滚动条。
* 这样既解决了弹窗的遮挡问题,又不至于让弹窗随着表格的滚动而跑出视口区域。
*/
getPopupContainer(el) {
viewportWrapper = el.closest('.vxe-table--viewport-wrapper');
return 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 || '']),
...props,
...opt,
icon: undefined,
onOpenChange: (open: boolean) => {
// 当弹窗打开时,禁止表格的滚动
if (open) {
viewportWrapper?.style.setProperty('pointer-events', 'none');
} else {
viewportWrapper?.style.removeProperty('pointer-events');
}
},
onConfirm: () => {
attrs?.onClick?.({
code: opt.code,

View File

@@ -8,7 +8,6 @@ import { defineOverridesPreferences } from '@vben/preferences';
export const overridesPreferences = defineOverridesPreferences({
// overrides
app: {
enableCheckUpdates: false,
name: import.meta.env.VITE_APP_TITLE,
},
});

View File

@@ -18,7 +18,7 @@ function setupCommonGuard(router: Router) {
// 记录已经加载的页面
const loadedPaths = new Set<string>();
router.beforeEach((to) => {
router.beforeEach(async (to) => {
to.meta.loaded = loadedPaths.has(to.path);
// 页面加载进度条

View File

@@ -19,7 +19,7 @@ const checkValue = ref(['a', 'b']);
const options = [
{ label: '选项1', value: 'a' },
{ label: '选项2', value: 'b', num: 999 },
{ label: '选项2', value: 'b' },
{ label: '选项3', value: 'c' },
{ label: '选项4', value: 'd' },
{ label: '选项5', value: 'e' },
@@ -168,11 +168,10 @@ function onBtnClick(value: any) {
:options="options"
v-bind="compProps"
>
<template #option="{ label, value, data }">
<template #option="{ label, value }">
<div class="flex items-center">
<span>{{ label }}</span>
<span class="ml-2 text-gray-400">{{ value }}</span>
<span v-if="data.num" class="white ml-2">{{ data.num }}</span>
</div>
</template>
</VbenCheckButtonGroup>

View File

@@ -2,9 +2,7 @@ import { defineConfig } from '@vben/vite-config';
export default defineConfig(async () => {
return {
application: {
electron: true,
},
application: {},
vite: {
server: {
proxy: {

1485
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -87,8 +87,6 @@ catalog:
depcheck: ^1.4.7
dotenv: ^16.5.0
echarts: ^5.6.0
electron: ^35.0.3
electron-builder: ^25.1.8
element-plus: ^2.9.9
eslint: ^9.26.0
eslint-config-turbo: ^2.5.2
@@ -171,8 +169,6 @@ catalog:
vee-validate: ^4.15.0
vite: ^6.3.4
vite-plugin-compression: ^0.5.1
vite-plugin-electron: ^0.29.0
vite-plugin-electron-renderer: ^0.14.6
vite-plugin-dts: ^4.5.3
vite-plugin-html: ^3.2.2
vite-plugin-lazy-import: ^1.0.7