mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-08-26 00:26:20 +08:00
Compare commits
64 Commits
electron-v
...
v5.5.6
Author | SHA1 | Date | |
---|---|---|---|
![]() |
cc6c9bf7a0 | ||
![]() |
6b1aab9c67 | ||
![]() |
8f4d3d418d | ||
![]() |
3b3f8e4e44 | ||
![]() |
f94ca10adf | ||
![]() |
4471bc7a5d | ||
![]() |
5689ac60ff | ||
![]() |
045bc4e5ee | ||
![]() |
17a18fc9ba | ||
![]() |
41152d1722 | ||
![]() |
f1af9f8f6e | ||
![]() |
0517a7014f | ||
![]() |
3e6d608a2f | ||
![]() |
5de954baa4 | ||
![]() |
add1e61b6f | ||
![]() |
20c15f352f | ||
![]() |
8aa7dabeff | ||
![]() |
78c7c1589a | ||
![]() |
dd833ca56b | ||
![]() |
681c1dc267 | ||
![]() |
4545422ee0 | ||
![]() |
ca94ca906f | ||
![]() |
76de450c71 | ||
![]() |
dd2b1ed580 | ||
![]() |
baec89f896 | ||
![]() |
7c7051a11e | ||
![]() |
aa27a2f7a1 | ||
![]() |
9ee6d06d50 | ||
![]() |
0cc1cb5a7b | ||
![]() |
0a9fc4e02d | ||
![]() |
be840460d8 | ||
![]() |
cb45987fe2 | ||
![]() |
5ffd7db8e0 | ||
![]() |
14377705e7 | ||
![]() |
b985ff0584 | ||
![]() |
b148b8ec92 | ||
![]() |
79de6bcbf7 | ||
![]() |
14bd6dd25d | ||
![]() |
7f269e0d69 | ||
![]() |
4baec83db5 | ||
![]() |
f7a4d13a4c | ||
![]() |
0936861da1 | ||
![]() |
3318d76bab | ||
![]() |
8f3881eabf | ||
![]() |
5252480b09 | ||
![]() |
d18f56177c | ||
![]() |
333998b518 | ||
![]() |
3fb4fba1cb | ||
![]() |
c7e6210c8d | ||
![]() |
d864085c13 | ||
![]() |
fcdc1a1602 | ||
![]() |
bf7496f0d5 | ||
![]() |
9700150653 | ||
![]() |
f0e9e55af2 | ||
![]() |
ff88274554 | ||
![]() |
afce9dc5c0 | ||
![]() |
b5700bd0b1 | ||
![]() |
a8c4786311 | ||
![]() |
2971ccc0b7 | ||
![]() |
4a2c7b313f | ||
![]() |
36bf6fc149 | ||
![]() |
f46ec30995 | ||
![]() |
9bd5a190c2 | ||
![]() |
86da3cedc2 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,7 +5,6 @@ dist-ssr
|
||||
dist.zip
|
||||
dist.tar
|
||||
dist.war
|
||||
dist-electron
|
||||
.nitro
|
||||
.output
|
||||
*-dist.zip
|
||||
|
3
.npmrc
3
.npmrc
@@ -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
|
||||
|
@@ -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);
|
||||
|
||||
// 页面加载进度条
|
||||
|
@@ -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);
|
||||
|
||||
// 页面加载进度条
|
||||
|
@@ -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);
|
||||
|
||||
// 页面加载进度条
|
||||
|
@@ -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.
|
||||
|
||||
:::
|
||||
|
@@ -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
|
||||
|
@@ -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 {
|
||||
|
@@ -12,7 +12,7 @@
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"stub": "pnpm unbuild --stub"
|
||||
"stub": "pnpm unbuild"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
@@ -1,9 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@vben/tsconfig/node.json",
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "bundler"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
@@ -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:"
|
||||
}
|
||||
|
@@ -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: {},
|
||||
});
|
||||
};
|
@@ -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)],
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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,
|
||||
|
@@ -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:"
|
||||
}
|
||||
}
|
||||
|
@@ -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 };
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
27
packages/@core/base/typings/electron.d.ts
vendored
27
packages/@core/base/typings/electron.d.ts
vendored
@@ -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 {};
|
@@ -27,9 +27,6 @@
|
||||
},
|
||||
"./vue-router": {
|
||||
"types": "./vue-router.d.ts"
|
||||
},
|
||||
"./electron": {
|
||||
"types": "./electron.d.ts"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
|
@@ -1,8 +1,3 @@
|
||||
import type { RouteLocationNormalized } from 'vue-router';
|
||||
|
||||
export interface TabDefinition extends RouteLocationNormalized {
|
||||
/**
|
||||
* 标签页的key
|
||||
*/
|
||||
key?: string;
|
||||
}
|
||||
export type TabDefinition = RouteLocationNormalized;
|
||||
|
@@ -43,10 +43,6 @@ interface RouteMeta {
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| string;
|
||||
/**
|
||||
* 路由的完整路径作为key(默认true)
|
||||
*/
|
||||
fullPathKey?: boolean;
|
||||
/**
|
||||
* 当前路由的子级在菜单中不展现
|
||||
* @default false
|
||||
|
@@ -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>
|
||||
|
@@ -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';
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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);
|
||||
}
|
||||
"
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -39,8 +39,5 @@
|
||||
"@vueuse/core": "catalog:",
|
||||
"vue": "catalog:",
|
||||
"vue-router": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vben-core/typings": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
@@ -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">
|
||||
|
@@ -1,2 +1 @@
|
||||
export { default as AuthPageLayout } from './authentication.vue';
|
||||
export * from './types';
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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);
|
||||
|
@@ -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 {
|
||||
|
@@ -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'),
|
||||
|
@@ -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>
|
@@ -1 +0,0 @@
|
||||
export { default as AppClose } from './app-close.vue';
|
@@ -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>
|
@@ -1 +0,0 @@
|
||||
export { default as AppMaxmize } from './app-maximize.vue';
|
@@ -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>
|
@@ -1 +0,0 @@
|
||||
export { default as AppMinmize } from './app-minimize.vue';
|
@@ -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';
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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');
|
||||
|
@@ -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 };
|
||||
|
@@ -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
|
||||
|
@@ -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 |
@@ -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;
|
||||
});
|
@@ -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);
|
@@ -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:"
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -8,7 +8,6 @@ import { defineOverridesPreferences } from '@vben/preferences';
|
||||
export const overridesPreferences = defineOverridesPreferences({
|
||||
// overrides
|
||||
app: {
|
||||
enableCheckUpdates: false,
|
||||
name: import.meta.env.VITE_APP_TITLE,
|
||||
},
|
||||
});
|
||||
|
@@ -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);
|
||||
|
||||
// 页面加载进度条
|
||||
|
@@ -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>
|
||||
|
@@ -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
1485
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
Reference in New Issue
Block a user