Merge branch 'vbenjs:main' into main
This commit is contained in:
3
.github/config.yml
vendored
3
.github/config.yml
vendored
@@ -21,12 +21,10 @@ newPRWelcomeComment: |
|
|||||||
firstPRMergeComment: >
|
firstPRMergeComment: >
|
||||||
Thanks for your contribution! 🎉🎉🎉
|
Thanks for your contribution! 🎉🎉🎉
|
||||||
|
|
||||||
|
|
||||||
# Comment to be posted to on first time issues
|
# Comment to be posted to on first time issues
|
||||||
newIssueWelcomeComment: >
|
newIssueWelcomeComment: >
|
||||||
Thanks for opening your first issue! Be sure to follow the issue template and provide every bit of information to help the developers!
|
Thanks for opening your first issue! Be sure to follow the issue template and provide every bit of information to help the developers!
|
||||||
|
|
||||||
|
|
||||||
# *OPTIONAL* default titles to check against for lack of descriptiveness
|
# *OPTIONAL* default titles to check against for lack of descriptiveness
|
||||||
# MUST BE ALL LOWERCASE
|
# MUST BE ALL LOWERCASE
|
||||||
requestInfoDefaultTitles:
|
requestInfoDefaultTitles:
|
||||||
@@ -36,4 +34,3 @@ requestInfoDefaultTitles:
|
|||||||
# *Required* Comment to reply with
|
# *Required* Comment to reply with
|
||||||
requestInfoReplyComment: >
|
requestInfoReplyComment: >
|
||||||
Thanks for filing this issue/PR! It would be much appreciated if you could provide us with more information so we can effectively analyze the situation in context.
|
Thanks for filing this issue/PR! It would be much appreciated if you could provide us with more information so we can effectively analyze the situation in context.
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export const MOCK_USERS: UserInfo[] = [
|
|||||||
realName: 'Vben',
|
realName: 'Vben',
|
||||||
roles: ['super'],
|
roles: ['super'],
|
||||||
username: 'vben',
|
username: 'vben',
|
||||||
|
homePath: '/dashboard/workspace',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -194,18 +195,46 @@ export const MOCK_MENUS = [
|
|||||||
export const MOCK_MENU_LIST = [
|
export const MOCK_MENU_LIST = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Workspace',
|
name: 'Dashboard',
|
||||||
status: 1,
|
status: 1,
|
||||||
type: 'menu',
|
type: 'catalog',
|
||||||
icon: 'mdi:dashboard',
|
icon: 'lucide:layout-dashboard',
|
||||||
path: '/workspace',
|
path: '/dashboard',
|
||||||
component: '/dashboard/workspace/index',
|
|
||||||
meta: {
|
meta: {
|
||||||
icon: 'carbon:workspace',
|
icon: 'lucide:layout-dashboard',
|
||||||
title: 'page.dashboard.workspace',
|
order: -1,
|
||||||
affixTab: true,
|
title: 'page.dashboard.title',
|
||||||
order: 0,
|
|
||||||
},
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 101,
|
||||||
|
pid: 1,
|
||||||
|
status: 1,
|
||||||
|
type: 'menu',
|
||||||
|
name: 'Analytics',
|
||||||
|
path: 'analytics',
|
||||||
|
component: '/dashboard/analytics/index',
|
||||||
|
meta: {
|
||||||
|
affixTab: true,
|
||||||
|
icon: 'lucide:area-chart',
|
||||||
|
title: 'page.dashboard.analytics',
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 102,
|
||||||
|
pid: 1,
|
||||||
|
status: 1,
|
||||||
|
type: 'menu',
|
||||||
|
name: 'Workspace',
|
||||||
|
path: 'workspace',
|
||||||
|
component: '/views/dashboard/workspace/index',
|
||||||
|
meta: {
|
||||||
|
icon: 'carbon:workspace',
|
||||||
|
title: 'page.dashboard.workspace',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ function info() {
|
|||||||
function error() {
|
function error() {
|
||||||
message.error({
|
message.error({
|
||||||
content: 'Once upon a time you dressed so fine',
|
content: 'Once upon a time you dressed so fine',
|
||||||
duration: 2500,
|
// ant-design-vue 的 message.duration 单位是「秒」, 不是毫秒
|
||||||
|
duration: 2.5,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +26,8 @@ function success() {
|
|||||||
|
|
||||||
function notify(type: NotificationType) {
|
function notify(type: NotificationType) {
|
||||||
notification[type]({
|
notification[type]({
|
||||||
duration: 2500,
|
// ant-design-vue 的 notification.duration 单位是「秒」, 不是毫秒
|
||||||
|
duration: 2.5,
|
||||||
message: '说点啥呢',
|
message: '说点啥呢',
|
||||||
type,
|
type,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -80,10 +80,7 @@ export type PromptProps<T = any> = {
|
|||||||
component?: Component;
|
component?: Component;
|
||||||
componentProps?: Recordable<any>;
|
componentProps?: Recordable<any>;
|
||||||
componentSlots?:
|
componentSlots?:
|
||||||
| (() => any)
|
(() => any) | Recordable<unknown> | VNode | VNodeArrayChildren;
|
||||||
| Recordable<unknown>
|
|
||||||
| VNode
|
|
||||||
| VNodeArrayChildren;
|
|
||||||
defaultValue?: T;
|
defaultValue?: T;
|
||||||
modelPropName?: string;
|
modelPropName?: string;
|
||||||
} & Omit<AlertProps, 'beforeClose'>;
|
} & Omit<AlertProps, 'beforeClose'>;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const items = [
|
|||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div style="display: flex; flex-direction: column; gap: 16px">
|
<div class="flex gap-4 flex-col">
|
||||||
<VbenDescriptions
|
<VbenDescriptions
|
||||||
size="small"
|
size="small"
|
||||||
bordered
|
bordered
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { DescriptionsItemType } from '@vben/common-ui';
|
||||||
|
|
||||||
import { VbenDescriptions } from '@vben/common-ui';
|
import { VbenDescriptions } from '@vben/common-ui';
|
||||||
|
|
||||||
const items = [
|
const items: DescriptionsItemType[] = [
|
||||||
{ content: '1', label: 'A' },
|
{ content: '1', label: 'A' },
|
||||||
{ content: '2(span: 2)', label: 'B', span: 2 },
|
{ content: '2(span: 2)', label: 'B', span: 2 },
|
||||||
{ content: '3', label: 'C' },
|
{ content: '3', label: 'C' },
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { DescriptionsItemType } from '@vben/common-ui';
|
||||||
|
|
||||||
import { VbenDescriptions } from '@vben/common-ui';
|
import { VbenDescriptions } from '@vben/common-ui';
|
||||||
|
|
||||||
const items = [
|
const items: DescriptionsItemType[] = [
|
||||||
{ content: 'Vben', label: '用户名' },
|
{ content: 'Vben', label: '用户名' },
|
||||||
{ content: '13800138000', label: '手机号' },
|
{ content: '13800138000', label: '手机号' },
|
||||||
{ content: '中国 · 杭州', label: '居住地' },
|
{ content: '中国 · 杭州', label: '居住地' },
|
||||||
|
|||||||
@@ -66,10 +66,7 @@ export type PromptProps<T = any> = {
|
|||||||
component?: Component;
|
component?: Component;
|
||||||
componentProps?: Recordable<any>;
|
componentProps?: Recordable<any>;
|
||||||
componentSlots?:
|
componentSlots?:
|
||||||
| (() => any)
|
(() => any) | Recordable<unknown> | VNode | VNodeArrayChildren;
|
||||||
| Recordable<unknown>
|
|
||||||
| VNode
|
|
||||||
| VNodeArrayChildren;
|
|
||||||
defaultValue?: T;
|
defaultValue?: T;
|
||||||
modelPropName?: string;
|
modelPropName?: string;
|
||||||
} & Omit<AlertProps, 'beforeClose'>;
|
} & Omit<AlertProps, 'beforeClose'>;
|
||||||
|
|||||||
@@ -341,12 +341,7 @@ interface RouteMeta {
|
|||||||
* Badge color
|
* Badge color
|
||||||
*/
|
*/
|
||||||
badgeVariants?:
|
badgeVariants?:
|
||||||
| 'default'
|
'default' | 'destructive' | 'primary' | 'success' | 'warning' | string;
|
||||||
| 'destructive'
|
|
||||||
| 'primary'
|
|
||||||
| 'success'
|
|
||||||
| 'warning'
|
|
||||||
| string;
|
|
||||||
/**
|
/**
|
||||||
* The children of the current route are not displayed in the menu
|
* The children of the current route are not displayed in the menu
|
||||||
* @default false
|
* @default false
|
||||||
|
|||||||
@@ -122,12 +122,12 @@ const dashboardMenus = [
|
|||||||
title: 'page.dashboard.title',
|
title: 'page.dashboard.title',
|
||||||
},
|
},
|
||||||
name: 'Dashboard',
|
name: 'Dashboard',
|
||||||
path: '/',
|
path: '/dashboard',
|
||||||
redirect: '/analytics',
|
redirect: '/dashboard/analytics',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
name: 'Analytics',
|
name: 'Analytics',
|
||||||
path: '/analytics',
|
path: 'analytics',
|
||||||
// Here is the path of the page, need to remove 'views/' and '.vue'
|
// Here is the path of the page, need to remove 'views/' and '.vue'
|
||||||
component: '/dashboard/analytics/index',
|
component: '/dashboard/analytics/index',
|
||||||
meta: {
|
meta: {
|
||||||
@@ -137,7 +137,7 @@ const dashboardMenus = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Workspace',
|
name: 'Workspace',
|
||||||
path: '/workspace',
|
path: 'workspace',
|
||||||
component: '/dashboard/workspace/index',
|
component: '/dashboard/workspace/index',
|
||||||
meta: {
|
meta: {
|
||||||
title: 'page.dashboard.workspace',
|
title: 'page.dashboard.workspace',
|
||||||
|
|||||||
@@ -60,25 +60,23 @@ To add new translation texts, simply find `src/locales/langs/` in the correspond
|
|||||||
|
|
||||||
**src/locales/langs/zh-CN/\*.json**
|
**src/locales/langs/zh-CN/\*.json**
|
||||||
|
|
||||||
````ts
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"about": {
|
"about": {
|
||||||
"desc": "Vben Admin 是一个现代的管理模版。"
|
"desc": "Vben Admin 是一个现代的管理模版。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
````
|
```
|
||||||
|
|
||||||
**src/locales/langs/en-US.ts**
|
**src/locales/langs/en-US/\*.json**
|
||||||
|
|
||||||
````ts
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"about": {
|
"about": {
|
||||||
"desc": "Vben Admin is a modern management template."
|
"desc": "Vben Admin is a modern management template."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
````
|
```
|
||||||
|
|
||||||
## Using Translation Texts
|
## Using Translation Texts
|
||||||
|
|
||||||
@@ -91,11 +89,11 @@ With `@vben/locales`, you can easily use translation texts:
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
const items = computed(() => [{ title: $t('about.desc') }]);
|
const items = computed(() => [{ title: $t('demos.title') }]);
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div>{{ $t('about.desc') }}</div>
|
<div>{{ $t('demos.title') }}</div>
|
||||||
<template v-for="item in items.value">
|
<template v-for="item in items">
|
||||||
<div>{{ item.title }}</div>
|
<div>{{ item.title }}</div>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
@@ -105,8 +103,8 @@ const items = computed(() => [{ title: $t('about.desc') }]);
|
|||||||
|
|
||||||
If you need to add a new language pack, follow these steps:
|
If you need to add a new language pack, follow these steps:
|
||||||
|
|
||||||
- Add the corresponding language pack file in the `packages/locales/langs` directory, for example, `zh-TW.json`, and translate the respective texts.
|
- Create the corresponding language pack folder and files under `packages/locales/langs` (e.g., `zh-TW/*.json`) and translate the texts accordingly.
|
||||||
- In the corresponding application, locate the `src/locales/langs` file and add the new language pack `zh-TW.json`.
|
- In the corresponding application, find the `src/locales/langs` directory and create the same language pack folder and files (e.g., `zh-TW/*.json`).
|
||||||
- Add the corresponding language in `packages/constants/src/core.ts`:
|
- Add the corresponding language in `packages/constants/src/core.ts`:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
|
|||||||
@@ -334,12 +334,7 @@ interface RouteMeta {
|
|||||||
* 徽标颜色
|
* 徽标颜色
|
||||||
*/
|
*/
|
||||||
badgeVariants?:
|
badgeVariants?:
|
||||||
| 'default'
|
'default' | 'destructive' | 'primary' | 'success' | 'warning' | string;
|
||||||
| 'destructive'
|
|
||||||
| 'primary'
|
|
||||||
| 'success'
|
|
||||||
| 'warning'
|
|
||||||
| string;
|
|
||||||
/**
|
/**
|
||||||
* 路由的完整路径作为key(默认true)
|
* 路由的完整路径作为key(默认true)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -120,12 +120,12 @@ const dashboardMenus = [
|
|||||||
title: 'page.dashboard.title',
|
title: 'page.dashboard.title',
|
||||||
},
|
},
|
||||||
name: 'Dashboard',
|
name: 'Dashboard',
|
||||||
path: '/',
|
path: '/dashboard',
|
||||||
redirect: '/analytics',
|
redirect: '/dashboard/analytics',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
name: 'Analytics',
|
name: 'Analytics',
|
||||||
path: '/analytics',
|
path: 'analytics',
|
||||||
// 这里为页面的路径,需要去掉 views/ 和 .vue
|
// 这里为页面的路径,需要去掉 views/ 和 .vue
|
||||||
component: '/dashboard/analytics/index',
|
component: '/dashboard/analytics/index',
|
||||||
meta: {
|
meta: {
|
||||||
@@ -135,7 +135,7 @@ const dashboardMenus = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Workspace',
|
name: 'Workspace',
|
||||||
path: '/workspace',
|
path: 'workspace',
|
||||||
component: '/dashboard/workspace/index',
|
component: '/dashboard/workspace/index',
|
||||||
meta: {
|
meta: {
|
||||||
title: 'page.dashboard.workspace',
|
title: 'page.dashboard.workspace',
|
||||||
|
|||||||
@@ -60,25 +60,23 @@ updateLocale('en-US');
|
|||||||
|
|
||||||
**src/locales/langs/zh-CN/\*.json**
|
**src/locales/langs/zh-CN/\*.json**
|
||||||
|
|
||||||
````ts
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"about": {
|
"about": {
|
||||||
"desc": "Vben Admin 是一个现代的管理模版。"
|
"desc": "Vben Admin 是一个现代的管理模版。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
````
|
```
|
||||||
|
|
||||||
**src/locales/langs/en-US.ts**
|
**src/locales/langs/en-US/\*.json**
|
||||||
|
|
||||||
````ts
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"about": {
|
"about": {
|
||||||
"desc": "Vben Admin is a modern management template."
|
"desc": "Vben Admin is a modern management template."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
````
|
```
|
||||||
|
|
||||||
## 使用翻译文本
|
## 使用翻译文本
|
||||||
|
|
||||||
@@ -91,11 +89,11 @@ updateLocale('en-US');
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
const items = computed(() => [{ title: $t('about.desc') }]);
|
const items = computed(() => [{ title: $t('demos.title') }]);
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div>{{ $t('about.desc') }}</div>
|
<div>{{ $t('demos.title') }}</div>
|
||||||
<template v-for="item in items.value">
|
<template v-for="item in items">
|
||||||
<div>{{ item.title }}</div>
|
<div>{{ item.title }}</div>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
@@ -105,8 +103,8 @@ const items = computed(() => [{ title: $t('about.desc') }]);
|
|||||||
|
|
||||||
如果你需要新增语言包,需要按照以下步骤进行:
|
如果你需要新增语言包,需要按照以下步骤进行:
|
||||||
|
|
||||||
- 在 `packages/locales/langs` 目录下新增对应的语言包文件,例:`zh-TW.json`,并翻译对应的文本。
|
- 在 `packages/locales/langs` 目录下新增对应的语言包文件夹和文件,例:`zh-TW/*.json`,并翻译对应的文本。
|
||||||
- 在对应的应用内,找到 `src/locales/langs` 文件,新增对应的语言包 `zh-TW.json`
|
- 在对应的应用内,找到 `src/locales/langs` 目录,新增对应的语言包文件夹和文件 `zh-TW/*.json`。
|
||||||
- 在 `packages/constants/src/core.ts`内,新增对应的语言:
|
- 在 `packages/constants/src/core.ts`内,新增对应的语言:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export default {
|
|||||||
'nesting-selector-no-missing-scoping-root': null,
|
'nesting-selector-no-missing-scoping-root': null,
|
||||||
'no-descending-specificity': null,
|
'no-descending-specificity': null,
|
||||||
'no-empty-source': null,
|
'no-empty-source': null,
|
||||||
|
'no-invalid-position-declaration': null,
|
||||||
'order/order': [
|
'order/order': [
|
||||||
[
|
[
|
||||||
'dollar-variables',
|
'dollar-variables',
|
||||||
|
|||||||
@@ -70,6 +70,7 @@
|
|||||||
"@changesets/cli": "catalog:",
|
"@changesets/cli": "catalog:",
|
||||||
"@tsdown/css": "catalog:",
|
"@tsdown/css": "catalog:",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
|
"@types/sortablejs": "catalog:",
|
||||||
"@vben/commitlint-config": "workspace:*",
|
"@vben/commitlint-config": "workspace:*",
|
||||||
"@vben/eslint-config": "workspace:*",
|
"@vben/eslint-config": "workspace:*",
|
||||||
"@vben/oxfmt-config": "workspace:*",
|
"@vben/oxfmt-config": "workspace:*",
|
||||||
@@ -103,8 +104,11 @@
|
|||||||
"vue-tsc": "catalog:"
|
"vue-tsc": "catalog:"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^22.18.0 || ^24.0.0",
|
"node": "^22.18.0 || ^24.12.0",
|
||||||
"pnpm": ">=11.0.0"
|
"pnpm": ">=11.0.0"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@11.7.0"
|
"packageManager": "pnpm@11.15.1",
|
||||||
|
"dependencies": {
|
||||||
|
"sortablejs": "catalog:"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
packages/@core/base/typings/src/app.d.ts
vendored
10
packages/@core/base/typings/src/app.d.ts
vendored
@@ -10,16 +10,18 @@ type LayoutType =
|
|||||||
type ThemeModeType = 'auto' | 'dark' | 'light';
|
type ThemeModeType = 'auto' | 'dark' | 'light';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按钮位置
|
* 偏好设置按钮位置
|
||||||
* user-dropdown 用户的下拉弹出框中
|
* auto 自动(按布局上下文在 header/fixed 间切换)
|
||||||
* fixed 固定在右侧
|
* fixed 固定在屏幕右边缘
|
||||||
* header 顶栏
|
* header 顶栏
|
||||||
* auto 自动
|
* user-dropdown 用户的下拉弹出框中
|
||||||
|
* none 不显示
|
||||||
*/
|
*/
|
||||||
type PreferencesButtonPositionType =
|
type PreferencesButtonPositionType =
|
||||||
| 'auto'
|
| 'auto'
|
||||||
| 'fixed'
|
| 'fixed'
|
||||||
| 'header'
|
| 'header'
|
||||||
|
| 'none'
|
||||||
| 'user-dropdown';
|
| 'user-dropdown';
|
||||||
|
|
||||||
type BuiltinThemeType =
|
type BuiltinThemeType =
|
||||||
|
|||||||
@@ -136,14 +136,23 @@ exports[`defaultPreferences immutability test > should not modify the config obj
|
|||||||
},
|
},
|
||||||
"widget": {
|
"widget": {
|
||||||
"fullscreen": true,
|
"fullscreen": true,
|
||||||
|
"fullscreenButtonPosition": "header",
|
||||||
"globalSearch": true,
|
"globalSearch": true,
|
||||||
|
"globalSearchButtonPosition": "header",
|
||||||
"languageToggle": true,
|
"languageToggle": true,
|
||||||
|
"languageToggleButtonPosition": "header",
|
||||||
"lockScreen": true,
|
"lockScreen": true,
|
||||||
|
"lockScreenButtonPosition": "header",
|
||||||
|
"logoutButtonPosition": "header",
|
||||||
"notification": true,
|
"notification": true,
|
||||||
|
"notificationButtonPosition": "header",
|
||||||
"refresh": true,
|
"refresh": true,
|
||||||
|
"refreshButtonPosition": "header",
|
||||||
"sidebarToggle": true,
|
"sidebarToggle": true,
|
||||||
"themeToggle": true,
|
"themeToggle": true,
|
||||||
|
"themeToggleButtonPosition": "header",
|
||||||
"timezone": true,
|
"timezone": true,
|
||||||
|
"timezoneButtonPosition": "header",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -154,6 +154,17 @@ const defaultPreferences: Preferences = {
|
|||||||
themeToggleButtonPosition: 'header',
|
themeToggleButtonPosition: 'header',
|
||||||
timezone: true,
|
timezone: true,
|
||||||
timezoneButtonPosition: 'header',
|
timezoneButtonPosition: 'header',
|
||||||
|
order: [
|
||||||
|
'globalSearch',
|
||||||
|
'preferences',
|
||||||
|
'themeToggle',
|
||||||
|
'languageToggle',
|
||||||
|
'timezone',
|
||||||
|
'fullscreen',
|
||||||
|
'notification',
|
||||||
|
'lockScreenBtn',
|
||||||
|
'logoutBtn',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ import type {
|
|||||||
import { markRaw, reactive, readonly, watch } from 'vue';
|
import { markRaw, reactive, readonly, watch } from 'vue';
|
||||||
|
|
||||||
import { StorageManager } from '@vben-core/shared/cache';
|
import { StorageManager } from '@vben-core/shared/cache';
|
||||||
import { isMacOs, merge } from '@vben-core/shared/utils';
|
import {
|
||||||
|
isMacOs,
|
||||||
|
merge,
|
||||||
|
mergeWithArrayOverride,
|
||||||
|
} from '@vben-core/shared/utils';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
breakpointsTailwind,
|
breakpointsTailwind,
|
||||||
@@ -133,7 +137,11 @@ class PreferenceManager {
|
|||||||
|
|
||||||
// 加载缓存的偏好设置,并仅用缓存补齐初始化配置中未显式设置的字段
|
// 加载缓存的偏好设置,并仅用缓存补齐初始化配置中未显式设置的字段
|
||||||
const cachedPreferences = (await this.loadFromCache()) || {};
|
const cachedPreferences = (await this.loadFromCache()) || {};
|
||||||
const mergedPreference = merge(
|
// 修复历史脏数据:早期版本用 defu(merge) 合并数组,导致 widget.order
|
||||||
|
// 在每次 updatePreferences 时被 concat 追加,膨胀到几十/上百条重复 key。
|
||||||
|
// 这里在加载缓存后做一次去重,保留首次出现的 key。
|
||||||
|
this.sanitizeCachedArray(cachedPreferences, 'widget', 'order');
|
||||||
|
const mergedPreference = mergeWithArrayOverride(
|
||||||
{},
|
{},
|
||||||
cachedPreferences, // 用户缓存的设置优先
|
cachedPreferences, // 用户缓存的设置优先
|
||||||
this.initialPreferences, // 初始设置仅补齐缺失字段
|
this.initialPreferences, // 初始设置仅补齐缺失字段
|
||||||
@@ -144,7 +152,7 @@ class PreferenceManager {
|
|||||||
|
|
||||||
const cachedCustom = (await this.loadCustomFromCache()) || {};
|
const cachedCustom = (await this.loadCustomFromCache()) || {};
|
||||||
this.replaceCustomPreferences(
|
this.replaceCustomPreferences(
|
||||||
merge(
|
mergeWithArrayOverride(
|
||||||
{},
|
{},
|
||||||
this.sanitizeCustomPreferences(cachedCustom),
|
this.sanitizeCustomPreferences(cachedCustom),
|
||||||
this.initialCustomPreferences,
|
this.initialCustomPreferences,
|
||||||
@@ -194,7 +202,7 @@ class PreferenceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.replaceCustomPreferences(
|
this.replaceCustomPreferences(
|
||||||
merge({}, sanitizedUpdates, markRaw(this.customState)),
|
mergeWithArrayOverride({}, sanitizedUpdates, markRaw(this.customState)),
|
||||||
);
|
);
|
||||||
this.debouncedSave();
|
this.debouncedSave();
|
||||||
};
|
};
|
||||||
@@ -205,7 +213,14 @@ class PreferenceManager {
|
|||||||
*/
|
*/
|
||||||
updatePreferences = (updates: DeepPartial<Preferences>) => {
|
updatePreferences = (updates: DeepPartial<Preferences>) => {
|
||||||
// 深度合并更新内容和当前状态
|
// 深度合并更新内容和当前状态
|
||||||
const mergedState = merge({}, updates, markRaw(this.state));
|
// 注意:必须用 mergeWithArrayOverride 而不是 merge。
|
||||||
|
// defu(merge) 对数组的语义是 concat 追加,会导致 widget.order 等
|
||||||
|
// 数组字段在每次 updatePreferences 时被重复追加,指数级膨胀。
|
||||||
|
const mergedState = mergeWithArrayOverride(
|
||||||
|
{},
|
||||||
|
updates,
|
||||||
|
markRaw(this.state),
|
||||||
|
);
|
||||||
Object.assign(this.state, mergedState);
|
Object.assign(this.state, mergedState);
|
||||||
|
|
||||||
// 根据更新的值执行更新
|
// 根据更新的值执行更新
|
||||||
@@ -361,6 +376,28 @@ class PreferenceManager {
|
|||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理缓存中膨胀的数组字段(保留首次出现的元素)
|
||||||
|
* 用于修复早期版本 defu 合并数组时 concat 追加导致的脏数据
|
||||||
|
*/
|
||||||
|
private sanitizeCachedArray(
|
||||||
|
cached: Record<string, any>,
|
||||||
|
group: string,
|
||||||
|
field: string,
|
||||||
|
) {
|
||||||
|
const node = cached?.[group]?.[field];
|
||||||
|
if (!Array.isArray(node) || node.length <= 1) return;
|
||||||
|
const seen = new Set<unknown>();
|
||||||
|
const deduped = node.filter((item) => {
|
||||||
|
if (seen.has(item)) return false;
|
||||||
|
seen.add(item);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
if (deduped.length !== node.length) {
|
||||||
|
cached[group][field] = deduped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private sanitizeCustomPreferences(
|
private sanitizeCustomPreferences(
|
||||||
updates: DeepPartial<CustomPreferencesRecord>,
|
updates: DeepPartial<CustomPreferencesRecord>,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -385,6 +385,8 @@ interface WidgetPreferences {
|
|||||||
notification: boolean;
|
notification: boolean;
|
||||||
/** 通知按钮位置 */
|
/** 通知按钮位置 */
|
||||||
notificationButtonPosition: 'header' | 'none' | 'user-dropdown';
|
notificationButtonPosition: 'header' | 'none' | 'user-dropdown';
|
||||||
|
/** 小部件排序 */
|
||||||
|
order: readonly string[];
|
||||||
/** 显示刷新按钮 */
|
/** 显示刷新按钮 */
|
||||||
refresh: boolean;
|
refresh: boolean;
|
||||||
/** 刷新按钮位置 */
|
/** 刷新按钮位置 */
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
// oxlint-disable unicorn/no-nested-ternary
|
||||||
import type { FormCommonConfig, FormSchema } from '../types';
|
import type { FormCommonConfig, FormSchema } from '../types';
|
||||||
|
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import VbenButton from './button.vue';
|
|||||||
interface Props extends VbenButtonProps {
|
interface Props extends VbenButtonProps {
|
||||||
class?: any;
|
class?: any;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: (() => void)[] | (() => void);
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
tooltipDelayDuration?: number;
|
tooltipDelayDuration?: number;
|
||||||
tooltipSide?: 'bottom' | 'left' | 'right' | 'top';
|
tooltipSide?: 'bottom' | 'left' | 'right' | 'top';
|
||||||
@@ -30,6 +30,16 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
const slots = useSlots();
|
const slots = useSlots();
|
||||||
|
|
||||||
const showTooltip = computed(() => !!slots.tooltip || !!props.tooltip);
|
const showTooltip = computed(() => !!slots.tooltip || !!props.tooltip);
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
if (Array.isArray(props.onClick)) {
|
||||||
|
for (const fn of props.onClick) {
|
||||||
|
fn?.();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
props.onClick?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -39,7 +49,7 @@ const showTooltip = computed(() => !!slots.tooltip || !!props.tooltip);
|
|||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:variant="variant"
|
:variant="variant"
|
||||||
size="icon"
|
size="icon"
|
||||||
@click="onClick"
|
@click="handleClick"
|
||||||
>
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</VbenButton>
|
</VbenButton>
|
||||||
@@ -55,7 +65,7 @@ const showTooltip = computed(() => !!slots.tooltip || !!props.tooltip);
|
|||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:variant="variant"
|
:variant="variant"
|
||||||
size="icon"
|
size="icon"
|
||||||
@click="onClick"
|
@click="handleClick"
|
||||||
>
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</VbenButton>
|
</VbenButton>
|
||||||
|
|||||||
@@ -7,6 +7,15 @@ import { VbenIconButton } from '../button';
|
|||||||
|
|
||||||
defineOptions({ name: 'FullScreen' });
|
defineOptions({ name: 'FullScreen' });
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
tooltip?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
tooltip: '',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const { isFullscreen, toggle } = useFullscreen();
|
const { isFullscreen, toggle } = useFullscreen();
|
||||||
|
|
||||||
// 重新检查全屏状态
|
// 重新检查全屏状态
|
||||||
@@ -22,6 +31,7 @@ isFullscreen.value = !!(
|
|||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<VbenIconButton
|
<VbenIconButton
|
||||||
|
:tooltip="tooltip || undefined"
|
||||||
class="hover:animate-[shrink_0.3s_ease-in-out]"
|
class="hover:animate-[shrink_0.3s_ease-in-out]"
|
||||||
@click="toggle"
|
@click="toggle"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PopoverRoot v-bind="forwarded">
|
<PopoverRoot v-bind="forwarded">
|
||||||
<PopoverTrigger :class="triggerClass">
|
<PopoverTrigger as="span" :class="triggerClass">
|
||||||
<slot name="trigger"></slot>
|
<slot name="trigger"></slot>
|
||||||
|
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ import { preferences, usePreferences } from '@vben/preferences';
|
|||||||
import { useAccessStore } from '@vben/stores';
|
import { useAccessStore } from '@vben/stores';
|
||||||
|
|
||||||
import { useVbenModal } from '@vben-core/popup-ui';
|
import { useVbenModal } from '@vben-core/popup-ui';
|
||||||
import { VbenFullScreen, VbenIconButton } from '@vben-core/shadcn-ui';
|
import {
|
||||||
|
VbenFullScreen,
|
||||||
|
VbenIconButton,
|
||||||
|
VbenTooltip,
|
||||||
|
} from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
import { useMagicKeys, whenever } from '@vueuse/core';
|
import { useMagicKeys, whenever } from '@vueuse/core';
|
||||||
|
|
||||||
@@ -16,6 +20,7 @@ import {
|
|||||||
GlobalSearch,
|
GlobalSearch,
|
||||||
LanguageToggle,
|
LanguageToggle,
|
||||||
LockScreenModal,
|
LockScreenModal,
|
||||||
|
Notification,
|
||||||
PreferencesButton,
|
PreferencesButton,
|
||||||
ThemeToggle,
|
ThemeToggle,
|
||||||
TimezoneButton,
|
TimezoneButton,
|
||||||
@@ -121,94 +126,77 @@ type SlotItem = { index: number; name: string };
|
|||||||
|
|
||||||
const rightSlots = computed(() => {
|
const rightSlots = computed(() => {
|
||||||
const list: Array<SlotItem> = [];
|
const list: Array<SlotItem> = [];
|
||||||
// 全局搜索
|
|
||||||
if (
|
// 按 widget.order 顺序遍历,检查每个部件是否应在 header 中显示
|
||||||
preferences.widget.globalSearch &&
|
const widgetChecks: Record<string, { slotName: string; visible: boolean }> = {
|
||||||
preferences.widget.globalSearchButtonPosition === 'header'
|
globalSearch: {
|
||||||
) {
|
visible:
|
||||||
list.push({
|
preferences.widget.globalSearch &&
|
||||||
index: REFERENCE_VALUE,
|
preferences.widget.globalSearchButtonPosition === 'header',
|
||||||
name: 'global-search',
|
slotName: 'global-search',
|
||||||
});
|
},
|
||||||
}
|
preferences: {
|
||||||
// 偏好设置快捷功能
|
visible: preferencesButtonPosition.value.header,
|
||||||
if (preferencesButtonPosition.value.header) {
|
slotName: 'preferences',
|
||||||
list.push({
|
},
|
||||||
index: REFERENCE_VALUE + 10,
|
themeToggle: {
|
||||||
name: 'preferences',
|
visible:
|
||||||
});
|
preferences.widget.themeToggle &&
|
||||||
}
|
preferences.widget.themeToggleButtonPosition === 'header',
|
||||||
// 主题、语言、时区等子功能由各自的 widget 开关独立控制,
|
slotName: 'theme-toggle',
|
||||||
// 不应跟随偏好设置按钮的显示与否(如 enablePreferences 为 false 或按钮位于其他位置时)
|
},
|
||||||
if (
|
languageToggle: {
|
||||||
preferences.widget.themeToggle &&
|
visible:
|
||||||
preferences.widget.themeToggleButtonPosition === 'header'
|
preferences.widget.languageToggle &&
|
||||||
) {
|
preferences.widget.languageToggleButtonPosition === 'header',
|
||||||
list.push({
|
slotName: 'language-toggle',
|
||||||
index: REFERENCE_VALUE + 20,
|
},
|
||||||
name: 'theme-toggle',
|
timezone: {
|
||||||
});
|
visible:
|
||||||
}
|
preferences.widget.timezone &&
|
||||||
if (
|
preferences.widget.timezoneButtonPosition === 'header',
|
||||||
preferences.widget.languageToggle &&
|
slotName: 'timezone',
|
||||||
preferences.widget.languageToggleButtonPosition === 'header'
|
},
|
||||||
) {
|
fullscreen: {
|
||||||
list.push({
|
visible:
|
||||||
index: REFERENCE_VALUE + 30,
|
preferences.widget.fullscreen &&
|
||||||
name: 'language-toggle',
|
preferences.widget.fullscreenButtonPosition === 'header',
|
||||||
});
|
slotName: 'fullscreen',
|
||||||
}
|
},
|
||||||
if (
|
notification: {
|
||||||
preferences.widget.timezone &&
|
visible:
|
||||||
preferences.widget.timezoneButtonPosition === 'header'
|
preferences.widget.notification &&
|
||||||
) {
|
preferences.widget.notificationButtonPosition === 'header',
|
||||||
list.push({
|
slotName: 'notification',
|
||||||
index: REFERENCE_VALUE + 40,
|
},
|
||||||
name: 'timezone',
|
lockScreenBtn: {
|
||||||
});
|
visible:
|
||||||
}
|
preferences.widget.lockScreen &&
|
||||||
// 全屏
|
preferences.widget.lockScreenButtonPosition === 'header',
|
||||||
if (
|
slotName: 'lock-screen-btn',
|
||||||
preferences.widget.fullscreen &&
|
},
|
||||||
preferences.widget.fullscreenButtonPosition === 'header'
|
logoutBtn: {
|
||||||
) {
|
visible: preferences.widget.logoutButtonPosition === 'header',
|
||||||
list.push({
|
slotName: 'logout-btn',
|
||||||
index: REFERENCE_VALUE + 50,
|
},
|
||||||
name: 'fullscreen',
|
refresh: {
|
||||||
});
|
visible:
|
||||||
}
|
preferences.widget.refresh &&
|
||||||
// 消息通知
|
preferences.widget.refreshButtonPosition === 'header',
|
||||||
if (
|
slotName: 'refresh',
|
||||||
preferences.widget.notification &&
|
},
|
||||||
preferences.widget.notificationButtonPosition === 'header'
|
};
|
||||||
) {
|
|
||||||
list.push({
|
for (const key of preferences.widget.order) {
|
||||||
index: REFERENCE_VALUE + 60,
|
const check = widgetChecks[key];
|
||||||
name: 'notification',
|
if (check?.visible) {
|
||||||
});
|
list.push({ index: REFERENCE_VALUE + list.length, name: check.slotName });
|
||||||
}
|
}
|
||||||
// 锁定屏幕(仅在 header 位置显示)
|
|
||||||
if (
|
|
||||||
preferences.widget.lockScreen &&
|
|
||||||
preferences.widget.lockScreenButtonPosition === 'header'
|
|
||||||
) {
|
|
||||||
list.push({
|
|
||||||
index: REFERENCE_VALUE + 70,
|
|
||||||
name: 'lock-screen-btn',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// 退出登录(仅在 header 位置显示)
|
|
||||||
if (preferences.widget.logoutButtonPosition === 'header') {
|
|
||||||
list.push({
|
|
||||||
index: REFERENCE_VALUE + 80,
|
|
||||||
name: 'logout-btn',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 用户插槽(header-right-N)追加在后面
|
||||||
Object.keys(slots).forEach((key) => {
|
Object.keys(slots).forEach((key) => {
|
||||||
// 适配插槽名称,例如第一个插槽名:header-right-1
|
|
||||||
if (key.startsWith('header-right')) {
|
if (key.startsWith('header-right')) {
|
||||||
// 取第三个占位的数字,若是第三个占位不是数字,则自动分配排序索引
|
|
||||||
const slotIndex = Number(key.split('-')[2]);
|
const slotIndex = Number(key.split('-')[2]);
|
||||||
const index = Number.isNaN(slotIndex) ? nextIndex(list) : slotIndex;
|
const index = Number.isNaN(slotIndex) ? nextIndex(list) : slotIndex;
|
||||||
list.push({ index, name: key });
|
list.push({ index, name: key });
|
||||||
@@ -223,16 +211,6 @@ const rightSlots = computed(() => {
|
|||||||
|
|
||||||
const leftSlots = computed(() => {
|
const leftSlots = computed(() => {
|
||||||
const list: Array<SlotItem> = [];
|
const list: Array<SlotItem> = [];
|
||||||
// 刷新
|
|
||||||
if (
|
|
||||||
preferences.widget.refresh &&
|
|
||||||
preferences.widget.refreshButtonPosition === 'header'
|
|
||||||
) {
|
|
||||||
list.push({
|
|
||||||
index: 0,
|
|
||||||
name: 'refresh',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.keys(slots).forEach((key) => {
|
Object.keys(slots).forEach((key) => {
|
||||||
// 适配插槽名称,例如第一个插槽名:header-left-1
|
// 适配插槽名称,例如第一个插槽名:header-left-1
|
||||||
@@ -322,19 +300,37 @@ function clearPreferencesAndLogout() {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="slot.name === 'preferences'">
|
<template v-else-if="slot.name === 'preferences'">
|
||||||
<PreferencesButton
|
<VbenTooltip side="bottom">
|
||||||
class="mr-1"
|
<template #trigger>
|
||||||
@clear-preferences-and-logout="clearPreferencesAndLogout"
|
<PreferencesButton
|
||||||
/>
|
class="mr-1"
|
||||||
|
@clear-preferences-and-logout="clearPreferencesAndLogout"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
{{ $t('preferences.title') }}
|
||||||
|
</VbenTooltip>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="slot.name === 'theme-toggle'">
|
<template v-else-if="slot.name === 'theme-toggle'">
|
||||||
<ThemeToggle class="mt-0.5 mr-1" />
|
<VbenTooltip side="bottom">
|
||||||
|
<template #trigger>
|
||||||
|
<ThemeToggle class="mt-0.5 mr-1" />
|
||||||
|
</template>
|
||||||
|
{{ $t('preferences.theme.title') }}
|
||||||
|
</VbenTooltip>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="slot.name === 'language-toggle'">
|
<template v-else-if="slot.name === 'language-toggle'">
|
||||||
<LanguageToggle class="mr-1" />
|
<VbenTooltip side="bottom">
|
||||||
|
<template #trigger>
|
||||||
|
<LanguageToggle class="mr-1" />
|
||||||
|
</template>
|
||||||
|
{{ $t('preferences.widget.languageToggle') }}
|
||||||
|
</VbenTooltip>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="slot.name === 'fullscreen'">
|
<template v-else-if="slot.name === 'fullscreen'">
|
||||||
<VbenFullScreen class="mr-1" />
|
<VbenFullScreen
|
||||||
|
class="mr-1"
|
||||||
|
:tooltip="$t('preferences.widget.fullscreen')"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="slot.name === 'timezone'">
|
<template v-else-if="slot.name === 'timezone'">
|
||||||
<TimezoneButton class="mt-0.5 mr-1" />
|
<TimezoneButton class="mt-0.5 mr-1" />
|
||||||
@@ -357,6 +353,23 @@ function clearPreferencesAndLogout() {
|
|||||||
<LogOut class="size-4" />
|
<LogOut class="size-4" />
|
||||||
</VbenIconButton>
|
</VbenIconButton>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="slot.name === 'notification'">
|
||||||
|
<VbenTooltip side="bottom">
|
||||||
|
<template #trigger>
|
||||||
|
<Notification class="mr-1" />
|
||||||
|
</template>
|
||||||
|
{{ $t('preferences.widget.notification') }}
|
||||||
|
</VbenTooltip>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="slot.name === 'refresh'">
|
||||||
|
<VbenIconButton
|
||||||
|
class="my-0 mr-1 rounded-md"
|
||||||
|
:tooltip="$t('preferences.widget.refresh')"
|
||||||
|
@click="refresh"
|
||||||
|
>
|
||||||
|
<RotateCw class="size-4" />
|
||||||
|
</VbenIconButton>
|
||||||
|
</template>
|
||||||
</slot>
|
</slot>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -369,9 +369,6 @@ const headerSlots = computed(() => {
|
|||||||
<template #notification>
|
<template #notification>
|
||||||
<slot name="notification"></slot>
|
<slot name="notification"></slot>
|
||||||
</template>
|
</template>
|
||||||
<template #timezone>
|
|
||||||
<slot name="timezone"></slot>
|
|
||||||
</template>
|
|
||||||
<template v-for="item in headerSlots" #[item]>
|
<template v-for="item in headerSlots" #[item]>
|
||||||
<slot :name="item"></slot>
|
<slot :name="item"></slot>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -24,10 +24,16 @@ defineOptions({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{ enableShortcutKey?: boolean; menus?: MenuRecordRaw[] }>(),
|
defineProps<{
|
||||||
|
enableShortcutKey?: boolean;
|
||||||
|
menus?: MenuRecordRaw[];
|
||||||
|
/** 是否显示触发按钮(嵌在 dropdown 等容器里时可关掉) */
|
||||||
|
showButton?: boolean;
|
||||||
|
}>(),
|
||||||
{
|
{
|
||||||
enableShortcutKey: true,
|
enableShortcutKey: true,
|
||||||
menus: () => [],
|
menus: () => [],
|
||||||
|
showButton: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -94,6 +100,13 @@ onMounted(() => {
|
|||||||
window.removeEventListener('keydown', preventDefaultBrowserSearchHotKey);
|
window.removeEventListener('keydown', preventDefaultBrowserSearchHotKey);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 暴露给父组件,允许从外部触发搜索弹框(如 user-dropdown 里点击菜单项触发)
|
||||||
|
defineExpose({
|
||||||
|
open: () => modalApi.open(),
|
||||||
|
close: () => modalApi.close(),
|
||||||
|
toggle: toggleOpen,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -135,6 +148,7 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
<div
|
<div
|
||||||
|
v-if="showButton"
|
||||||
class="group flex h-8 cursor-pointer items-center gap-3 rounded-2xl border-none bg-none px-2 py-0.5 outline-hidden md:bg-accent"
|
class="group flex h-8 cursor-pointer items-center gap-3 rounded-2xl border-none bg-none px-2 py-0.5 outline-hidden md:bg-accent"
|
||||||
@click="toggleOpen()"
|
@click="toggleOpen()"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectOption } from '@vben/types';
|
||||||
|
|
||||||
|
import { computed, nextTick, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { GripVertical } from '@vben/icons';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
import Sortable from 'sortablejs';
|
||||||
|
|
||||||
|
interface Item {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
position: 'auto' | 'fixed' | 'header' | 'none' | 'user-dropdown';
|
||||||
|
/** 每项可选的 position 选项;不传则用 props.positionItems */
|
||||||
|
positionItems?: SelectOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
items: Item[];
|
||||||
|
positionItems: SelectOption[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
updateOrder: [keys: string[]];
|
||||||
|
updatePosition: [
|
||||||
|
key: string,
|
||||||
|
position: 'auto' | 'fixed' | 'header' | 'none' | 'user-dropdown',
|
||||||
|
];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const listRef = ref<HTMLElement>();
|
||||||
|
let sortableInstance: null | Sortable = null;
|
||||||
|
|
||||||
|
// 划分规则:
|
||||||
|
// - hidden:position === 'none'(不显示)
|
||||||
|
// - sortable:其它(含 auto/fixed,偏好按钮的智能模式视觉归入顶栏组参与排序,
|
||||||
|
// 避免 preferences 从 widget.order 丢失)
|
||||||
|
const sortableList = computed(() =>
|
||||||
|
props.items.filter((item) => item.position !== 'none'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const hiddenList = computed(() =>
|
||||||
|
props.items.filter((item) => item.position === 'none'),
|
||||||
|
);
|
||||||
|
function initSortable() {
|
||||||
|
if (!listRef.value) return;
|
||||||
|
sortableInstance?.destroy();
|
||||||
|
sortableInstance = Sortable.create(listRef.value, {
|
||||||
|
animation: 200,
|
||||||
|
handle: '.drag-handle',
|
||||||
|
onEnd() {
|
||||||
|
// Sortable 已经改了 DOM,但 sortableList computed 还是旧顺序。
|
||||||
|
// 直接从 DOM 读 children 的 data-key 拿新顺序,再追加 hidden 部分。
|
||||||
|
const newOrder = [...listRef.value!.children]
|
||||||
|
.map((el) => (el as HTMLElement).dataset.key)
|
||||||
|
.filter(Boolean) as string[];
|
||||||
|
emit('updateOrder', [...newOrder, ...hiddenList.value.map((i) => i.key)]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(initSortable);
|
||||||
|
|
||||||
|
function setPosition(key: string, event: Event) {
|
||||||
|
const value = (event.target as HTMLSelectElement).value as
|
||||||
|
| 'auto'
|
||||||
|
| 'fixed'
|
||||||
|
| 'header'
|
||||||
|
| 'none'
|
||||||
|
| 'user-dropdown';
|
||||||
|
emit('updatePosition', key, value);
|
||||||
|
nextTick(() => {
|
||||||
|
emit(
|
||||||
|
'updateOrder',
|
||||||
|
[...sortableList.value, ...hiddenList.value].map((i) => i.key),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div ref="listRef" class="space-y-1">
|
||||||
|
<div
|
||||||
|
v-for="item in sortableList"
|
||||||
|
:key="item.key"
|
||||||
|
:data-key="item.key"
|
||||||
|
class="bg-accent flex items-center gap-2 rounded-md px-2 py-1.5"
|
||||||
|
>
|
||||||
|
<GripVertical
|
||||||
|
class="drag-handle size-4 shrink-0 cursor-grab text-muted-foreground active:cursor-grabbing"
|
||||||
|
/>
|
||||||
|
<span class="min-w-0 flex-1 truncate text-sm">{{ item.label }}</span>
|
||||||
|
<select
|
||||||
|
:value="item.position"
|
||||||
|
class="bg-background h-7 w-28 shrink-0 rounded border px-1 text-xs"
|
||||||
|
@change="(e) => setPosition(item.key, e)"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="opt in item.positionItems ?? positionItems"
|
||||||
|
:key="opt.value"
|
||||||
|
:value="opt.value"
|
||||||
|
>
|
||||||
|
{{ opt.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="hiddenList.length > 0" class="pt-2">
|
||||||
|
<div class="text-muted-foreground mb-1 text-xs font-medium">
|
||||||
|
{{ $t('preferences.widget.hidden') }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="item in hiddenList"
|
||||||
|
:key="item.key"
|
||||||
|
class="flex items-center gap-2 rounded-md px-2 py-1"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-muted-foreground min-w-0 flex-1 truncate text-sm line-through decoration-dotted"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</span>
|
||||||
|
<select
|
||||||
|
:value="item.position"
|
||||||
|
class="bg-background h-7 w-28 shrink-0 rounded border px-1 text-xs"
|
||||||
|
@change="(e) => setPosition(item.key, e)"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="opt in item.positionItems ?? positionItems"
|
||||||
|
:key="opt.value"
|
||||||
|
:value="opt.value"
|
||||||
|
>
|
||||||
|
{{ opt.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { SelectOption } from '@vben/types';
|
import type { PreferencesButtonPositionType, SelectOption } from '@vben/types';
|
||||||
|
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
import SelectItem from '../select-item.vue';
|
import DraggableList from '../draggable-list.vue';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'PreferenceInterfaceControl',
|
name: 'PreferenceInterfaceControl',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const widgetOrder = defineModel<string[]>('widgetOrder', { required: true });
|
||||||
|
|
||||||
const widgetGlobalSearchButtonPosition = defineModel<string>(
|
const widgetGlobalSearchButtonPosition = defineModel<string>(
|
||||||
'widgetGlobalSearchButtonPosition',
|
'widgetGlobalSearchButtonPosition',
|
||||||
);
|
);
|
||||||
@@ -32,17 +34,37 @@ const widgetLockScreenButtonPosition = defineModel<string>(
|
|||||||
const widgetLogoutButtonPosition = defineModel<string>(
|
const widgetLogoutButtonPosition = defineModel<string>(
|
||||||
'widgetLogoutButtonPosition',
|
'widgetLogoutButtonPosition',
|
||||||
);
|
);
|
||||||
const appPreferencesButtonPosition = defineModel<string>(
|
|
||||||
'appPreferencesButtonPosition',
|
|
||||||
);
|
|
||||||
const widgetRefreshButtonPosition = defineModel<string>(
|
const widgetRefreshButtonPosition = defineModel<string>(
|
||||||
'widgetRefreshButtonPosition',
|
'widgetRefreshButtonPosition',
|
||||||
);
|
);
|
||||||
const widgetTimezoneButtonPosition = defineModel<string>(
|
const widgetTimezoneButtonPosition = defineModel<string>(
|
||||||
'widgetTimezoneButtonPosition',
|
'widgetTimezoneButtonPosition',
|
||||||
);
|
);
|
||||||
|
const appPreferencesButtonPosition = defineModel<PreferencesButtonPositionType>(
|
||||||
|
'appPreferencesButtonPosition',
|
||||||
|
);
|
||||||
|
|
||||||
const positionItems = computed((): SelectOption[] => [
|
const buttonPositionItems = computed((): SelectOption[] => [
|
||||||
|
{
|
||||||
|
label: $t('preferences.widget.header'),
|
||||||
|
value: 'header',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('preferences.widget.userDropdown'),
|
||||||
|
value: 'user-dropdown',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.notShow'),
|
||||||
|
value: 'none',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* preferences 按钮独享的位置选项:
|
||||||
|
* 保留 auto/fixed 让 use-preferences.ts 的智能 fallback 生效
|
||||||
|
* (移动端/全屏内容模式下自动切换 header/fixed,避免偏好入口死锁)
|
||||||
|
*/
|
||||||
|
const preferencesPositionItems = computed((): SelectOption[] => [
|
||||||
{
|
{
|
||||||
label: $t('preferences.position.auto'),
|
label: $t('preferences.position.auto'),
|
||||||
value: 'auto',
|
value: 'auto',
|
||||||
@@ -59,77 +81,99 @@ const positionItems = computed((): SelectOption[] => [
|
|||||||
label: $t('preferences.position.userDropdown'),
|
label: $t('preferences.position.userDropdown'),
|
||||||
value: 'user-dropdown',
|
value: 'user-dropdown',
|
||||||
},
|
},
|
||||||
]);
|
|
||||||
|
|
||||||
const buttonPositionItems = computed((): SelectOption[] => [
|
|
||||||
{
|
|
||||||
label: $t('preferences.widget.header'),
|
|
||||||
value: 'header',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: $t('preferences.widget.userDropdown'),
|
|
||||||
value: 'user-dropdown',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: $t('common.notShow'),
|
label: $t('common.notShow'),
|
||||||
value: 'none',
|
value: 'none',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const positionMap: Record<string, string> = {
|
||||||
|
globalSearch: 'widgetGlobalSearchButtonPosition',
|
||||||
|
preferences: 'appPreferencesButtonPosition',
|
||||||
|
themeToggle: 'widgetThemeToggleButtonPosition',
|
||||||
|
languageToggle: 'widgetLanguageToggleButtonPosition',
|
||||||
|
timezone: 'widgetTimezoneButtonPosition',
|
||||||
|
fullscreen: 'widgetFullscreenButtonPosition',
|
||||||
|
notification: 'widgetNotificationButtonPosition',
|
||||||
|
lockScreenBtn: 'widgetLockScreenButtonPosition',
|
||||||
|
logoutBtn: 'widgetLogoutButtonPosition',
|
||||||
|
refresh: 'widgetRefreshButtonPosition',
|
||||||
|
};
|
||||||
|
|
||||||
|
const labelMap: Record<string, string> = {
|
||||||
|
globalSearch: 'preferences.widget.globalSearch',
|
||||||
|
preferences: 'preferences.title',
|
||||||
|
themeToggle: 'preferences.widget.themeToggle',
|
||||||
|
languageToggle: 'preferences.widget.languageToggle',
|
||||||
|
timezone: 'preferences.widget.timezone',
|
||||||
|
fullscreen: 'preferences.widget.fullscreen',
|
||||||
|
notification: 'preferences.widget.notification',
|
||||||
|
lockScreenBtn: 'ui.widgets.lockScreen.title',
|
||||||
|
logoutBtn: 'common.logout',
|
||||||
|
refresh: 'preferences.widget.refresh',
|
||||||
|
};
|
||||||
|
|
||||||
|
const draggableItems = computed(() =>
|
||||||
|
(widgetOrder.value ?? []).map((key) => ({
|
||||||
|
key,
|
||||||
|
label: $t(labelMap[key] ?? key),
|
||||||
|
position: getPosition(key),
|
||||||
|
positionItems:
|
||||||
|
key === 'preferences' ? preferencesPositionItems.value : undefined,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
function getPosition(
|
||||||
|
key: string,
|
||||||
|
): 'auto' | 'fixed' | 'header' | 'none' | 'user-dropdown' {
|
||||||
|
const modelName = positionMap[key];
|
||||||
|
if (!modelName) return 'none';
|
||||||
|
const modelMap: Record<string, any> = {
|
||||||
|
widgetGlobalSearchButtonPosition,
|
||||||
|
widgetFullscreenButtonPosition,
|
||||||
|
widgetLanguageToggleButtonPosition,
|
||||||
|
widgetNotificationButtonPosition,
|
||||||
|
widgetThemeToggleButtonPosition,
|
||||||
|
widgetLockScreenButtonPosition,
|
||||||
|
widgetLogoutButtonPosition,
|
||||||
|
widgetRefreshButtonPosition,
|
||||||
|
widgetTimezoneButtonPosition,
|
||||||
|
appPreferencesButtonPosition,
|
||||||
|
};
|
||||||
|
return modelMap[modelName]?.value ?? 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUpdateOrder(keys: string[]) {
|
||||||
|
widgetOrder.value = keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUpdatePosition(
|
||||||
|
key: string,
|
||||||
|
position: 'auto' | 'fixed' | 'header' | 'none' | 'user-dropdown',
|
||||||
|
) {
|
||||||
|
const modelName = positionMap[key];
|
||||||
|
if (!modelName) return;
|
||||||
|
const modelMap: Record<string, any> = {
|
||||||
|
widgetGlobalSearchButtonPosition,
|
||||||
|
widgetFullscreenButtonPosition,
|
||||||
|
widgetLanguageToggleButtonPosition,
|
||||||
|
widgetNotificationButtonPosition,
|
||||||
|
widgetThemeToggleButtonPosition,
|
||||||
|
widgetLockScreenButtonPosition,
|
||||||
|
widgetLogoutButtonPosition,
|
||||||
|
widgetRefreshButtonPosition,
|
||||||
|
widgetTimezoneButtonPosition,
|
||||||
|
appPreferencesButtonPosition,
|
||||||
|
};
|
||||||
|
modelMap[modelName].value = position;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SelectItem
|
<DraggableList
|
||||||
v-model="widgetGlobalSearchButtonPosition"
|
:items="draggableItems"
|
||||||
:items="buttonPositionItems"
|
:position-items="buttonPositionItems"
|
||||||
>
|
@update-order="handleUpdateOrder"
|
||||||
{{ $t('preferences.widget.globalSearchPosition') }}
|
@update-position="handleUpdatePosition"
|
||||||
</SelectItem>
|
/>
|
||||||
<SelectItem
|
|
||||||
v-model="widgetThemeToggleButtonPosition"
|
|
||||||
:items="buttonPositionItems"
|
|
||||||
>
|
|
||||||
{{ $t('preferences.widget.themeTogglePosition') }}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem
|
|
||||||
v-model="widgetLanguageToggleButtonPosition"
|
|
||||||
:items="buttonPositionItems"
|
|
||||||
>
|
|
||||||
{{ $t('preferences.widget.languageTogglePosition') }}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem
|
|
||||||
v-model="widgetFullscreenButtonPosition"
|
|
||||||
:items="buttonPositionItems"
|
|
||||||
>
|
|
||||||
{{ $t('preferences.widget.fullscreenPosition') }}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem
|
|
||||||
v-model="widgetNotificationButtonPosition"
|
|
||||||
:items="buttonPositionItems"
|
|
||||||
>
|
|
||||||
{{ $t('preferences.widget.notificationPosition') }}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem
|
|
||||||
v-model="widgetLockScreenButtonPosition"
|
|
||||||
:items="buttonPositionItems"
|
|
||||||
>
|
|
||||||
{{ $t('preferences.widget.lockScreenPosition') }}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem v-model="widgetLogoutButtonPosition" :items="buttonPositionItems">
|
|
||||||
{{ $t('preferences.widget.logoutButtonPosition') }}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem
|
|
||||||
v-model="widgetRefreshButtonPosition"
|
|
||||||
:items="buttonPositionItems"
|
|
||||||
>
|
|
||||||
{{ $t('preferences.widget.refreshPosition') }}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem
|
|
||||||
v-model="widgetTimezoneButtonPosition"
|
|
||||||
:items="buttonPositionItems"
|
|
||||||
>
|
|
||||||
{{ $t('preferences.widget.timezonePosition') }}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem v-model="appPreferencesButtonPosition" :items="positionItems">
|
|
||||||
{{ $t('preferences.position.title') }}
|
|
||||||
</SelectItem>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ const widgetLockScreenButtonPosition = defineModel<string>(
|
|||||||
const widgetLogoutButtonPosition = defineModel<string>(
|
const widgetLogoutButtonPosition = defineModel<string>(
|
||||||
'widgetLogoutButtonPosition',
|
'widgetLogoutButtonPosition',
|
||||||
);
|
);
|
||||||
|
const widgetOrder = defineModel<string[]>('widgetOrder', { required: true });
|
||||||
const widgetRefreshButtonPosition = defineModel<string>(
|
const widgetRefreshButtonPosition = defineModel<string>(
|
||||||
'widgetRefreshButtonPosition',
|
'widgetRefreshButtonPosition',
|
||||||
);
|
);
|
||||||
@@ -516,6 +517,7 @@ function handleCustomPreferencesUpdate(updates: CustomPreferencesRecord) {
|
|||||||
v-model:widget-logout-button-position="
|
v-model:widget-logout-button-position="
|
||||||
widgetLogoutButtonPosition
|
widgetLogoutButtonPosition
|
||||||
"
|
"
|
||||||
|
v-model:widget-order="widgetOrder"
|
||||||
v-model:widget-notification-button-position="
|
v-model:widget-notification-button-position="
|
||||||
widgetNotificationButtonPosition
|
widgetNotificationButtonPosition
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -3,23 +3,24 @@ import type { Component } from 'vue';
|
|||||||
|
|
||||||
import type { AnyFunction } from '@vben/types';
|
import type { AnyFunction } from '@vben/types';
|
||||||
|
|
||||||
import { computed, useTemplateRef, watch } from 'vue';
|
import { computed, ref, useTemplateRef, watch } from 'vue';
|
||||||
|
|
||||||
import { useHoverToggle } from '@vben/hooks';
|
import { SUPPORT_LANGUAGES } from '@vben/constants';
|
||||||
|
import { useHoverToggle, useRefresh } from '@vben/hooks';
|
||||||
import {
|
import {
|
||||||
Bell,
|
|
||||||
Fullscreen,
|
|
||||||
Languages,
|
Languages,
|
||||||
LockKeyhole,
|
LockKeyhole,
|
||||||
LogOut,
|
LogOut,
|
||||||
RotateCw,
|
RotateCw,
|
||||||
Search,
|
Search,
|
||||||
Settings,
|
Settings,
|
||||||
Sun,
|
|
||||||
SunMoon,
|
|
||||||
} from '@vben/icons';
|
} from '@vben/icons';
|
||||||
import { $t } from '@vben/locales';
|
import { $t, loadLocaleMessages } from '@vben/locales';
|
||||||
import { preferences, usePreferences } from '@vben/preferences';
|
import {
|
||||||
|
preferences,
|
||||||
|
updatePreferences,
|
||||||
|
usePreferences,
|
||||||
|
} from '@vben/preferences';
|
||||||
import { useAccessStore } from '@vben/stores';
|
import { useAccessStore } from '@vben/stores';
|
||||||
import { isWindowsOs } from '@vben/utils';
|
import { isWindowsOs } from '@vben/utils';
|
||||||
|
|
||||||
@@ -34,13 +35,19 @@ import {
|
|||||||
DropdownMenuShortcut,
|
DropdownMenuShortcut,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
VbenAvatar,
|
VbenAvatar,
|
||||||
|
VbenFullScreen,
|
||||||
VbenIcon,
|
VbenIcon,
|
||||||
|
VbenIconButton,
|
||||||
} from '@vben-core/shadcn-ui';
|
} from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
import { useMagicKeys, whenever } from '@vueuse/core';
|
import { useMagicKeys, whenever } from '@vueuse/core';
|
||||||
|
|
||||||
|
import { GlobalSearch } from '../global-search';
|
||||||
import { LockScreenModal } from '../lock-screen';
|
import { LockScreenModal } from '../lock-screen';
|
||||||
|
import { Notification } from '../notification';
|
||||||
import { Preferences } from '../preferences';
|
import { Preferences } from '../preferences';
|
||||||
|
import { ThemeToggle } from '../theme-toggle';
|
||||||
|
import { TimezoneButton } from '../timezone';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/**
|
/**
|
||||||
@@ -91,10 +98,12 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
const emit = defineEmits<{ clearPreferencesAndLogout: []; logout: [] }>();
|
const emit = defineEmits<{ clearPreferencesAndLogout: []; logout: [] }>();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
globalLockScreenShortcutKey,
|
|
||||||
globalLogoutShortcutKey,
|
globalLogoutShortcutKey,
|
||||||
|
globalLockScreenShortcutKey,
|
||||||
|
globalSearchShortcutKey,
|
||||||
preferencesButtonPosition,
|
preferencesButtonPosition,
|
||||||
} = usePreferences();
|
} = usePreferences();
|
||||||
|
const { refresh } = useRefresh();
|
||||||
const accessStore = useAccessStore();
|
const accessStore = useAccessStore();
|
||||||
const [LockModal, lockModalApi] = useVbenModal({
|
const [LockModal, lockModalApi] = useVbenModal({
|
||||||
connectedComponent: LockScreenModal,
|
connectedComponent: LockScreenModal,
|
||||||
@@ -108,6 +117,7 @@ const [LogoutModal, logoutModalApi] = useVbenModal({
|
|||||||
const refTrigger = useTemplateRef('refTrigger');
|
const refTrigger = useTemplateRef('refTrigger');
|
||||||
const refContent = useTemplateRef('refContent');
|
const refContent = useTemplateRef('refContent');
|
||||||
const refPreferences = useTemplateRef('refPreferences');
|
const refPreferences = useTemplateRef('refPreferences');
|
||||||
|
const refGlobalSearch = useTemplateRef('refGlobalSearch');
|
||||||
const [openPopover, hoverWatcher] = useHoverToggle(
|
const [openPopover, hoverWatcher] = useHoverToggle(
|
||||||
[refTrigger, refContent],
|
[refTrigger, refContent],
|
||||||
() => props.hoverDelay,
|
() => props.hoverDelay,
|
||||||
@@ -233,6 +243,32 @@ function handleOpenSettings() {
|
|||||||
refPreferences.value?.open();
|
refPreferences.value?.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 刷新当前页面
|
||||||
|
function handleRefresh() {
|
||||||
|
openPopover.value = false;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索 - 不关闭 dropdown,直接触发 GlobalSearch 组件的弹框
|
||||||
|
function handleGlobalSearch() {
|
||||||
|
refGlobalSearch.value?.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 语言切换 - 阻止 Radix 默认关闭外层 dropdown,就地展开/收起 locale 列表
|
||||||
|
const showLanguageList = ref(false);
|
||||||
|
function handleLanguageToggleSelect(event?: Event) {
|
||||||
|
event?.preventDefault();
|
||||||
|
showLanguageList.value = !showLanguageList.value;
|
||||||
|
}
|
||||||
|
async function handleLocaleChange(event: Event, value: 'en-US' | 'zh-CN') {
|
||||||
|
// 阻止默认关闭,让用户能继续看到选择结果;选完手动收起
|
||||||
|
event.preventDefault();
|
||||||
|
updatePreferences({ app: { locale: value } });
|
||||||
|
await loadLocaleMessages(value);
|
||||||
|
showLanguageList.value = false;
|
||||||
|
openPopover.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (enableShortcutKey.value) {
|
if (enableShortcutKey.value) {
|
||||||
const keys = useMagicKeys();
|
const keys = useMagicKeys();
|
||||||
const logoutKey = keys['Alt+KeyQ'];
|
const logoutKey = keys['Alt+KeyQ'];
|
||||||
@@ -285,6 +321,14 @@ if (enableShortcutKey.value) {
|
|||||||
@clear-preferences-and-logout="emit('clearPreferencesAndLogout')"
|
@clear-preferences-and-logout="emit('clearPreferencesAndLogout')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<GlobalSearch
|
||||||
|
v-if="showGlobalSearchInDropdown"
|
||||||
|
ref="refGlobalSearch"
|
||||||
|
:enable-shortcut-key="globalSearchShortcutKey"
|
||||||
|
:menus="accessStore.accessMenus"
|
||||||
|
:show-button="false"
|
||||||
|
/>
|
||||||
|
|
||||||
<DropdownMenu v-model:open="openPopover" :modal="false">
|
<DropdownMenu v-model:open="openPopover" :modal="false">
|
||||||
<DropdownMenuTrigger ref="refTrigger" :disabled="props.trigger === 'hover'">
|
<DropdownMenuTrigger ref="refTrigger" :disabled="props.trigger === 'hover'">
|
||||||
<div class="mr-2 ml-1 cursor-pointer rounded-full p-1.5 hover:bg-accent">
|
<div class="mr-2 ml-1 cursor-pointer rounded-full p-1.5 hover:bg-accent">
|
||||||
@@ -331,7 +375,9 @@ if (enableShortcutKey.value) {
|
|||||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||||
@click="menu.handler"
|
@click="menu.handler"
|
||||||
>
|
>
|
||||||
<VbenIcon :icon="menu.icon" class="mr-2 size-4" />
|
<VbenIconButton class="mr-2" @click="menu.handler">
|
||||||
|
<VbenIcon :icon="menu.icon" class="size-4" />
|
||||||
|
</VbenIconButton>
|
||||||
{{ menu.text }}
|
{{ menu.text }}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<template v-if="showLockInDropdown || showLogoutInDropdown">
|
<template v-if="showLockInDropdown || showLogoutInDropdown">
|
||||||
@@ -341,7 +387,9 @@ if (enableShortcutKey.value) {
|
|||||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||||
@click="handleOpenLock"
|
@click="handleOpenLock"
|
||||||
>
|
>
|
||||||
<LockKeyhole class="mr-2 size-4" />
|
<VbenIconButton class="mr-2" @click="handleOpenLock">
|
||||||
|
<LockKeyhole class="size-4" />
|
||||||
|
</VbenIconButton>
|
||||||
{{ $t('ui.widgets.lockScreen.title') }}
|
{{ $t('ui.widgets.lockScreen.title') }}
|
||||||
<DropdownMenuShortcut v-if="enableLockScreenShortcutKey">
|
<DropdownMenuShortcut v-if="enableLockScreenShortcutKey">
|
||||||
{{ altView }} L
|
{{ altView }} L
|
||||||
@@ -353,7 +401,9 @@ if (enableShortcutKey.value) {
|
|||||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||||
@click="handleLogout"
|
@click="handleLogout"
|
||||||
>
|
>
|
||||||
<LogOut class="mr-2 size-4" />
|
<VbenIconButton class="mr-2" @click="handleLogout">
|
||||||
|
<LogOut class="size-4" />
|
||||||
|
</VbenIconButton>
|
||||||
{{ $t('common.logout') }}
|
{{ $t('common.logout') }}
|
||||||
<DropdownMenuShortcut v-if="enableLogoutShortcutKey">
|
<DropdownMenuShortcut v-if="enableLogoutShortcutKey">
|
||||||
{{ altView }} Q
|
{{ altView }} Q
|
||||||
@@ -375,50 +425,75 @@ if (enableShortcutKey.value) {
|
|||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
v-if="showGlobalSearchInDropdown"
|
v-if="showGlobalSearchInDropdown"
|
||||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||||
|
@select="handleGlobalSearch"
|
||||||
>
|
>
|
||||||
<Search class="mr-2 size-4" />
|
<VbenIconButton class="mr-2" @click="handleGlobalSearch">
|
||||||
|
<Search class="size-4" />
|
||||||
|
</VbenIconButton>
|
||||||
{{ $t('preferences.widget.globalSearch') }}
|
{{ $t('preferences.widget.globalSearch') }}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
v-if="showThemeToggleInDropdown"
|
v-if="showThemeToggleInDropdown"
|
||||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||||
>
|
>
|
||||||
<Sun class="mr-2 size-4" />
|
<ThemeToggle class="mr-2" />
|
||||||
{{ $t('preferences.widget.themeToggle') }}
|
{{ $t('preferences.theme.title') }}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
v-if="showLanguageToggleInDropdown"
|
v-if="showLanguageToggleInDropdown"
|
||||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||||
|
@select="handleLanguageToggleSelect"
|
||||||
>
|
>
|
||||||
<Languages class="mr-2 size-4" />
|
<VbenIconButton class="mr-2" @click="handleLanguageToggleSelect">
|
||||||
|
<Languages class="size-4" />
|
||||||
|
</VbenIconButton>
|
||||||
{{ $t('preferences.widget.languageToggle') }}
|
{{ $t('preferences.widget.languageToggle') }}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<template v-if="showLanguageList">
|
||||||
|
<DropdownMenuItem
|
||||||
|
v-for="lang in SUPPORT_LANGUAGES"
|
||||||
|
:key="lang.value"
|
||||||
|
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 pl-8 leading-8"
|
||||||
|
@select="(e: Event) => handleLocaleChange(e, lang.value)"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:class="
|
||||||
|
lang.value === preferences.app.locale ? 'bg-foreground' : ''
|
||||||
|
"
|
||||||
|
class="mr-2 size-1.5 rounded-full"
|
||||||
|
></span>
|
||||||
|
{{ lang.label }}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</template>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
v-if="showTimezoneInDropdown"
|
v-if="showTimezoneInDropdown"
|
||||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||||
>
|
>
|
||||||
<SunMoon class="mr-2 size-4" />
|
<TimezoneButton class="mr-2" />
|
||||||
{{ $t('preferences.widget.timezone') }}
|
{{ $t('ui.widgets.timezone.setTimezone') }}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
v-if="showFullscreenInDropdown"
|
v-if="showFullscreenInDropdown"
|
||||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||||
>
|
>
|
||||||
<Fullscreen class="mr-2 size-4" />
|
<VbenFullScreen class="mr-2" />
|
||||||
{{ $t('preferences.widget.fullscreen') }}
|
{{ $t('preferences.widget.fullscreen') }}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
v-if="showNotificationInDropdown"
|
v-if="showNotificationInDropdown"
|
||||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||||
>
|
>
|
||||||
<Bell class="mr-2 size-4" />
|
<Notification class="mr-2" />
|
||||||
{{ $t('preferences.widget.notification') }}
|
{{ $t('preferences.widget.notification') }}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
v-if="showRefreshInDropdown"
|
v-if="showRefreshInDropdown"
|
||||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||||
|
@click="handleRefresh"
|
||||||
>
|
>
|
||||||
<RotateCw class="mr-2 size-4" />
|
<VbenIconButton class="mr-2" @click="handleRefresh">
|
||||||
|
<RotateCw class="size-4" />
|
||||||
|
</VbenIconButton>
|
||||||
{{ $t('preferences.widget.refresh') }}
|
{{ $t('preferences.widget.refresh') }}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</template>
|
</template>
|
||||||
@@ -430,7 +505,9 @@ if (enableShortcutKey.value) {
|
|||||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||||
@click="handleOpenSettings"
|
@click="handleOpenSettings"
|
||||||
>
|
>
|
||||||
<Settings class="mr-2 size-4" />
|
<VbenIconButton class="mr-2" @click="handleOpenSettings">
|
||||||
|
<Settings class="size-4" />
|
||||||
|
</VbenIconButton>
|
||||||
{{ $t('preferences.title') }}
|
{{ $t('preferences.title') }}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -192,13 +192,13 @@
|
|||||||
},
|
},
|
||||||
"widget": {
|
"widget": {
|
||||||
"title": "Widget",
|
"title": "Widget",
|
||||||
"globalSearch": "Enable Global Search",
|
"globalSearch": "Global Search",
|
||||||
"globalSearchPosition": "Global Search Position",
|
"globalSearchPosition": "Global Search Position",
|
||||||
"fullscreen": "Enable Fullscreen",
|
"fullscreen": "Enable Fullscreen",
|
||||||
"fullscreenPosition": "Fullscreen Position",
|
"fullscreenPosition": "Fullscreen Position",
|
||||||
"themeToggle": "Enable Theme Toggle",
|
"themeToggle": "Enable Theme Toggle",
|
||||||
"themeTogglePosition": "Theme Toggle Position",
|
"themeTogglePosition": "Theme Toggle Position",
|
||||||
"languageToggle": "Enable Language Toggle",
|
"languageToggle": "Language Toggle",
|
||||||
"languageTogglePosition": "Language Toggle Position",
|
"languageTogglePosition": "Language Toggle Position",
|
||||||
"notification": "Enable Notification",
|
"notification": "Enable Notification",
|
||||||
"notificationPosition": "Notification Position",
|
"notificationPosition": "Notification Position",
|
||||||
@@ -208,6 +208,7 @@
|
|||||||
"logoutButtonPosition": "Logout Button Position",
|
"logoutButtonPosition": "Logout Button Position",
|
||||||
"header": "Header",
|
"header": "Header",
|
||||||
"userDropdown": "User Dropdown",
|
"userDropdown": "User Dropdown",
|
||||||
|
"hidden": "Hidden",
|
||||||
"refresh": "Enable Refresh",
|
"refresh": "Enable Refresh",
|
||||||
"refreshPosition": "Refresh Position",
|
"refreshPosition": "Refresh Position",
|
||||||
"timezone": "Enable Timezone",
|
"timezone": "Enable Timezone",
|
||||||
|
|||||||
@@ -192,13 +192,13 @@
|
|||||||
},
|
},
|
||||||
"widget": {
|
"widget": {
|
||||||
"title": "小部件",
|
"title": "小部件",
|
||||||
"globalSearch": "启用全局搜索",
|
"globalSearch": "全局搜索",
|
||||||
"globalSearchPosition": "全局搜索位置",
|
"globalSearchPosition": "全局搜索位置",
|
||||||
"fullscreen": "启用全屏",
|
"fullscreen": "启用全屏",
|
||||||
"fullscreenPosition": "全屏按钮位置",
|
"fullscreenPosition": "全屏按钮位置",
|
||||||
"themeToggle": "启用主题切换",
|
"themeToggle": "启用主题切换",
|
||||||
"themeTogglePosition": "主题切换位置",
|
"themeTogglePosition": "主题切换位置",
|
||||||
"languageToggle": "启用语言切换",
|
"languageToggle": "语言切换",
|
||||||
"languageTogglePosition": "语言切换位置",
|
"languageTogglePosition": "语言切换位置",
|
||||||
"notification": "启用通知",
|
"notification": "启用通知",
|
||||||
"notificationPosition": "通知位置",
|
"notificationPosition": "通知位置",
|
||||||
@@ -208,6 +208,7 @@
|
|||||||
"logoutButtonPosition": "退出按钮位置",
|
"logoutButtonPosition": "退出按钮位置",
|
||||||
"header": "顶栏",
|
"header": "顶栏",
|
||||||
"userDropdown": "用户下拉窗",
|
"userDropdown": "用户下拉窗",
|
||||||
|
"hidden": "已隐藏",
|
||||||
"refresh": "启用刷新",
|
"refresh": "启用刷新",
|
||||||
"refreshPosition": "刷新按钮位置",
|
"refreshPosition": "刷新按钮位置",
|
||||||
"timezone": "启用时区",
|
"timezone": "启用时区",
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ const schema: VbenFormSchema[] = [
|
|||||||
数组字段名。假设为 `contacts`,第 1 行子字段 `name` 会被转换成:
|
数组字段名。假设为 `contacts`,第 1 行子字段 `name` 会被转换成:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
contacts[0].name
|
contacts[0].name;
|
||||||
```
|
```
|
||||||
|
|
||||||
### `children`
|
### `children`
|
||||||
@@ -102,22 +102,22 @@ contacts[0].name
|
|||||||
|
|
||||||
传给数组编辑器的配置:
|
传给数组编辑器的配置:
|
||||||
|
|
||||||
| 字段 | 说明 |
|
| 字段 | 说明 |
|
||||||
| --- | --- |
|
| --------------- | ------------------------ |
|
||||||
| `addButtonText` | 新增按钮文案 |
|
| `addButtonText` | 新增按钮文案 |
|
||||||
| `actionText` | 操作列表头文案 |
|
| `actionText` | 操作列表头文案 |
|
||||||
| `emptyText` | 空数据文案 |
|
| `emptyText` | 空数据文案 |
|
||||||
| `min` | 最少行数,达到后禁用删除 |
|
| `min` | 最少行数,达到后禁用删除 |
|
||||||
| `max` | 最多行数,达到后禁用新增 |
|
| `max` | 最多行数,达到后禁用新增 |
|
||||||
| `showIndex` | 是否显示序号 |
|
| `showIndex` | 是否显示序号 |
|
||||||
| `createRow` | 新增行时生成默认数据 |
|
| `createRow` | 新增行时生成默认数据 |
|
||||||
|
|
||||||
## 校验建议
|
## 校验建议
|
||||||
|
|
||||||
数组父级 `rules` 建议只写数组级规则,例如至少一行:
|
数组父级 `rules` 建议只写数组级规则,例如至少一行:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
rules: z.array(z.any()).min(1, '请至少添加一个联系人')
|
rules: z.array(z.any()).min(1, '请至少添加一个联系人');
|
||||||
```
|
```
|
||||||
|
|
||||||
每个子字段的必填、长度、格式校验写在 children 自己的 `rules`:
|
每个子字段的必填、长度、格式校验写在 children 自己的 `rules`:
|
||||||
@@ -156,20 +156,20 @@ children 里的 `dependencies.triggerFields` 默认是“当前行相对路径
|
|||||||
在第 1 行中,`triggerFields: ['role']` 会被转换成:
|
在第 1 行中,`triggerFields: ['role']` 会被转换成:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
contacts[0].role
|
contacts[0].role;
|
||||||
```
|
```
|
||||||
|
|
||||||
回调多了一个可选 `ctx` 参数:
|
回调多了一个可选 `ctx` 参数:
|
||||||
|
|
||||||
| 字段 | 说明 |
|
| 字段 | 说明 |
|
||||||
| --- | --- |
|
| ----------------------- | ---------------------------------------- |
|
||||||
| `ctx.row` | 当前行数据 |
|
| `ctx.row` | 当前行数据 |
|
||||||
| `ctx.rowIndex` | 当前行索引 |
|
| `ctx.rowIndex` | 当前行索引 |
|
||||||
| `ctx.rowPath` | 当前行路径,例如 `contacts[0]` |
|
| `ctx.rowPath` | 当前行路径,例如 `contacts[0]` |
|
||||||
| `ctx.arrayField` | 数组字段名,例如 `contacts` |
|
| `ctx.arrayField` | 数组字段名,例如 `contacts` |
|
||||||
| `ctx.fieldName` | 当前真实字段名,例如 `contacts[0].phone` |
|
| `ctx.fieldName` | 当前真实字段名,例如 `contacts[0].phone` |
|
||||||
| `ctx.originalFieldName` | 原始 child 字段名,例如 `phone` |
|
| `ctx.originalFieldName` | 原始 child 字段名,例如 `phone` |
|
||||||
| `ctx.rootValues` | 表单完整值 |
|
| `ctx.rootValues` | 表单完整值 |
|
||||||
|
|
||||||
如果 child 需要依赖表单根字段,可以使用 `$root.` 前缀:
|
如果 child 需要依赖表单根字段,可以使用 `$root.` 前缀:
|
||||||
|
|
||||||
@@ -185,7 +185,7 @@ dependencies: {
|
|||||||
如果想显式写当前行字段,也可以使用 `$row.` 前缀:
|
如果想显式写当前行字段,也可以使用 `$row.` 前缀:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
triggerFields: ['$row.role']
|
triggerFields: ['$row.role'];
|
||||||
```
|
```
|
||||||
|
|
||||||
## valueFormat 用法
|
## valueFormat 用法
|
||||||
@@ -210,19 +210,19 @@ children 里的 `valueFormat` 也会按行执行:
|
|||||||
在 `contacts[0]` 中,`setValue('phone', nextValue)` 会自动写到:
|
在 `contacts[0]` 中,`setValue('phone', nextValue)` 会自动写到:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
contacts[0].phone
|
contacts[0].phone;
|
||||||
```
|
```
|
||||||
|
|
||||||
如果要写根字段,用 `$root.`:
|
如果要写根字段,用 `$root.`:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
setValue('$root.firstContactPhone', value)
|
setValue('$root.firstContactPhone', value);
|
||||||
```
|
```
|
||||||
|
|
||||||
如果要显式写当前行字段,用 `$row.`:
|
如果要显式写当前行字段,用 `$row.`:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
setValue('$row.phone', value)
|
setValue('$row.phone', value);
|
||||||
```
|
```
|
||||||
|
|
||||||
## updateSchema 用法
|
## updateSchema 用法
|
||||||
|
|||||||
@@ -88,7 +88,13 @@ const schema: VbenFormSchema[] = [
|
|||||||
label: $t('system.menu.parent'),
|
label: $t('system.menu.parent'),
|
||||||
renderComponentContent() {
|
renderComponentContent() {
|
||||||
return {
|
return {
|
||||||
title({ label, meta }: { label: string; meta: Recordable<any> }) {
|
treeTitleRender({
|
||||||
|
label,
|
||||||
|
meta,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
meta: Recordable<any>;
|
||||||
|
}) {
|
||||||
const coms = [];
|
const coms = [];
|
||||||
if (!label) return '';
|
if (!label) return '';
|
||||||
if (meta?.icon) {
|
if (meta?.icon) {
|
||||||
@@ -129,12 +135,6 @@ const schema: VbenFormSchema[] = [
|
|||||||
.string()
|
.string()
|
||||||
.min(2, $t('ui.formRules.minLength', [$t('system.menu.path'), 2]))
|
.min(2, $t('ui.formRules.minLength', [$t('system.menu.path'), 2]))
|
||||||
.max(100, $t('ui.formRules.maxLength', [$t('system.menu.path'), 100]))
|
.max(100, $t('ui.formRules.maxLength', [$t('system.menu.path'), 100]))
|
||||||
.refine(
|
|
||||||
(value: string) => {
|
|
||||||
return value.startsWith('/');
|
|
||||||
},
|
|
||||||
$t('ui.formRules.startWith', [$t('system.menu.path'), '/']),
|
|
||||||
)
|
|
||||||
.refine(
|
.refine(
|
||||||
async (value: string) => {
|
async (value: string) => {
|
||||||
return !(await isMenuPathExists(value, formData.value?.id));
|
return !(await isMenuPathExists(value, formData.value?.id));
|
||||||
@@ -162,12 +162,6 @@ const schema: VbenFormSchema[] = [
|
|||||||
.string()
|
.string()
|
||||||
.min(2, $t('ui.formRules.minLength', [$t('system.menu.path'), 2]))
|
.min(2, $t('ui.formRules.minLength', [$t('system.menu.path'), 2]))
|
||||||
.max(100, $t('ui.formRules.maxLength', [$t('system.menu.path'), 100]))
|
.max(100, $t('ui.formRules.maxLength', [$t('system.menu.path'), 100]))
|
||||||
.refine(
|
|
||||||
(value: string) => {
|
|
||||||
return value.startsWith('/');
|
|
||||||
},
|
|
||||||
$t('ui.formRules.startWith', [$t('system.menu.path'), '/']),
|
|
||||||
)
|
|
||||||
.refine(async (value: string) => {
|
.refine(async (value: string) => {
|
||||||
return await isMenuPathExists(value, formData.value?.id);
|
return await isMenuPathExists(value, formData.value?.id);
|
||||||
}, $t('system.menu.activePathMustExist'))
|
}, $t('system.menu.activePathMustExist'))
|
||||||
|
|||||||
8086
pnpm-lock.yaml
generated
8086
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -37,70 +37,70 @@ overrides:
|
|||||||
vue: 'catalog:'
|
vue: 'catalog:'
|
||||||
|
|
||||||
catalog:
|
catalog:
|
||||||
'@ast-grep/napi': ^0.43.0
|
'@ast-grep/napi': ^0.44.1
|
||||||
'@changesets/changelog-github': ^0.7.0
|
'@changesets/changelog-github': ^0.7.0
|
||||||
'@changesets/cli': ^2.31.0
|
'@changesets/cli': ^2.31.1
|
||||||
'@changesets/git': ^3.0.4
|
'@changesets/git': ^3.0.4
|
||||||
'@clack/prompts': ^1.5.1
|
'@clack/prompts': ^1.7.0
|
||||||
'@commitlint/cli': ^21.0.2
|
'@commitlint/cli': ^21.2.1
|
||||||
'@commitlint/config-conventional': ^21.0.2
|
'@commitlint/config-conventional': ^21.2.0
|
||||||
'@ctrl/tinycolor': ^4.2.0
|
'@ctrl/tinycolor': ^4.2.0
|
||||||
'@eslint-community/eslint-plugin-eslint-comments': ^4.7.2
|
'@eslint-community/eslint-plugin-eslint-comments': ^4.7.2
|
||||||
'@eslint/js': ^10.0.1
|
'@eslint/js': ^10.0.1
|
||||||
'@faker-js/faker': ^10.4.0
|
'@faker-js/faker': ^10.5.0
|
||||||
'@iconify/json': ^2.2.487
|
'@iconify/json': ^2.2.501
|
||||||
'@iconify/tailwind4': ^1.2.3
|
'@iconify/tailwind4': ^1.2.3
|
||||||
'@iconify/vue': ^5.0.1
|
'@iconify/vue': ^5.0.1
|
||||||
'@intlify/core-base': ^11.4.5
|
'@intlify/core-base': ^11.4.6
|
||||||
'@intlify/unplugin-vue-i18n': ^11.2.3
|
'@intlify/unplugin-vue-i18n': ^11.2.4
|
||||||
'@jspm/generator': ^2.16.1
|
'@jspm/generator': ^2.16.3
|
||||||
'@lucide/vue': ^1.18.0
|
'@lucide/vue': ^1.25.0
|
||||||
'@manypkg/get-packages': ^3.1.0
|
'@manypkg/get-packages': ^3.1.0
|
||||||
'@nolebase/vitepress-plugin-git-changelog': ^2.18.2
|
'@nolebase/vitepress-plugin-git-changelog': ^2.18.2
|
||||||
'@playwright/test': ^1.61.0
|
'@playwright/test': ^1.61.1
|
||||||
'@pnpm/workspace.read-manifest': ^1000.3.1
|
'@pnpm/workspace.read-manifest': ^1000.3.1
|
||||||
'@stylistic/stylelint-plugin': ^5.2.0
|
'@stylistic/stylelint-plugin': ^5.2.1
|
||||||
'@tailwindcss/typography': ^0.5.20
|
'@tailwindcss/typography': ^0.5.20
|
||||||
'@tailwindcss/vite': ^4.3.1
|
'@tailwindcss/vite': ^4.3.3
|
||||||
'@tanstack/vue-query': ^5.101.0
|
'@tanstack/vue-query': ^5.101.2
|
||||||
'@tanstack/vue-store': ^0.11.0
|
'@tanstack/vue-store': ^0.11.0
|
||||||
'@tiptap/core': ^3.26.1
|
'@tiptap/core': ^3.28.0
|
||||||
'@tiptap/extension-document': ^3.26.1
|
'@tiptap/extension-document': ^3.28.0
|
||||||
'@tiptap/extension-highlight': ^3.26.1
|
'@tiptap/extension-highlight': ^3.28.0
|
||||||
'@tiptap/extension-image': ^3.26.1
|
'@tiptap/extension-image': ^3.28.0
|
||||||
'@tiptap/extension-link': ^3.26.1
|
'@tiptap/extension-link': ^3.28.0
|
||||||
'@tiptap/extension-placeholder': ^3.26.1
|
'@tiptap/extension-placeholder': ^3.28.0
|
||||||
'@tiptap/extension-text-align': ^3.26.1
|
'@tiptap/extension-text-align': ^3.28.0
|
||||||
'@tiptap/extension-text-style': ^3.26.1
|
'@tiptap/extension-text-style': ^3.28.0
|
||||||
'@tiptap/extension-underline': ^3.26.1
|
'@tiptap/extension-underline': ^3.28.0
|
||||||
'@tiptap/pm': ^3.26.1
|
'@tiptap/pm': ^3.28.0
|
||||||
'@tiptap/starter-kit': ^3.26.1
|
'@tiptap/starter-kit': ^3.28.0
|
||||||
'@tiptap/vue-3': ^3.26.1
|
'@tiptap/vue-3': ^3.28.0
|
||||||
'@tsdown/css': ^0.22.3
|
'@tsdown/css': ^0.22.9
|
||||||
'@types/archiver': ^8.0.0
|
'@types/archiver': ^8.0.0
|
||||||
'@types/html-minifier-terser': ^7.0.2
|
'@types/html-minifier-terser': ^7.0.2
|
||||||
'@types/json-bigint': ^1.0.4
|
'@types/json-bigint': ^1.0.4
|
||||||
'@types/jsonwebtoken': ^9.0.10
|
'@types/jsonwebtoken': ^9.0.10
|
||||||
'@types/lodash.clonedeep': ^4.5.9
|
'@types/lodash.clonedeep': ^4.5.9
|
||||||
'@types/node': ^25.9.4
|
'@types/node': ^25.9.5
|
||||||
'@types/nprogress': ^0.2.3
|
'@types/nprogress': ^0.2.3
|
||||||
'@types/qrcode': ^1.5.6
|
'@types/qrcode': ^1.5.6
|
||||||
'@types/qs': ^6.15.1
|
'@types/qs': ^6.15.1
|
||||||
'@types/sortablejs': ^1.15.9
|
'@types/sortablejs': ^1.15.9
|
||||||
'@typescript-eslint/parser': ^8.61.1
|
'@typescript-eslint/parser': ^8.64.0
|
||||||
'@vee-validate/zod': ^4.15.1
|
'@vee-validate/zod': ^4.15.1
|
||||||
'@vite-pwa/vitepress': ^1.1.0
|
'@vite-pwa/vitepress': ^1.1.0
|
||||||
'@vitejs/plugin-vue': ^6.0.7
|
'@vitejs/plugin-vue': ^6.0.8
|
||||||
'@vitejs/plugin-vue-jsx': ^5.1.5
|
'@vitejs/plugin-vue-jsx': ^5.1.6
|
||||||
'@vue/shared': ^3.5.38
|
'@vue/shared': ^3.5.40
|
||||||
'@vue/test-utils': ^2.4.11
|
'@vue/test-utils': ^2.4.11
|
||||||
'@vueuse/core': ^14.3.0
|
'@vueuse/core': ^14.3.0
|
||||||
'@vueuse/integrations': ^14.3.0
|
'@vueuse/integrations': ^14.3.0
|
||||||
'@vueuse/motion': ^3.0.3
|
'@vueuse/motion': ^3.0.3
|
||||||
ant-design-vue: ^4.2.6
|
ant-design-vue: ^4.2.6
|
||||||
antdv-next: ^1.3.5
|
antdv-next: ^1.4.3
|
||||||
archiver: ^8.0.0
|
archiver: ^8.0.0
|
||||||
axios: ^1.18.0
|
axios: ^1.18.1
|
||||||
axios-mock-adapter: ^2.1.0
|
axios-mock-adapter: ^2.1.0
|
||||||
cac: ^7.0.0
|
cac: ^7.0.0
|
||||||
chalk: ^5.6.2
|
chalk: ^5.6.2
|
||||||
@@ -118,50 +118,50 @@ catalog:
|
|||||||
defu: ^6.1.7
|
defu: ^6.1.7
|
||||||
dotenv: ^17.4.2
|
dotenv: ^17.4.2
|
||||||
echarts: ^6.1.0
|
echarts: ^6.1.0
|
||||||
element-plus: ^2.14.2
|
element-plus: ^2.14.3
|
||||||
es-toolkit: ^1.47.1
|
es-toolkit: ^1.49.0
|
||||||
eslint: ^10.5.0
|
eslint: ^10.7.0
|
||||||
eslint-plugin-better-tailwindcss: ^4.6.0
|
eslint-plugin-better-tailwindcss: ^4.6.1
|
||||||
eslint-plugin-command: ^3.5.2
|
eslint-plugin-command: ^3.5.3
|
||||||
eslint-plugin-jsonc: ^3.2.0
|
eslint-plugin-jsonc: ^3.3.0
|
||||||
eslint-plugin-n: ^18.1.0
|
eslint-plugin-n: ^18.2.2
|
||||||
eslint-plugin-perfectionist: ^5.9.1
|
eslint-plugin-perfectionist: ^5.10.0
|
||||||
eslint-plugin-pnpm: ^1.6.1
|
eslint-plugin-pnpm: ^1.6.1
|
||||||
eslint-plugin-unused-imports: ^4.4.1
|
eslint-plugin-unused-imports: ^4.4.1
|
||||||
eslint-plugin-vue: ^10.9.2
|
eslint-plugin-vue: ^10.9.2
|
||||||
eslint-plugin-yml: ^3.4.0
|
eslint-plugin-yml: ^3.6.0
|
||||||
execa: ^9.6.1
|
execa: ^10.0.0
|
||||||
find-up: ^8.0.0
|
find-up: ^8.0.0
|
||||||
get-port: ^7.2.0
|
get-port: ^7.2.0
|
||||||
globals: ^17.6.0
|
globals: ^17.7.0
|
||||||
h3: ^1.15.11
|
h3: ^1.15.11
|
||||||
happy-dom: ^20.10.6
|
happy-dom: ^20.11.0
|
||||||
html-minifier-terser: ^7.2.0
|
html-minifier-terser: ^7.2.0
|
||||||
is-ci: ^4.1.0
|
is-ci: ^4.1.0
|
||||||
json-bigint: ^1.0.0
|
json-bigint: ^1.0.0
|
||||||
jsonwebtoken: ^9.0.3
|
jsonwebtoken: ^9.0.3
|
||||||
knip: ^6.17.0
|
knip: ^6.27.0
|
||||||
lefthook: ^2.1.9
|
lefthook: ^2.1.10
|
||||||
lodash.clonedeep: ^4.5.0
|
lodash.clonedeep: ^4.5.0
|
||||||
medium-zoom: ^1.1.0
|
medium-zoom: ^1.1.0
|
||||||
naive-ui: ^2.44.1
|
naive-ui: ^2.44.1
|
||||||
nitropack: ^2.13.4
|
nitropack: ^2.13.4
|
||||||
nprogress: ^0.2.0
|
nprogress: ^0.2.0
|
||||||
ora: ^9.4.0
|
ora: ^9.4.1
|
||||||
oxfmt: ^0.55.0
|
oxfmt: ^0.59.0
|
||||||
oxlint: ^1.70.0
|
oxlint: ^1.74.0
|
||||||
oxlint-tsgolint: ^0.23.0
|
oxlint-tsgolint: ^0.25.0
|
||||||
pinia: ^3.0.4
|
pinia: ^4.0.2
|
||||||
pinia-plugin-persistedstate: ^4.7.1
|
pinia-plugin-persistedstate: ^4.7.1
|
||||||
pkg-types: ^2.3.1
|
pkg-types: ^2.3.1
|
||||||
playwright: ^1.61.0
|
playwright: ^1.61.1
|
||||||
postcss: ^8.5.15
|
postcss: ^8.5.20
|
||||||
postcss-html: ^1.8.1
|
postcss-html: ^1.8.1
|
||||||
postcss-scss: ^4.0.9
|
postcss-scss: ^4.0.9
|
||||||
publint: ^0.3.21
|
publint: ^0.3.21
|
||||||
qrcode: ^1.5.4
|
qrcode: ^1.5.4
|
||||||
qs: ^6.15.2
|
qs: ^6.15.3
|
||||||
reka-ui: ^2.9.10
|
reka-ui: ^2.10.1
|
||||||
resolve.exports: ^2.0.3
|
resolve.exports: ^2.0.3
|
||||||
rimraf: ^6.1.3
|
rimraf: ^6.1.3
|
||||||
rollup-plugin-visualizer: ^7.0.1
|
rollup-plugin-visualizer: ^7.0.1
|
||||||
@@ -169,7 +169,7 @@ catalog:
|
|||||||
sass-embedded: ^1.100.0
|
sass-embedded: ^1.100.0
|
||||||
secure-ls: ^2.0.0
|
secure-ls: ^2.0.0
|
||||||
sortablejs: ^1.15.7
|
sortablejs: ^1.15.7
|
||||||
stylelint: ^17.13.0
|
stylelint: ^17.14.0
|
||||||
stylelint-config-recess-order: ^7.7.0
|
stylelint-config-recess-order: ^7.7.0
|
||||||
stylelint-config-recommended: ^18.0.0
|
stylelint-config-recommended: ^18.0.0
|
||||||
stylelint-config-recommended-scss: ^17.0.1
|
stylelint-config-recommended-scss: ^17.0.1
|
||||||
@@ -178,37 +178,37 @@ catalog:
|
|||||||
stylelint-order: ^8.1.1
|
stylelint-order: ^8.1.1
|
||||||
stylelint-scss: ^7.2.0
|
stylelint-scss: ^7.2.0
|
||||||
tailwind-merge: ^3.6.0
|
tailwind-merge: ^3.6.0
|
||||||
tailwindcss: ^4.3.1
|
tailwindcss: ^4.3.3
|
||||||
tdesign-vue-next: ^1.20.1
|
tdesign-vue-next: ^1.20.3
|
||||||
theme-colors: ^0.1.0
|
theme-colors: ^0.1.0
|
||||||
tippy.js: ^6.3.7
|
tippy.js: ^6.3.7
|
||||||
tsdown: ^0.22.3
|
tsdown: ^0.22.9
|
||||||
turbo: ^2.9.18
|
turbo: ^2.10.5
|
||||||
tw-animate-css: ^1.4.0
|
tw-animate-css: ^1.4.0
|
||||||
typescript: ^6.0.3
|
typescript: ^6.0.3
|
||||||
unplugin-dts: ^1.0.2
|
unplugin-dts: ^1.0.3
|
||||||
unplugin-element-plus: ^0.11.2
|
unplugin-element-plus: ^0.11.2
|
||||||
unplugin-vue: ^7.2.0
|
unplugin-vue: ^7.2.0
|
||||||
vee-validate: ^4.15.1
|
vee-validate: ^4.15.1
|
||||||
vite: 8.0.10
|
vite: ^8.1.5
|
||||||
vite-plugin-compression: ^0.5.1
|
vite-plugin-compression: ^0.5.1
|
||||||
vite-plugin-lazy-import: ^1.0.7
|
vite-plugin-lazy-import: ^1.0.7
|
||||||
vite-plugin-pwa: ^1.3.0
|
vite-plugin-pwa: ^1.3.0
|
||||||
vite-plugin-vue-devtools: ^8.1.3
|
vite-plugin-vue-devtools: ^8.1.5
|
||||||
vitepress: ^2.0.0-alpha.17
|
vitepress: ^2.0.0-alpha.18
|
||||||
vitepress-plugin-group-icons: ^1.7.5
|
vitepress-plugin-group-icons: ^1.7.5
|
||||||
vitest: ^4.1.9
|
vitest: ^4.1.10
|
||||||
vue: ^3.5.38
|
vue: ^3.5.40
|
||||||
vue-eslint-parser: ^10.4.1
|
vue-eslint-parser: ^10.4.1
|
||||||
vue-i18n: ^11.4.5
|
vue-i18n: ^11.4.6
|
||||||
vue-json-pretty: ^2.6.0
|
vue-json-pretty: ^2.6.0
|
||||||
vue-router: ^5.1.0
|
vue-router: ^5.2.0
|
||||||
vue-tippy: ^6.7.1
|
vue-tippy: ^6.7.1
|
||||||
vue-tsc: ^3.3.5
|
vue-tsc: ^3.3.7
|
||||||
vxe-pc-ui: ^4.14.39
|
vxe-pc-ui: ^4.16.15
|
||||||
vxe-table: ^4.19.14
|
vxe-table: ^4.20.5
|
||||||
watermark-js-plus: ^1.6.3
|
watermark-js-plus: ^1.6.6
|
||||||
yaml-eslint-parser: ^2.0.0
|
yaml-eslint-parser: ^2.1.0
|
||||||
zod: ^3.25.76
|
zod: ^3.25.76
|
||||||
zod-defaults: 0.1.3
|
zod-defaults: 0.1.3
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { execaCommand, getPackages } from '@vben/node-utils';
|
import { execa, getPackages } from '@vben/node-utils';
|
||||||
|
|
||||||
import { cancel, isCancel, select } from '@clack/prompts';
|
import { cancel, isCancel, select } from '@clack/prompts';
|
||||||
|
|
||||||
@@ -46,9 +46,13 @@ export async function run(options: RunOptions) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
execaCommand(`pnpm --filter=${selectPkg} run ${command}`, {
|
try {
|
||||||
stdio: 'inherit',
|
await execa('pnpm', [`--filter=${selectPkg}`, 'run', command], {
|
||||||
});
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
process.exit(error.exitCode || 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { CAC } from 'cac';
|
|||||||
import { execSync } from 'node:child_process';
|
import { execSync } from 'node:child_process';
|
||||||
import { availableParallelism, freemem } from 'node:os';
|
import { availableParallelism, freemem } from 'node:os';
|
||||||
|
|
||||||
import { execaCommand } from '@vben/node-utils';
|
import { execa } from '@vben/node-utils';
|
||||||
|
|
||||||
interface LintCommandOptions {
|
interface LintCommandOptions {
|
||||||
/**
|
/**
|
||||||
@@ -29,16 +29,35 @@ const CPU_CORE_THRESHOLD = 4;
|
|||||||
*/
|
*/
|
||||||
const FREE_MEMORY_THRESHOLD = 8 * 1024 ** 3;
|
const FREE_MEMORY_THRESHOLD = 8 * 1024 ** 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一条命令:可执行文件名 + 参数数组。
|
||||||
|
*
|
||||||
|
* execa v10 移除了 execaCommand,execa(file, args?, options?) 的第一个参数是
|
||||||
|
* 「可执行文件名」而非整条命令行;且默认不走 shell。因此统一用 [file, args] 数组形式,
|
||||||
|
* 参数逐个传入、无需 shell 转义,跨平台行为一致(避免把整条字符串误当文件名)。
|
||||||
|
*/
|
||||||
|
type Command = [file: string, args: string[]];
|
||||||
|
|
||||||
|
/** 执行单条命令。 */
|
||||||
|
function runCommand([file, args]: Command) {
|
||||||
|
return execa(file, args, { stdio: 'inherit' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将命令还原为可读字符串,用于失败信息展示。 */
|
||||||
|
function formatCommand([file, args]: Command) {
|
||||||
|
return [file, ...args].join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 串行执行所有命令:一次只运行一个进程。
|
* 串行执行所有命令:一次只运行一个进程。
|
||||||
* 保证一次能看到所有工具的报错(配置低的机器更友好)。
|
* 保证一次能看到所有工具的报错(配置低的机器更友好)。
|
||||||
*/
|
*/
|
||||||
async function runSerial(commands: string[]) {
|
async function runSerial(commands: Command[]) {
|
||||||
const failed: string[] = [];
|
const failed: Command[] = [];
|
||||||
|
|
||||||
for (const command of commands) {
|
for (const command of commands) {
|
||||||
try {
|
try {
|
||||||
await execaCommand(command, { stdio: 'inherit' });
|
await runCommand(command);
|
||||||
} catch {
|
} catch {
|
||||||
failed.push(command);
|
failed.push(command);
|
||||||
}
|
}
|
||||||
@@ -46,7 +65,9 @@ async function runSerial(commands: string[]) {
|
|||||||
|
|
||||||
if (failed.length > 0) {
|
if (failed.length > 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Lint failed:\n${failed.map((command) => ` - ${command}`).join('\n')}`,
|
`Lint failed:\n${failed
|
||||||
|
.map((command) => ` - ${formatCommand(command)}`)
|
||||||
|
.join('\n')}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,10 +76,8 @@ async function runSerial(commands: string[]) {
|
|||||||
* 并行执行所有命令:同时启动全部进程。
|
* 并行执行所有命令:同时启动全部进程。
|
||||||
* 任一进程失败时,强制结束其余仍在运行的进程,避免产生遗漏进程。
|
* 任一进程失败时,强制结束其余仍在运行的进程,避免产生遗漏进程。
|
||||||
*/
|
*/
|
||||||
async function runParallel(commands: string[]) {
|
async function runParallel(commands: Command[]) {
|
||||||
const subprocesses = commands.map((command) =>
|
const subprocesses = commands.map((command) => runCommand(command));
|
||||||
execaCommand(command, { stdio: 'inherit' }),
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all(subprocesses);
|
await Promise.all(subprocesses);
|
||||||
@@ -89,29 +108,23 @@ async function runLint({ format, threads }: LintCommandOptions) {
|
|||||||
// 用户通过 --threads 显式指定时优先使用其值。
|
// 用户通过 --threads 显式指定时优先使用其值。
|
||||||
const defaultThreads =
|
const defaultThreads =
|
||||||
cpuCores > CPU_CORE_THRESHOLD && freemem() > FREE_MEMORY_THRESHOLD ? 4 : 2;
|
cpuCores > CPU_CORE_THRESHOLD && freemem() > FREE_MEMORY_THRESHOLD ? 4 : 2;
|
||||||
const threadsArg = ` --threads=${threads || defaultThreads}`;
|
const threadsArg = `--threads=${threads || defaultThreads}`;
|
||||||
|
|
||||||
if (format) {
|
if (format) {
|
||||||
await execaCommand(`stylelint "**/*.{vue,css,less,scss}" --cache --fix`, {
|
await runSerial([
|
||||||
stdio: 'inherit',
|
['stylelint', ['**/*.{vue,css,less,scss}', '--cache', '--fix']],
|
||||||
});
|
['oxfmt', [threadsArg]],
|
||||||
await execaCommand(`oxfmt${threadsArg}`, {
|
['oxlint', ['--fix', '--type-aware', threadsArg]],
|
||||||
stdio: 'inherit',
|
['eslint', ['.', '--cache', '--fix']],
|
||||||
});
|
]);
|
||||||
await execaCommand(`oxlint --fix --type-aware${threadsArg}`, {
|
|
||||||
stdio: 'inherit',
|
|
||||||
});
|
|
||||||
await execaCommand(`eslint . --cache --fix`, {
|
|
||||||
stdio: 'inherit',
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const commands = [
|
const commands: Command[] = [
|
||||||
`oxfmt --check${threadsArg}`,
|
['oxfmt', ['--check', threadsArg]],
|
||||||
`oxlint --type-aware${threadsArg}`,
|
['oxlint', ['--type-aware', threadsArg]],
|
||||||
`eslint . --cache`,
|
['eslint', ['.', '--cache']],
|
||||||
`stylelint "**/*.{vue,css,less,scss}" --cache`,
|
['stylelint', ['**/*.{vue,css,less,scss}', '--cache']],
|
||||||
];
|
];
|
||||||
|
|
||||||
// 低配机器(CPU 核心数较少)串行执行,避免多进程并发导致瞬时占用飙升;
|
// 低配机器(CPU 核心数较少)串行执行,避免多进程并发导致瞬时占用飙升;
|
||||||
|
|||||||
Reference in New Issue
Block a user