This commit is contained in:
xingyu4j
2026-07-17 11:09:46 +08:00
47 changed files with 391 additions and 237 deletions

View File

@@ -30,7 +30,7 @@ jobs:
- windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0

View File

@@ -25,7 +25,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0

View File

@@ -28,7 +28,7 @@ jobs:
timeout-minutes: 20
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
@@ -67,7 +67,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
@@ -90,7 +90,7 @@ jobs:
- windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0

View File

@@ -57,7 +57,7 @@ jobs:
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
@@ -43,7 +43,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
@@ -67,7 +67,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
@@ -98,7 +98,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
@@ -129,7 +129,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0

View File

@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0

View File

@@ -12,18 +12,9 @@ const routes: RouteRecordRaw[] = [
name: 'Dashboard',
path: '/dashboard',
children: [
{
name: 'Workspace',
path: '/workspace',
component: () => import('#/views/dashboard/workspace/index.vue'),
meta: {
icon: 'carbon:workspace',
title: $t('page.dashboard.workspace'),
},
},
{
name: 'Analytics',
path: '/analytics',
path: 'analytics',
component: () => import('#/views/dashboard/analytics/index.vue'),
meta: {
affixTab: true,
@@ -31,6 +22,15 @@ const routes: RouteRecordRaw[] = [
title: $t('page.dashboard.analytics'),
},
},
{
name: 'Workspace',
path: 'workspace',
component: () => import('#/views/dashboard/workspace/index.vue'),
meta: {
icon: 'carbon:workspace',
title: $t('page.dashboard.workspace'),
},
},
],
},
{

View File

@@ -1,7 +1,7 @@
import { initPreferences } from '@vben/preferences';
import { unmountGlobalLoading } from '@vben/utils';
import { overridesPreferences, preferencesExtension } from './preferences';
import { overridesPreferences } from './preferences';
/**
* 应用初始化完成之后再进行页面加载渲染
@@ -15,7 +15,7 @@ async function initApplication() {
// app偏好设置初始化
await initPreferences({
extension: preferencesExtension,
// extension: preferencesExtension,
namespace,
overrides: overridesPreferences,
});

View File

@@ -1,14 +1,4 @@
import {
defineOverridesPreferences,
definePreferencesExtension,
} from '@vben/preferences';
interface WebAntdPreferencesExtension {
defaultTableSize: number;
enableFormFullscreen: boolean;
reportTitle: string;
tenantMode: 'multi' | 'single';
}
import { defineOverridesPreferences } from '@vben/preferences';
/**
* @description 项目配置文件
@@ -35,51 +25,51 @@ export const overridesPreferences = defineOverridesPreferences({
// copyright: appCopyrightPreferences,
});
export const preferencesExtension =
definePreferencesExtension<WebAntdPreferencesExtension>({
tabLabel: 'preferences.antd.tabLabel',
title: 'preferences.antd.title',
fields: [
{
component: 'switch',
defaultValue: true,
key: 'enableFormFullscreen',
label: 'preferences.antd.fields.enableFormFullscreen.label',
tip: 'preferences.antd.fields.enableFormFullscreen.tip',
},
{
component: 'select',
defaultValue: 'single',
key: 'tenantMode',
label: 'preferences.antd.fields.tenantMode.label',
options: [
{
label: 'preferences.antd.fields.tenantMode.options.single.label',
value: 'single',
},
{
label: 'preferences.antd.fields.tenantMode.options.multi.label',
value: 'multi',
},
],
},
{
component: 'number',
componentProps: {
max: 200,
min: 10,
step: 10,
},
defaultValue: 20,
key: 'defaultTableSize',
label: 'preferences.antd.fields.defaultTableSize.label',
},
{
component: 'input',
defaultValue: '',
key: 'reportTitle',
label: 'preferences.antd.fields.reportTitle.label',
placeholder: 'preferences.antd.fields.reportTitle.placeholder',
},
],
});
// export const preferencesExtension =
// definePreferencesExtension<WebAntdPreferencesExtension>({
// tabLabel: 'preferences.antd.tabLabel',
// title: 'preferences.antd.title',
// fields: [
// {
// component: 'switch',
// defaultValue: true,
// key: 'enableFormFullscreen',
// label: 'preferences.antd.fields.enableFormFullscreen.label',
// tip: 'preferences.antd.fields.enableFormFullscreen.tip',
// },
// {
// component: 'select',
// defaultValue: 'single',
// key: 'tenantMode',
// label: 'preferences.antd.fields.tenantMode.label',
// options: [
// {
// label: 'preferences.antd.fields.tenantMode.options.single.label',
// value: 'single',
// },
// {
// label: 'preferences.antd.fields.tenantMode.options.multi.label',
// value: 'multi',
// },
// ],
// },
// {
// component: 'number',
// componentProps: {
// max: 200,
// min: 10,
// step: 10,
// },
// defaultValue: 20,
// key: 'defaultTableSize',
// label: 'preferences.antd.fields.defaultTableSize.label',
// },
// {
// component: 'input',
// defaultValue: '',
// key: 'reportTitle',
// label: 'preferences.antd.fields.reportTitle.label',
// placeholder: 'preferences.antd.fields.reportTitle.placeholder',
// },
// ],
// });

View File

@@ -12,18 +12,9 @@ const routes: RouteRecordRaw[] = [
name: 'Dashboard',
path: '/dashboard',
children: [
{
name: 'Workspace',
path: '/workspace',
component: () => import('#/views/dashboard/workspace/index.vue'),
meta: {
icon: 'carbon:workspace',
title: $t('page.dashboard.workspace'),
},
},
{
name: 'Analytics',
path: '/analytics',
path: 'analytics',
component: () => import('#/views/dashboard/analytics/index.vue'),
meta: {
affixTab: true,
@@ -31,6 +22,15 @@ const routes: RouteRecordRaw[] = [
title: $t('page.dashboard.analytics'),
},
},
{
name: 'Workspace',
path: 'workspace',
component: () => import('#/views/dashboard/workspace/index.vue'),
meta: {
icon: 'carbon:workspace',
title: $t('page.dashboard.workspace'),
},
},
],
},
{

View File

@@ -23,7 +23,7 @@ const routes: RouteRecordRaw[] = [
},
{
name: 'Analytics',
path: '/analytics',
path: 'analytics',
component: () => import('#/views/dashboard/analytics/index.vue'),
meta: {
affixTab: true,
@@ -31,6 +31,15 @@ const routes: RouteRecordRaw[] = [
title: $t('page.dashboard.analytics'),
},
},
{
name: 'Workspace',
path: 'workspace',
component: () => import('#/views/dashboard/workspace/index.vue'),
meta: {
icon: 'carbon:workspace',
title: $t('page.dashboard.workspace'),
},
},
],
},
{

View File

@@ -14,7 +14,7 @@ const routes: RouteRecordRaw[] = [
children: [
{
name: 'Analytics',
path: '/analytics',
path: 'analytics',
component: () => import('#/views/dashboard/analytics/index.vue'),
meta: {
affixTab: true,
@@ -24,7 +24,7 @@ const routes: RouteRecordRaw[] = [
},
{
name: 'Workspace',
path: '/workspace',
path: 'workspace',
component: () => import('#/views/dashboard/workspace/index.vue'),
meta: {
icon: 'carbon:workspace',

View File

@@ -14,7 +14,7 @@ const routes: RouteRecordRaw[] = [
children: [
{
name: 'Analytics',
path: '/analytics',
path: 'analytics',
component: () => import('#/views/dashboard/analytics/index.vue'),
meta: {
affixTab: true,
@@ -24,7 +24,7 @@ const routes: RouteRecordRaw[] = [
},
{
name: 'Workspace',
path: '/workspace',
path: 'workspace',
component: () => import('#/views/dashboard/workspace/index.vue'),
meta: {
icon: 'carbon:workspace',

View File

@@ -30,6 +30,7 @@
"gitee",
"iconify",
"iconoir",
"indexeddb",
"intlify",
"isequal",
"jspm",
@@ -41,6 +42,7 @@
"mkdist",
"mockjs",
"moddle",
"myapp",
"naiveui",
"napi",
"nocheck",

View File

@@ -61,6 +61,13 @@ The configuration method of static routes and dynamic routes is the same. Below
### Secondary Routes
::: tip
- Only top-level routes should start their `path` with `/`; routes inside `children` should use relative segments such as `about` or `menu1`.
- When a parent route does not define `redirect`, the framework automatically fills it with the first child route. This only works when child paths stay relative.
:::
::: details Secondary Route Example Code
```ts
@@ -68,7 +75,6 @@ import type { RouteRecordRaw } from 'vue-router';
import { VBEN_LOGO_URL } from '@vben/constants';
import { BasicLayout } from '#/layouts';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
@@ -82,11 +88,10 @@ const routes: RouteRecordRaw[] = [
},
name: 'VbenProject',
path: '/vben-admin',
redirect: '/vben-admin/about',
children: [
{
name: 'VbenAbout',
path: '/vben-admin/about',
path: 'about',
component: () => import('#/views/_core/about/index.vue'),
meta: {
badgeType: 'dot',
@@ -108,6 +113,7 @@ export default routes;
::: tip
- Multi-level routes follow the same rule: keep top-level paths absolute and child paths relative, instead of repeating the full path inside `children`.
- The parent route of multi-level routes does not need to set the `component` property, just set the `children` property. Unless you really need to display content nested under the parent route.
- In most cases, the `redirect` property of the parent route does not need to be specified, it will default to the first child route.
@@ -118,7 +124,6 @@ export default routes;
```ts
import type { RouteRecordRaw } from 'vue-router';
import { BasicLayout } from '#/layouts';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
@@ -131,7 +136,6 @@ const routes: RouteRecordRaw[] = [
},
name: 'Demos',
path: '/demos',
redirect: '/demos/access',
children: [
// Nested menu
{
@@ -140,12 +144,11 @@ const routes: RouteRecordRaw[] = [
title: $t('demos.nested.title'),
},
name: 'NestedDemos',
path: '/demos/nested',
redirect: '/demos/nested/menu1',
path: 'nested',
children: [
{
name: 'Menu1Demo',
path: '/demos/nested/menu1',
path: 'menu1',
component: () => import('#/views/demos/nested/menu-1.vue'),
meta: {
icon: 'ic:round-menu',
@@ -155,17 +158,16 @@ const routes: RouteRecordRaw[] = [
},
{
name: 'Menu2Demo',
path: '/demos/nested/menu2',
path: 'menu2',
meta: {
icon: 'ic:round-menu',
keepAlive: true,
title: $t('demos.nested.menu2'),
},
redirect: '/demos/nested/menu2/menu2-1',
children: [
{
name: 'Menu21Demo',
path: '/demos/nested/menu2/menu2-1',
path: 'menu2-1',
component: () => import('#/views/demos/nested/menu-2-1.vue'),
meta: {
icon: 'ic:round-menu',
@@ -177,12 +179,11 @@ const routes: RouteRecordRaw[] = [
},
{
name: 'Menu3Demo',
path: '/demos/nested/menu3',
path: 'menu3',
meta: {
icon: 'ic:round-menu',
title: $t('demos.nested.menu3'),
},
redirect: '/demos/nested/menu3/menu3-1',
children: [
{
name: 'Menu31Demo',
@@ -201,11 +202,10 @@ const routes: RouteRecordRaw[] = [
icon: 'ic:round-menu',
title: $t('demos.nested.menu3_2'),
},
redirect: '/demos/nested/menu3/menu3-2/menu3-2-1',
children: [
{
name: 'Menu321Demo',
path: '/demos/nested/menu3/menu3-2/menu3-2-1',
path: 'menu3-2-1',
component: () =>
import('#/views/demos/nested/menu-3-2-1.vue'),
meta: {
@@ -242,7 +242,6 @@ import type { RouteRecordRaw } from 'vue-router';
import { VBEN_LOGO_URL } from '@vben/constants';
import { BasicLayout } from '#/layouts';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
@@ -253,11 +252,10 @@ const routes: RouteRecordRaw[] = [
},
name: 'Home',
path: '/home',
redirect: '/home/index',
children: [
{
name: 'HomeIndex',
path: '/home/index',
path: 'index',
component: () => import('#/views/home/index.vue'),
meta: {
icon: 'mdi:home',
@@ -294,11 +292,11 @@ The route configuration items are mainly in the `meta` property of the route obj
```ts {5-8}
const routes = [
{
name: 'HomeIndex',
path: '/home/index',
name: 'Home',
path: '/home',
meta: {
icon: 'mdi:home',
title: $t('page.home.index'),
title: $t('page.home.title'),
},
},
];

View File

@@ -4,7 +4,7 @@
This document explains how to use Mock data and interact with the server in a development environment, involving technologies such as:
- [Nitro](https://nitro.unjs.io/) A lightweight backend server that can be deployed anywhere, used as a Mock server in the project.
- [Nitro](https://nitro.build/) A lightweight backend server that can be deployed anywhere, used as a Mock server in the project.
- [axios](https://axios-http.com/docs/intro) Used to send HTTP requests to interact with the server.
:::
@@ -63,16 +63,18 @@ Based on the above configuration, we can use `/api` as the prefix for API reques
```ts
import axios from 'axios';
axios.get('/api/user').then((res) => {
console.log(res);
});
axios
.post('/api/auth/login', { username: 'vben', password: '123456' })
.then((res) => {
console.log(res);
});
```
At this point, the request will be proxied to `http://localhost:5320/api/user`.
At this point, the request will be proxied to `http://localhost:5320/api/auth/login`.
::: warning Note
From the browser's console Network tab, the request appears as `http://localhost:5555/api/user`. This is because the proxy configuration does not change the local request's URL.
From the browser's console Network tab, the request appears as `http://localhost:5555/api/auth/login`. This is because the proxy configuration does not change the local request's URL.
:::
@@ -338,13 +340,13 @@ The new version no longer supports mock in the production environment. Please us
Mock data is an indispensable part of frontend development, serving as a key link in separating frontend and backend development. By agreeing on interfaces with the server side in advance and simulating request data and even logic, frontend development can proceed independently, without being blocked by the backend development process.
The project uses [Nitro](https://nitro.unjs.io/) for local mock data processing. The principle is to start an additional backend service locally, which is a real backend service that can handle requests and return data.
The project uses [Nitro](https://nitro.build/) for local mock data processing. The principle is to start an additional backend service locally, which is a real backend service that can handle requests and return data.
### Using Nitro
The mock service code is located in the `apps/backend-mock` directory. It does not need to be started manually and is already integrated into the project. You only need to run `pnpm dev` in the project root directory. After running successfully, the console will print `http://localhost:5320/api`, and you can access this address to view the mock service.
[Nitro](https://nitro.unjs.io/) syntax is simple, and you can configure and develop according to your needs. For specific configurations, you can refer to the [Nitro documentation](https://nitro.unjs.io/).
[Nitro](https://nitro.build/) syntax is simple, and you can configure and develop according to your needs. For specific configurations, you can refer to the [Nitro documentation](https://nitro.build/docs/).
## Disabling Mock Service

View File

@@ -18,7 +18,7 @@ The rules are consistent with [Vite Env Variables and Modes](https://vitejs.dev/
- Only variables starting with `VITE_` will be embedded into the client-side package. You can access them in the project code like this:
```ts
console.log(import.meta.env.VITE_PROT);
console.log(import.meta.env.VITE_PORT);
```
- Variables starting with `VITE_GLOB_*` will be added to the `_app-config-{version}-{hash}.js` configuration file during packaging.
@@ -365,7 +365,7 @@ const defaultPreferences: Preferences = {
contentPaddingTop: 0,
defaultAvatar:
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
defaultHomePath: '/analytics',
defaultHomePath: '/dashboard/analytics',
dynamicTitle: true,
enableCheckUpdates: true,
enablePreferences: true,

View File

@@ -69,7 +69,7 @@ features:
icon:
src: /logos/nitro.svg
details: Built-in Nitro Mock service makes your mock service more powerful.
link: https://nitro.unjs.io/
link: https://nitro.build/
linkText: Official Site
---

View File

@@ -55,6 +55,13 @@ const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
### 二级路由
::: tip
- 只有顶级路由的 `path` 需要以 `/` 开头;写在 `children` 中的子路由请使用相对路径片段,例如 `about``menu1`
- 当父级路由未显式配置 `redirect` 时,框架会自动补全到第一个子路由的重定向;这依赖子路由使用相对路径。
:::
::: details 二级路由示例代码
```ts
@@ -75,11 +82,10 @@ const routes: RouteRecordRaw[] = [
},
name: 'VbenProject',
path: '/vben-admin',
redirect: '/vben-admin/about',
children: [
{
name: 'VbenAbout',
path: '/vben-admin/about',
path: 'about',
component: () => import('#/views/_core/about/index.vue'),
meta: {
badgeType: 'dot',
@@ -101,6 +107,7 @@ export default routes;
::: tip
- 多级路由同样遵循“顶级绝对路径、子级相对路径”的组织方式,避免在 `children` 中重复写完整路径。
- 如果没有特殊情况,父级路由的 `redirect` 属性,不需要指定,默认会指向第一个子路由。
:::
@@ -122,7 +129,6 @@ const routes: RouteRecordRaw[] = [
},
name: 'Demos',
path: '/demos',
redirect: '/demos/access',
children: [
// 嵌套菜单
{
@@ -131,12 +137,11 @@ const routes: RouteRecordRaw[] = [
title: $t('demos.nested.title'),
},
name: 'NestedDemos',
path: '/demos/nested',
redirect: '/demos/nested/menu1',
path: 'nested',
children: [
{
name: 'Menu1Demo',
path: '/demos/nested/menu1',
path: 'menu1',
component: () => import('#/views/demos/nested/menu-1.vue'),
meta: {
icon: 'ic:round-menu',
@@ -146,17 +151,16 @@ const routes: RouteRecordRaw[] = [
},
{
name: 'Menu2Demo',
path: '/demos/nested/menu2',
path: 'menu2',
meta: {
icon: 'ic:round-menu',
keepAlive: true,
title: $t('demos.nested.menu2'),
},
redirect: '/demos/nested/menu2/menu2-1',
children: [
{
name: 'Menu21Demo',
path: '/demos/nested/menu2/menu2-1',
path: 'menu2-1',
component: () => import('#/views/demos/nested/menu-2-1.vue'),
meta: {
icon: 'ic:round-menu',
@@ -168,12 +172,11 @@ const routes: RouteRecordRaw[] = [
},
{
name: 'Menu3Demo',
path: '/demos/nested/menu3',
path: 'menu3',
meta: {
icon: 'ic:round-menu',
title: $t('demos.nested.menu3'),
},
redirect: '/demos/nested/menu3/menu3-1',
children: [
{
name: 'Menu31Demo',
@@ -192,11 +195,10 @@ const routes: RouteRecordRaw[] = [
icon: 'ic:round-menu',
title: $t('demos.nested.menu3_2'),
},
redirect: '/demos/nested/menu3/menu3-2/menu3-2-1',
children: [
{
name: 'Menu321Demo',
path: '/demos/nested/menu3/menu3-2/menu3-2-1',
path: 'menu3-2-1',
component: () =>
import('#/views/demos/nested/menu-3-2-1.vue'),
meta: {
@@ -243,11 +245,10 @@ const routes: RouteRecordRaw[] = [
},
name: 'Home',
path: '/home',
redirect: '/home/index',
children: [
{
name: 'HomeIndex',
path: '/home/index',
path: 'index',
component: () => import('#/views/home/index.vue'),
meta: {
icon: 'mdi:home',
@@ -284,11 +285,11 @@ export default routes;
```ts {5-8}
const routes = [
{
name: 'HomeIndex',
path: '/home/index',
name: 'Home',
path: '/home',
meta: {
icon: 'mdi:home',
title: $t('page.home.index'),
title: $t('page.home.title'),
},
},
];

View File

@@ -4,7 +4,7 @@
本文档介绍如何在开发环境下使用 Mock 数据和与服务端进行交互,涉及到的技术有:
- [Nitro](https://nitro.unjs.io/) 轻量级后端服务器,可部署在任何地方,项目用作于 Mock 服务器。
- [Nitro](https://nitro.build/) 轻量级后端服务器,可部署在任何地方,项目用作于 Mock 服务器。
- [axios](https://axios-http.com/docs/intro) 用于发送 HTTP 请求与服务端进行交互。
:::
@@ -63,16 +63,18 @@ export default defineConfig(async () => {
```ts
import axios from 'axios';
axios.get('/api/user').then((res) => {
console.log(res);
});
axios
.post('/api/auth/login', { username: 'vben', password: '123456' })
.then((res) => {
console.log(res);
});
```
此时,请求会被代理到 `http://localhost:5320/api/user`。
此时,请求会被代理到 `http://localhost:5320/api/auth/login`。
::: warning 注意
从浏览器控制台的 Network 看,请求是 `http://localhost:5555/api/user`, 这是因为 proxy 配置不会改变本地请求的 url。
从浏览器控制台的 Network 看,请求是 `http://localhost:5555/api/auth/login`, 这是因为 proxy 配置不会改变本地请求的 url。
:::
@@ -369,13 +371,13 @@ async function doRefreshToken() {
Mock 数据是前端开发过程中必不可少的一环,是分离前后端开发的关键链路。通过预先跟服务器端约定好的接口,模拟请求数据甚至逻辑,能够让前端开发独立自主,不会被服务端的开发进程所阻塞。
项目使用 [Nitro](https://nitro.unjs.io/) 来进行本地 mock 数据处理。其原理是本地额外启动一个后端服务,是一个真实的后端服务,可以处理请求,返回数据。
项目使用 [Nitro](https://nitro.build/) 来进行本地 mock 数据处理。其原理是本地额外启动一个后端服务,是一个真实的后端服务,可以处理请求,返回数据。
### Nitro 使用
Mock 服务代码位于`apps/backend-mock`目录下,无需手动启动,已经集成在项目中,只需要在项目根目录下运行`pnpm dev`即可,运行成功之后,控制台会打印 `http://localhost:5320/api`, 访问该地址即可查看 mock 服务。
[Nitro](https://nitro.unjs.io/) 语法简单,可以根据自己的需求进行配置及开发,具体配置可以查看 [Nitro 文档](https://nitro.unjs.io/)。
[Nitro](https://nitro.build/) 语法简单,可以根据自己的需求进行配置及开发,具体配置可以查看 [Nitro 文档](https://nitro.build/docs/)。
## 关闭 Mock 服务

View File

@@ -18,7 +18,7 @@
- 只有以 `VITE_` 开头的变量会被嵌入到客户端侧的包中,你可以在项目代码中这样访问它们:
```ts
console.log(import.meta.env.VITE_PROT);
console.log(import.meta.env.VITE_PORT);
```
- 以 `VITE_GLOB_*` 开头的的变量,在打包的时候,会被加入 `_app-config-{version}-{hash}.js`配置文件当中.
@@ -364,7 +364,7 @@ const defaultPreferences: Preferences = {
contentPaddingTop: 0,
defaultAvatar:
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
defaultHomePath: '/analytics',
defaultHomePath: '/dashboard/analytics',
dynamicTitle: true,
enableCheckUpdates: true,
enablePreferences: true,

View File

@@ -72,7 +72,7 @@ features:
icon:
src: /logos/nitro.svg
details: 内置 Nitro Mock 服务,让你的 mock 服务更加强大。
link: https://nitro.unjs.io/
link: https://nitro.build/
linkText: 官方站点
---

View File

@@ -16,6 +16,7 @@ const rulesCoveredByOxlint = new Set([
'no-const-assign',
'no-constant-binary-expression',
'no-constant-condition',
'no-control-regex',
'no-debugger',
'no-delete-var',
'no-dupe-args',
@@ -48,6 +49,8 @@ const rulesCoveredByOxlint = new Set([
'no-shadow-restricted-names',
'no-sparse-arrays',
'no-this-before-super',
'no-unassigned-vars',
'no-unexpected-multiline',
'no-unreachable',
'no-unsafe-finally',
'no-unsafe-negation',
@@ -59,6 +62,7 @@ const rulesCoveredByOxlint = new Set([
'no-useless-catch',
'no-useless-escape',
'no-with',
'preserve-caught-error',
'require-yield',
'use-isnan',
'valid-typeof',
@@ -102,7 +106,6 @@ export async function javascript(): Promise<Linter.Config[]> {
...recommendedRules,
'dot-notation': ['error', { allowKeywords: true }],
'keyword-spacing': 'off',
'no-control-regex': 'error',
'no-empty-function': 'off',
'no-octal': 'error',
'no-octal-escape': 'error',

View File

@@ -41,7 +41,7 @@ const javascript: OxlintConfig = {
'no-caller': 'error',
'no-case-declarations': 'error',
'no-console': ['error', { allow: ['warn', 'error'] }],
'no-control-regex': 'off',
'no-control-regex': 'error',
'no-debugger': 'error',
'no-empty': ['error', { allowEmptyCatch: true }],
'no-fallthrough': 'error',
@@ -68,6 +68,8 @@ const javascript: OxlintConfig = {
],
'no-template-curly-in-string': 'error',
'no-throw-literal': 'error',
'no-unassigned-vars': 'error',
'no-unexpected-multiline': 'error',
'no-unused-expressions': [
'error',
{
@@ -108,6 +110,12 @@ const javascript: OxlintConfig = {
'prefer-rest-params': 'error',
'prefer-spread': 'error',
'prefer-template': 'error',
'preserve-caught-error': [
'error',
{
requireCatchParameter: false,
},
],
'symbol-description': 'error',
'unicode-bom': ['error', 'never'],
'use-isnan': [

View File

@@ -2,6 +2,7 @@ import type { OxlintConfig } from 'oxlint';
const vue: OxlintConfig = {
rules: {
'vue/no-reserved-component-names': 'off',
'vue/prefer-import-from-vue': 'error',
},
};

View File

@@ -20,7 +20,13 @@ async function getStagedFiles(): Promise<string[]> {
'-z',
]);
let changedList = stdout ? stdout.replace(/\0$/, '').split('\0') : [];
const nullSeparator = '\u0000';
const normalizedStdout = stdout.endsWith(nullSeparator)
? stdout.slice(0, -1)
: stdout;
let changedList = normalizedStdout
? normalizedStdout.split(nullSeparator)
: [];
changedList = changedList.map((item) => path.resolve(process.cwd(), item));
const changedSet = new Set(changedList);
changedSet.delete('');

View File

@@ -192,16 +192,22 @@ function filterTree<T extends Record<string, any>>(
*/
function mapTree<T, V extends Record<string, any>>(
tree: T[],
mapper: (node: T) => V,
mapper: (node: T, parent: null | V) => V,
options?: TreeConfigOptions,
parent: null | V = null,
): V[] {
const { childProps } = options || {
childProps: 'children',
};
return tree.map((node) => {
const mapperNode: Record<string, any> = mapper(node);
const mapperNode: Record<string, any> = mapper(node, parent as null | V);
if (mapperNode[childProps]) {
mapperNode[childProps] = mapTree(mapperNode[childProps], mapper, options);
mapperNode[childProps] = mapTree(
mapperNode[childProps],
mapper,
options,
mapperNode as V,
);
}
return mapperNode as V;
});

View File

@@ -8,6 +8,7 @@ type SelectOption = BasicOption;
type TabOption = BasicOption;
interface BasicUserInfo {
[key: string]: any;
/**
* 头像
*/

View File

@@ -17,7 +17,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
"contentPaddingRight": 0,
"contentPaddingTop": 0,
"defaultAvatar": "https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp",
"defaultHomePath": "/analytics",
"defaultHomePath": "/dashboard",
"dynamicTitle": true,
"enableCheckUpdates": true,
"enableCopyPreferences": true,
@@ -66,6 +66,8 @@ exports[`defaultPreferences immutability test > should not modify the config obj
"logo": {
"enable": true,
"fit": "contain",
"logoMode": "icon",
"showText": true,
"source": "https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp",
},
"navigation": {

View File

@@ -17,7 +17,7 @@ const defaultPreferences: Preferences = {
contentPaddingTop: 0,
defaultAvatar:
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
defaultHomePath: '/analytics',
defaultHomePath: '/dashboard',
dynamicTitle: true,
enableCheckUpdates: true,
enableCopyPreferences: true,
@@ -68,6 +68,8 @@ const defaultPreferences: Preferences = {
enable: true,
fit: 'contain',
source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
showText: true,
logoMode: 'icon',
},
navigation: {
accordion: true,

View File

@@ -228,6 +228,12 @@ interface LogoPreferences {
enable: boolean;
/** logo图片适应方式 */
fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
/** logo高度 只在 logoMode=full时生效 */
fullLogoHeight?: number | string;
/** logo 展示类型icon 图标模式, full 铺满logo区域 */
logoMode: 'full' | 'icon';
/** logo text是否展示 */
showText: boolean;
/** logo地址 */
source: string;
/** 暗色主题logo地址 (可选,若不设置则使用 source) */

View File

@@ -16,6 +16,10 @@ interface Props {
* 是否移动端
*/
isMobile: boolean;
/**
* logo是否显示
*/
logoVisible?: boolean;
/**
* 是否显示
*/
@@ -38,7 +42,9 @@ interface Props {
zIndex: number;
}
const props = withDefaults(defineProps<Props>(), {});
const props = withDefaults(defineProps<Props>(), {
logoVisible: true,
});
const slots = useSlots();
@@ -54,6 +60,12 @@ const style = computed((): CSSProperties => {
});
const logoStyle = computed((): CSSProperties => {
if (!props.logoVisible) {
return {
minWidth: '12px',
};
}
return {
minWidth: `${props.isMobile ? 40 : props.sidebarWidth}px`,
};
@@ -66,7 +78,7 @@ const logoStyle = computed((): CSSProperties => {
:style="style"
class="top-0 flex w-full flex-[0_0_auto] items-center border-b border-border bg-header pl-2 transition-[margin-top] duration-200"
>
<div v-if="slots.logo" :style="logoStyle">
<div v-if="slots.logo || (!logoVisible && !isMobile)" :style="logoStyle">
<slot name="logo"></slot>
</div>

View File

@@ -26,6 +26,10 @@ interface Props {
* @default true
*/
domVisible?: boolean;
/**
* 扩展区域extra-title的高度
*/
extraTitleHeight?: number;
/**
* 扩展区域宽度
*/
@@ -97,6 +101,7 @@ const props = withDefaults(defineProps<Props>(), {
collapseHeight: 42,
collapseWidth: 48,
domVisible: true,
extraTitleHeight: undefined,
fixedExtra: false,
isSidebarMixed: false,
marginTop: 0,
@@ -149,10 +154,10 @@ const extraStyle = computed((): CSSProperties => {
});
const extraTitleStyle = computed((): CSSProperties => {
const { headerHeight } = props;
const { extraTitleHeight, headerHeight } = props;
return {
height: `${headerHeight - 1}px`,
height: `${extraTitleHeight ?? headerHeight - 1}px`,
};
});
@@ -185,9 +190,10 @@ const headerStyle = computed((): CSSProperties => {
});
const extraContentStyle = computed((): CSSProperties => {
const { collapseHeight, headerHeight } = props;
const { collapseHeight, extraTitleHeight, headerHeight } = props;
const titleHeight = extraTitleHeight ?? headerHeight;
return {
height: `calc(100% - ${headerHeight + collapseHeight}px)`,
height: `calc(100% - ${titleHeight + collapseHeight}px)`,
};
});

View File

@@ -126,6 +126,10 @@ interface VbenLayoutProps {
* @default 48
*/
sidebarExtraCollapsedWidth?: number;
/**
* 扩展区域extra-title的高度
*/
sidebarExtraTitleHeight?: number;
/**
* 侧边菜单折叠按钮是否固定
* @default true
@@ -136,6 +140,10 @@ interface VbenLayoutProps {
* @default false
*/
sidebarHidden?: boolean;
/**
* 侧边栏 Logo 区域是否显示
*/
sidebarLogoVisible: boolean;
/**
* 混合侧边栏宽度
* @default 80

View File

@@ -368,6 +368,17 @@ const maskStyle = computed((): CSSProperties => {
return { zIndex: props.zIndex };
});
/**
* 侧边栏 Logo 区域是否显示
*/
const sidebarHeaderHeight = computed(() => {
if (isMixedNav.value || !props.sidebarLogoVisible) {
return 0;
}
return props.headerHeight;
});
const showHeaderToggleButton = computed(() => {
return (
props.isMobile ||
@@ -510,7 +521,10 @@ const idMainContent = ELEMENT_ID_MAIN_CONTENT;
:dom-visible="!isMobile"
:extra-width="sidebarExtraWidth"
:fixed-extra="sidebarExpandOnHover"
:header-height="isMixedNav ? 0 : headerHeight"
:header-height="sidebarHeaderHeight"
:extra-title-height="
isSidebarMixedNav || isHeaderMixedNav ? sidebarExtraTitleHeight : 0
"
:is-sidebar-mixed="isSidebarMixedNav || isHeaderMixedNav"
:margin-top="sidebarMarginTop"
:mixed-width="sidebarMixedWidth"
@@ -522,7 +536,7 @@ const idMainContent = ELEMENT_ID_MAIN_CONTENT;
@leave="() => emit('sideMouseLeave')"
@update:width="(val) => emit('update:sidebarWidth', val)"
>
<template v-if="isSideMode && !isMixedNav" #logo>
<template v-if="isSideMode && !isMixedNav && sidebarLogoVisible" #logo>
<slot name="logo"></slot>
</template>
@@ -565,6 +579,7 @@ const idMainContent = ELEMENT_ID_MAIN_CONTENT;
:theme="headerTheme"
:width="mainStyle.width"
:z-index="headerZIndex"
:logo-visible="sidebarLogoVisible"
>
<template v-if="showHeaderLogo" #logo>
<slot name="logo"></slot>

View File

@@ -243,7 +243,6 @@ function handleClosed() {
cn(
'inset-x-0 top-[10vh] mx-auto flex w-130 flex-col p-0',
shouldFullscreen ? 'rounded-none' : 'rounded-(--radius)',
modalClass,
{
'border border-border': bordered,
'shadow-3xl': !bordered,
@@ -255,6 +254,7 @@ function handleClosed() {
'duration-300': !dragging,
hidden: isClosed,
},
modalClass,
)
"
:force-mount="getForceMount"

View File

@@ -5,21 +5,33 @@ import { VbenAvatar } from '../avatar';
interface Props {
/**
* @zh_CN 是否收起文本
* @zh_CN 是否收起文本;布局状态,侧边栏收起时隐藏文字。
*/
collapsed?: boolean;
/**
* @zh_CN Logo 图片适应方式
*/
fit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
/**
* @zh_CN logo高度 只在 logoMode=full时失效
*/
fullLogoHeight?: number | string;
/**
* @zh_CN Logo 跳转地址
*/
href?: string;
/**
* @zh_CN logo 展示类型icon 图标模式, full 铺满logo区域
*/
logoMode?: 'full' | 'icon';
/**
* @zh_CN Logo 图片大小
*/
logoSize?: number;
/**
* @zh_CN Logo 是否展示文本
*/
showText?: boolean;
/**
* @zh_CN Logo 图标
*/
@@ -28,10 +40,12 @@ interface Props {
* @zh_CN 暗色主题 Logo 图标 (可选,若不设置则使用 src)
*/
srcDark?: string;
/**
* @zh_CN Logo 文本
*/
text: string;
/**
* @zh_CN Logo 主题
*/
@@ -45,11 +59,14 @@ defineOptions({
const props = withDefaults(defineProps<Props>(), {
collapsed: false,
href: 'javascript:void 0',
logoMode: 'icon',
logoSize: 32,
fullLogoHeight: 42,
src: '',
srcDark: '',
theme: 'light',
fit: 'cover',
showText: true,
});
/**
@@ -63,24 +80,67 @@ const logoSrc = computed(() => {
// 否则使用默认的 src
return props.src;
});
/**
* @zh_CN 是否铺满容器显示log
*/
const shouldUseFullLogo = computed(() => {
return props.logoMode === 'full' && !props.collapsed;
});
/**
* @zh_CN 根据配置展示 logo text
*/
const shouldShowText = computed(() => {
return (
props.showText &&
!props.collapsed &&
!shouldUseFullLogo.value &&
!!props.text
);
});
/**
* @zh_CN full 模式下logo的样式
*/
const fullLogoStyle = computed(() => ({
height:
typeof props.fullLogoHeight === 'number'
? `${props.fullLogoHeight}px`
: props.fullLogoHeight,
objectFit: props.fit,
}));
</script>
<template>
<div :class="theme" class="flex h-full items-center text-lg">
<a
:class="$attrs.class"
:class="[
$attrs.class,
shouldShowText
? 'gap-2 px-3 justify-start'
: 'w-full p-0 justify-center',
]"
:href="href"
class="flex h-full items-center gap-2 overflow-hidden px-3 text-lg leading-normal transition-all duration-500"
class="flex h-full items-center overflow-hidden text-lg leading-normal transition-all duration-500"
>
<img
v-if="logoSrc && shouldUseFullLogo"
:alt="text"
:src="logoSrc"
:style="fullLogoStyle"
class="w-full"
/>
<VbenAvatar
v-if="logoSrc"
v-else-if="logoSrc"
:alt="text"
:src="logoSrc"
:size="logoSize"
:fit="fit"
class="relative rounded-none bg-transparent"
/>
<template v-if="!collapsed">
<template v-if="shouldShowText">
<slot name="text">
<span class="text-foreground truncate font-semibold text-nowrap">
{{ text }}

View File

@@ -6,6 +6,7 @@ import { computed } from 'vue';
import { TabsTrigger } from 'reka-ui';
import { Tabs, TabsContent, TabsList } from '../../ui';
import { VbenTooltip } from '../tooltip';
import TabsIndicator from './tabs-indicator.vue';
interface Props {
@@ -52,9 +53,16 @@ function activeClass(tab: string): string[] {
<TabsTrigger
:value="tab.value"
:class="activeClass(tab.value)"
class="hover:text-primary z-20 inline-flex items-center justify-center rounded-md px-3 py-1 text-sm font-medium whitespace-nowrap disabled:pointer-events-none disabled:opacity-50"
class="hover:text-primary z-20 size-full inline-flex items-center justify-center rounded-md text-sm font-medium whitespace-nowrap disabled:pointer-events-none disabled:opacity-50"
>
{{ tab.label }}
<VbenTooltip :delay-duration="300" side="bottom">
<template #trigger>
<div class="whitespace-nowrap overflow-hidden text-ellipsis px-1">
{{ tab.label }}
</div>
</template>
{{ tab.label }}
</VbenTooltip>
</TabsTrigger>
</template>
</TabsList>

View File

@@ -23,13 +23,13 @@ const forwardedProps = useForwardProps(delegatedProps);
v-bind="forwardedProps"
:class="
cn(
'absolute bottom-0 left-0 z-10 h-full w-1/2 translate-x-(--reka-tabs-indicator-position) rounded-full px-0 py-1 pr-0.5 transition-[width,transform] duration-300',
'absolute bottom-0 left-0 z-10 size-full translate-x-(--reka-tabs-indicator-position) rounded-full py-1 transition-[width,transform] duration-300',
props.class,
)
"
>
<div
class="bg-background text-foreground inline-flex h-full w-full items-center justify-center rounded-md px-3 py-1 text-sm font-medium whitespace-nowrap focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
class="bg-background text-foreground inline-flex size-full items-center justify-center rounded-md text-sm font-medium whitespace-nowrap focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
>
<slot></slot>
</div>

View File

@@ -21,7 +21,7 @@ const delegatedProps = reactiveOmit(props, 'class');
v-bind="delegatedProps"
:class="
cn(
'bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]',
'bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-md p-1',
props.class,
)
"

View File

@@ -5,7 +5,12 @@ import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { TooltipContent, TooltipPortal, useForwardPropsEmits } from 'reka-ui';
import {
TooltipArrow,
TooltipContent,
TooltipPortal,
useForwardPropsEmits,
} from 'reka-ui';
defineOptions({
inheritAttrs: false,
@@ -38,12 +43,13 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
data-slot="tooltip-content"
:class="
cn(
'bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-popup w-fit origin-(--reka-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance',
'bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-popup w-fit origin-(--reka-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance border border-border',
props.class,
)
"
>
<slot></slot>
<TooltipArrow class="fill-accent stroke-gray-200" />
</TooltipContent>
</TooltipPortal>
</template>

View File

@@ -115,7 +115,7 @@ async function generateRoutes(
* 1. 对未添加redirect的路由添加redirect
* 2. 将懒加载的组件名称修改为当前路由的名称如果启用了keep-alive的话
*/
resultRoutes = mapTree(resultRoutes, (route) => {
resultRoutes = mapTree(resultRoutes, (route, parent) => {
// 重新包装component使用与路由名称相同的name以支持keep-alive的条件缓存。
if (
route.meta?.keepAlive &&
@@ -144,12 +144,19 @@ async function generateRoutes(
}
const firstChild = route.children[0];
// 如果子路由不是以/开头,则直接返回,这种情况需要计算全部父级的path才能得出正确的path这里不做处理
if (!firstChild?.path || !firstChild.path.startsWith('/')) {
if (!firstChild?.path || firstChild.path.startsWith('/')) {
return route;
}
route.redirect = firstChild.path;
if (parent && parent.redirect) {
const parentSplit = (parent.redirect as string).split('/');
parentSplit.splice(-1, 2, route.path, firstChild.path);
const redirectPath = parentSplit.join('/');
route.redirect = redirectPath;
} else {
route.redirect = `${route.path}/${firstChild.path}`;
}
return route;
});

View File

@@ -44,7 +44,6 @@ const showComponent = (route: RouteLocationNormalizedLoadedGeneric) => {
v-if="getEnabledTransition"
:name="getTransitionName(route)"
appear
mode="out-in"
>
<KeepAlive
v-if="keepAlive"

View File

@@ -60,25 +60,26 @@ const rightSlots = computed(() => {
index: REFERENCE_VALUE + 10,
name: 'preferences',
});
// 将偏好设置中的子功能分组到同一个按钮位置控制逻辑下
if (preferences.widget.themeToggle) {
list.push({
index: REFERENCE_VALUE + 20,
name: 'theme-toggle',
});
}
if (preferences.widget.languageToggle) {
list.push({
index: REFERENCE_VALUE + 30,
name: 'language-toggle',
});
}
if (preferences.widget.timezone) {
list.push({
index: REFERENCE_VALUE + 40,
name: 'timezone',
});
}
}
// 主题、语言、时区等子功能由各自的 widget 开关独立控制,
// 不应跟随偏好设置按钮的显示与否(如 enablePreferences 为 false 或按钮位于其他位置时)
if (preferences.widget.themeToggle) {
list.push({
index: REFERENCE_VALUE + 20,
name: 'theme-toggle',
});
}
if (preferences.widget.languageToggle) {
list.push({
index: REFERENCE_VALUE + 30,
name: 'language-toggle',
});
}
if (preferences.widget.timezone) {
list.push({
index: REFERENCE_VALUE + 40,
name: 'timezone',
});
}
// 全屏
if (preferences.widget.fullscreen) {

View File

@@ -118,6 +118,15 @@ const logoTheme = computed(() => {
return showLogoInHeader ? headerTheme.value : sidebarTheme.value;
});
/**
* layout-sidebar扩展区域插槽extra-title的高度
*/
const sidebarExtraTitleHeight = computed<number | undefined>(() => {
const showSideExtraTitle =
preferences.logo.enable && preferences.logo.showText;
return showSideExtraTitle ? undefined : 0;
});
const {
handleMenuSelect,
handleMenuOpen,
@@ -253,12 +262,14 @@ const headerSlots = computed(() => {
:sidebar-expand-on-hover="preferences.sidebar.expandOnHover"
:sidebar-extra-collapse="preferences.sidebar.extraCollapse"
:sidebar-extra-collapsed-width="preferences.sidebar.extraCollapsedWidth"
:sidebar-extra-title-height="sidebarExtraTitleHeight"
:sidebar-hidden="preferences.sidebar.hidden"
:sidebar-mixed-width="preferences.sidebar.mixedWidth"
:sidebar-theme="sidebarTheme"
:sidebar-theme-sub="sidebarThemeSub"
:sidebar-width="preferences.sidebar.width"
:side-collapse-width="preferences.sidebar.collapseWidth"
:sidebar-logo-visible="preferences.logo.enable"
:tabbar-enable="preferences.tabbar.enable"
:tabbar-height="preferences.tabbar.height"
:z-index="preferences.app.zIndex"
@@ -292,6 +303,9 @@ const headerSlots = computed(() => {
:src="preferences.logo.source"
:src-dark="preferences.logo.sourceDark"
:text="preferences.app.name"
:show-text="preferences.logo.showText"
:logo-mode="preferences.logo.logoMode"
:full-logo-height="preferences.logo.fullLogoHeight"
:theme="logoTheme"
@click="clickLogo"
>
@@ -382,6 +396,7 @@ const headerSlots = computed(() => {
<VbenLogo
v-if="preferences.logo.enable"
:fit="preferences.logo.fit"
:show-text="preferences.logo.showText"
:text="preferences.app.name"
:theme="sidebarThemeSub"
>

View File

@@ -36,7 +36,7 @@ function handleClick() {
<VbenTooltip v-if="slots.tip || tip" side="bottom">
<template #trigger>
<CircleHelp class="ml-1 size-3 cursor-help" />
<CircleHelp class="ml-1 size-3 cursor-help pointer-events-auto" />
</template>
<slot name="tip">
<template v-if="tip">

View File

@@ -1,28 +1,6 @@
import { acceptHMRUpdate, defineStore } from 'pinia';
import type { BasicUserInfo } from '@vben-core/typings';
interface BasicUserInfo {
[key: string]: any;
/**
* 头像
*/
avatar: string;
/**
* 用户邮箱
*/
email?: string;
/**
* 用户昵称
*/
nickname: string;
/**
* 用户 id
*/
userId: string;
/**
* 用户名
*/
username: string;
}
import { acceptHMRUpdate, defineStore } from 'pinia';
interface AccessState {
/**