Browse Source

Merge branch 'vbenjs:main' into main

pull/8367/head
失忆 3 months ago
committed by GitHub
parent
commit
e2f3b8d5d8
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 3
      .changeset/.prettierrc.json
  2. 7
      .changeset/fancy-ears-walk.md
  3. 2
      .github/workflows/build.yml
  4. 2
      .github/workflows/changeset-version.yml
  5. 6
      .github/workflows/ci.yml
  6. 6
      .github/workflows/codeql.yml
  7. 10
      .github/workflows/deploy.yml
  8. 2
      .github/workflows/issue-close-require.yml
  9. 6
      .github/workflows/issue-labeled.yml
  10. 2
      .github/workflows/lock.yml
  11. 76
      .github/workflows/release-tag.yml
  12. 2
      .github/workflows/semantic-pull-request.yml
  13. 2
      .github/workflows/stale.yml
  14. 3
      .gitignore
  15. 2
      .node-version
  16. 2
      .vscode/settings.json
  17. 2
      README.ja-JP.md
  18. 2
      README.md
  19. 2
      README.zh-CN.md
  20. 8
      apps/backend-mock/api/timezone/setTimezone.ts
  21. 2
      apps/backend-mock/nitro.config.ts
  22. 10
      apps/backend-mock/tsconfig.json
  23. 1
      apps/web-antd/src/layouts/basic.vue
  24. 2
      apps/web-antd/src/preferences.ts
  25. 4
      apps/web-antd/src/views/dashboard/workspace/index.vue
  26. 652
      apps/web-antdv-next/src/adapter/component/index.ts
  27. 1
      apps/web-antdv-next/src/api/core/index.ts
  28. 25
      apps/web-antdv-next/src/api/core/upload.ts
  29. 1
      apps/web-antdv-next/src/layouts/basic.vue
  30. 6
      apps/web-antdv-next/src/preferences.ts
  31. 1
      apps/web-ele/src/layouts/basic.vue
  32. 6
      apps/web-ele/src/preferences.ts
  33. 1
      apps/web-naive/package.json
  34. 1
      apps/web-naive/src/layouts/basic.vue
  35. 1
      apps/web-naive/src/locales/langs/en-US/demos.json
  36. 1
      apps/web-naive/src/locales/langs/zh-CN/demos.json
  37. 6
      apps/web-naive/src/preferences.ts
  38. 8
      apps/web-naive/src/router/routes/modules/demos.ts
  39. 123
      apps/web-naive/src/views/demos/naive/array-form/index.vue
  40. 9
      apps/web-tdesign/src/app.vue
  41. 3
      apps/web-tdesign/src/bootstrap.ts
  42. 1
      apps/web-tdesign/src/layouts/basic.vue
  43. 6
      apps/web-tdesign/src/preferences.ts
  44. 2
      cspell.json
  45. 2
      docs/.vitepress/components/preview-group.vue
  46. 8
      docs/.vitepress/config/en.mts
  47. 8
      docs/.vitepress/config/zh.mts
  48. 2
      docs/package.json
  49. 102
      docs/src/components/common-ui/vben-descriptions.md
  50. 165
      docs/src/components/common-ui/vben-table-action.md
  51. 2
      docs/src/demos/vben-alert/prompt/index.vue
  52. 18
      docs/src/demos/vben-descriptions/basic/index.vue
  53. 22
      docs/src/demos/vben-descriptions/bordered/index.vue
  54. 17
      docs/src/demos/vben-descriptions/custom/index.vue
  55. 35
      docs/src/demos/vben-descriptions/size/index.vue
  56. 15
      docs/src/demos/vben-descriptions/span/index.vue
  57. 13
      docs/src/demos/vben-descriptions/vertical/index.vue
  58. 28
      docs/src/demos/vben-table-action/basic/index.vue
  59. 44
      docs/src/demos/vben-table-action/dropdown/index.vue
  60. 28
      docs/src/demos/vben-table-action/permission/index.vue
  61. 32
      docs/src/demos/vben-table-action/popconfirm/index.vue
  62. 17
      docs/src/demos/vben-table-action/tooltip/index.vue
  63. 102
      docs/src/en/components/common-ui/vben-descriptions.md
  64. 165
      docs/src/en/components/common-ui/vben-table-action.md
  65. 2
      docs/src/en/guide/essentials/server.md
  66. 10
      docs/src/en/guide/essentials/settings.md
  67. 2
      docs/src/en/guide/introduction/quick-start.md
  68. 2
      docs/src/en/guide/introduction/vben.md
  69. 2
      docs/src/guide/essentials/server.md
  70. 10
      docs/src/guide/essentials/settings.md
  71. 2
      docs/src/guide/introduction/quick-start.md
  72. 2
      docs/src/guide/introduction/vben.md
  73. 6
      internal/lint-configs/commitlint-config/index.d.ts
  74. 1
      internal/lint-configs/commitlint-config/package.json
  75. 2
      internal/lint-configs/eslint-config/package.json
  76. 1
      internal/lint-configs/eslint-config/src/configs/index.ts
  77. 2
      internal/lint-configs/eslint-config/src/configs/node.ts
  78. 46
      internal/lint-configs/eslint-config/src/configs/typescript.ts
  79. 44
      internal/lint-configs/eslint-config/src/configs/unicorn.ts
  80. 8
      internal/lint-configs/eslint-config/src/configs/yaml.ts
  81. 2
      internal/lint-configs/eslint-config/src/index.ts
  82. 96
      internal/lint-configs/oxfmt-config/src/index.ts
  83. 9
      internal/lint-configs/oxlint-config/src/configs/overrides.ts
  84. 19
      internal/lint-configs/oxlint-config/src/configs/plugins.ts
  85. 38
      internal/lint-configs/oxlint-config/src/configs/typescript.ts
  86. 117
      internal/lint-configs/oxlint-config/src/configs/unicorn.ts
  87. 1
      internal/vite-config/package.json
  88. 5
      internal/vite-config/src/plugins/archiver.ts
  89. 10
      internal/vite-config/src/plugins/extra-app-config.ts
  90. 14
      internal/vite-config/src/plugins/license.ts
  91. 2
      internal/vite-config/tsconfig.json
  92. 43
      lefthook.yml
  93. 11
      package.json
  94. 2
      packages/@core/base/icons/package.json
  95. 3
      packages/@core/base/icons/src/lucide.ts
  96. 3
      packages/@core/base/shared/src/cache/local-storage-driver.ts
  97. 18
      packages/@core/base/shared/src/utils/__tests__/update-css-variables.test.ts
  98. 2
      packages/@core/base/shared/src/utils/index.ts
  99. 7
      packages/@core/base/shared/src/utils/update-css-variables.ts
  100. 9
      packages/@core/base/typings/src/app.d.ts

3
.changeset/.prettierrc.json

@ -0,0 +1,3 @@
{
"singleQuote": true
}

7
.changeset/fancy-ears-walk.md

@ -0,0 +1,7 @@
---
'@vben/styles': patch
'@vben-core/form-ui': patch
'@vben/web-naive': patch
---
feat(@core/form-ui): 新增 useVbenForm 数组编辑器 VbenFormFieldArray

2
.github/workflows/build.yml

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

2
.github/workflows/changeset-version.yml

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

6
.github/workflows/ci.yml

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

6
.github/workflows/codeql.yml

@ -57,11 +57,11 @@ jobs:
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@ -89,6 +89,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4
with:
category: '/language:${{matrix.language}}'

10
.github/workflows/deploy.yml

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

2
.github/workflows/issue-close-require.yml

@ -19,7 +19,7 @@ jobs:
steps:
# 关闭未活动的 Issues
- name: Close Inactive Issues
uses: actions/stale@v9
uses: actions/stale@v10
with:
days-before-stale: -1 # Issues and PR will never be flagged stale automatically.
stale-issue-label: needs-reproduction # Label that flags an issue as stale.

6
.github/workflows/issue-labeled.yml

@ -18,7 +18,7 @@ jobs:
steps:
- name: remove enhancement pending
if: github.event.label.name == 'enhancement'
uses: actions-cool/issues-helper@v3
uses: actions-cool/issues-helper-backup@d65454423c6fbbd20026b9b499d403f79422ac69
with:
actions: 'remove-labels'
token: ${{ secrets.GITHUB_TOKEN }}
@ -27,7 +27,7 @@ jobs:
- name: remove bug pending
if: github.event.label.name == 'bug'
uses: actions-cool/issues-helper@v3
uses: actions-cool/issues-helper-backup@d65454423c6fbbd20026b9b499d403f79422ac69
with:
actions: 'remove-labels'
token: ${{ secrets.GITHUB_TOKEN }}
@ -36,7 +36,7 @@ jobs:
- name: needs reproduction
if: github.event.label.name == 'needs reproduction'
uses: actions-cool/issues-helper@v3
uses: actions-cool/issues-helper-backup@d65454423c6fbbd20026b9b499d403f79422ac69
with:
actions: 'create-comment, remove-labels'
token: ${{ secrets.GITHUB_TOKEN }}

2
.github/workflows/lock.yml

@ -14,7 +14,7 @@ jobs:
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v5
- uses: dessant/lock-threads@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
issue-inactive-days: '14'

76
.github/workflows/release-tag.yml

@ -3,60 +3,44 @@ name: Create Release Tag
on:
push:
tags:
- 'v*.*.*' # Push events to matching v*, i.e. v1.0, v20.15.10
env:
HUSKY: '0'
- 'v*.*.*'
workflow_dispatch:
inputs:
tag:
description: 'Tag to create (e.g. v1.2.3)'
required: true
type: string
permissions:
pull-requests: write
contents: write
jobs:
build:
release:
name: Create Release
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22]
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
# - name: Checkout code
# uses: actions/checkout@v6
# with:
# fetch-depth: 0
# - name: Install pnpm
# uses: pnpm/action-setup@v4
# - name: Use Node.js ${{ matrix.node-version }}
# uses: actions/setup-node@v4
# with:
# node-version: ${{ matrix.node-version }}
# cache: "pnpm"
# - name: Install dependencies
# run: pnpm install --frozen-lockfile
# - name: Test and Build
# run: |
# pnpm run test
# pnpm run build
- name: version
- name: Extract version
id: version
run: |
tag=${GITHUB_REF/refs\/tags\//}
version=${tag#v}
major=${version%%.*}
echo "tag=${tag}" >> $GITHUB_OUTPUT
echo "version=${version}" >> $GITHUB_OUTPUT
echo "major=${major}" >> $GITHUB_OUTPUT
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
raw_tag="${{ inputs.tag }}"
else
raw_tag="${GITHUB_REF_NAME}"
fi
# Normalize: ensure v prefix
tag="${raw_tag}"
[[ "${tag:0:1}" != "v" ]] && tag="v${tag}"
version="${tag#v}"
major="${version%%.*}"
echo "tag=${tag}" >> "${GITHUB_OUTPUT}"
echo "version=${version}" >> "${GITHUB_OUTPUT}"
echo "major=${major}" >> "${GITHUB_OUTPUT}"
- uses: release-drafter/release-drafter@v7
with:
@ -64,17 +48,3 @@ jobs:
publish: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# - name: force update major tag
# run: |
# git tag v${{ steps.version.outputs.major }} ${{ steps.version.outputs.tag }} -f
# git push origin refs/tags/v${{ steps.version.outputs.major }} -f
# - name: Create Release for Tag
# id: release_tag
# uses: ncipollo/release-action@v1
# with:
# token: ${{ secrets.GITHUB_TOKEN }}
# generateReleaseNotes: "true"
# body: |
# > Please refer to [CHANGELOG.md](https://github.com/vbenjs/vue-vben-admin/blob/main/CHANGELOG.md) for details.

2
.github/workflows/semantic-pull-request.yml

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Validate PR title
uses: amannn/action-semantic-pull-request@v5
uses: amannn/action-semantic-pull-request@v6
with:
wip: true
subjectPattern: ^(?![A-Z]).+$

2
.github/workflows/stale.yml

@ -9,7 +9,7 @@ jobs:
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
- uses: actions/stale@v10
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days'

3
.gitignore

@ -23,6 +23,7 @@ package-lock.json
.VSCodeCounter
**/backend-mock/data
.omx
.pnpm-store
# local env files
.env.local
.env.*.local
@ -58,4 +59,4 @@ vite.config.ts.*
.codex
skills-lock.json
.atomcode
.datalog
datalog

2
.node-version

@ -1 +1 @@
22.22.0
24.16.0

2
.vscode/settings.json

@ -38,7 +38,7 @@
// lint && format
"oxc.enable": true,
"oxc.typeAware": false,
"oxc.typeAware": true,
"oxc.configPath": "oxlint.config.ts",
"oxc.fmt.configPath": "oxfmt.config.ts",
"eslint.useFlatConfig": true,

2
README.ja-JP.md

@ -114,7 +114,7 @@ pnpm build
## ブラウザサポート
ローカル開発には `Chrome 80+` ブラウザを推奨します
Tailwind CSS v4.0 is designed for Safari 16.4+, Chrome 111+, and Firefox 128+
モダンブラウザをサポートし、IEはサポートしません

2
README.md

@ -114,7 +114,7 @@ Reference [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.
## Browser Support
The `Chrome 80+` browser is recommended for local development
Tailwind CSS v4.0 is designed for Safari 16.4+, Chrome 111+, and Firefox 128+
Support modern browsers, not IE

2
README.zh-CN.md

@ -114,7 +114,7 @@ pnpm build
## 浏览器支持
本地开发推荐使用 `Chrome 80+` 浏览器
Tailwind CSS v4.0 is designed for Safari 16.4+, Chrome 111+, and Firefox 128+
支持现代浏览器,不支持 IE

8
apps/backend-mock/api/timezone/setTimezone.ts

@ -1,7 +1,11 @@
import { eventHandler, readBody } from 'h3';
import { eventHandler, readBody, setResponseStatus } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { TIME_ZONE_OPTIONS } from '~/utils/mock-data';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
import {
unAuthorizedResponse,
useResponseError,
useResponseSuccess,
} from '~/utils/response';
import { setTimezone } from '~/utils/timezone-utils';
export default eventHandler(async (event) => {

2
apps/backend-mock/nitro.config.ts

@ -1,3 +1,5 @@
import { defineNitroConfig } from 'nitropack/config';
import errorHandler from './error';
process.env.COMPATIBILITY_DATE = new Date().toISOString();

10
apps/backend-mock/tsconfig.json

@ -1,3 +1,11 @@
{
"extends": "./.nitro/types/tsconfig.json"
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/node.json",
"compilerOptions": {
"paths": {
"~/*": ["./*"]
}
},
"include": ["**/*.ts", "**/.*.ts"],
"exclude": ["node_modules", "dist", ".nitro"]
}

1
apps/web-antd/src/layouts/basic.vue

@ -226,6 +226,7 @@ watch(
description="ann.vben@gmail.com"
tag-text="Pro"
@logout="handleLogout"
@clear-preferences-and-logout="handleLogout"
/>
</template>
<template #notification>

2
apps/web-antd/src/preferences.ts

@ -1,4 +1,5 @@
import {
appCopyrightPreferences,
defineOverridesPreferences,
definePreferencesExtension,
} from '@vben/preferences';
@ -20,6 +21,7 @@ export const overridesPreferences = defineOverridesPreferences({
app: {
name: import.meta.env.VITE_APP_TITLE,
},
copyright: appCopyrightPreferences,
});
export const preferencesExtension =

4
apps/web-antd/src/views/dashboard/workspace/index.vue

@ -244,7 +244,7 @@ function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
<template #description> 今日晴,20℃ - 32℃! </template>
</WorkbenchHeader>
<div class="mt-5 flex flex-col lg:flex-row">
<div class="flex flex-col lg:flex-row">
<div class="mr-4 w-full lg:w-3/5">
<WorkbenchProject :items="projectItems" title="项目" @click="navTo" />
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" />
@ -252,7 +252,7 @@ function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
<div class="w-full lg:w-2/5">
<WorkbenchQuickNav
:items="quickNavItems"
class="mt-5 lg:mt-0"
class="lg:mt-0"
title="快捷导航"
@click="navTo"
/>

652
apps/web-antdv-next/src/adapter/component/index.ts

@ -36,8 +36,11 @@ import type { Component, Ref } from 'vue';
import type {
ApiComponentSharedProps,
BaseFormComponentType,
CollapsibleParamsProps,
IconPickerProps,
} from '@vben/common-ui';
import type { Sortable } from '@vben/hooks';
import type { TipTapProps } from '@vben/plugins/tiptap';
import type { Recordable } from '@vben/types';
import {
@ -45,6 +48,9 @@ import {
defineAsyncComponent,
defineComponent,
h,
nextTick,
onMounted,
onUnmounted,
ref,
render,
unref,
@ -55,19 +61,25 @@ import {
ApiComponent,
globalShareState,
IconPicker,
VbenCollapsibleParams,
VCropper,
} from '@vben/common-ui';
import { useSortable } from '@vben/hooks';
import { IconifyIcon } from '@vben/icons';
import { $t } from '@vben/locales';
import { VbenTiptap } from '@vben/plugins/tiptap';
import { isEmpty } from '@vben/utils';
import { message, Modal, notification } from 'antdv-next';
import { upload_file } from '#/api';
type AdapterUploadProps = UploadProps & {
aspectRatio?: string;
crop?: boolean;
draggable?: boolean;
handleChange?: (event: UploadChangeParam) => void;
maxSize?: number;
onDragSort?: (oldIndex: number, newIndex: number) => void;
onHandleChange?: (event: UploadChangeParam) => void;
};
@ -80,8 +92,8 @@ const Button = defineAsyncComponent(
const Checkbox = defineAsyncComponent(
() => import('antdv-next/dist/checkbox/index'),
);
const CheckboxGroup = defineAsyncComponent(
() => import('antdv-next/dist/checkbox/Group'),
const CheckboxGroup = defineAsyncComponent(() =>
import('antdv-next/dist/checkbox/index').then((res) => res.CheckboxGroup),
);
const DatePicker = defineAsyncComponent(
() => import('antdv-next/dist/date-picker/index'),
@ -170,260 +182,263 @@ const withDefaultPlaceholder = (
});
};
const withPreviewUpload = () => {
// 检查是否为图片文件的辅助函数
const isImageFile = (file: UploadFile): boolean => {
const imageExtensions = new Set([
'bmp',
'gif',
'jpeg',
'jpg',
'png',
'svg',
'webp',
]);
if (file.url) {
try {
const pathname = new URL(file.url, 'http://localhost').pathname;
const ext = pathname.split('.').pop()?.toLowerCase();
return ext ? imageExtensions.has(ext) : false;
} catch {
const ext = file.url?.split('.').pop()?.toLowerCase();
return ext ? imageExtensions.has(ext) : false;
}
}
if (!file.type) {
const ext = file.name?.split('.').pop()?.toLowerCase();
return ext ? imageExtensions.has(ext) : false;
const IMAGE_EXTENSIONS = new Set([
'bmp',
'gif',
'jpeg',
'jpg',
'png',
'svg',
'webp',
]);
/**
* 检查是否为图片文件
*/
function isImageFile(file: UploadFile): boolean {
if (file.url) {
try {
const pathname = new URL(file.url, 'http://localhost').pathname;
const ext = pathname.split('.').pop()?.toLowerCase();
return ext ? IMAGE_EXTENSIONS.has(ext) : false;
} catch {
const ext = file.url?.split('.').pop()?.toLowerCase();
return ext ? IMAGE_EXTENSIONS.has(ext) : false;
}
return file.type.startsWith('image/');
}
if (!file.type) {
const ext = file.name?.split('.').pop()?.toLowerCase();
return ext ? IMAGE_EXTENSIONS.has(ext) : false;
}
return file.type.startsWith('image/');
}
/**
* 创建默认的上传按钮插槽
*/
function createDefaultUploadSlots(listType: string, placeholder: string) {
if (listType === 'picture-card') {
return { default: () => placeholder };
}
return {
default: () =>
h(
Button,
{
icon: h(IconifyIcon, {
icon: 'ant-design:upload-outlined',
class: 'mb-1 size-4',
}),
},
() => placeholder,
),
};
// 创建默认的上传按钮插槽
const createDefaultSlotsWithUpload = (
listType: string,
placeholder: string,
) => {
switch (listType) {
case 'picture-card': {
return {
default: () => placeholder,
};
}
default: {
return {
default: () =>
h(
Button,
{
icon: h(IconifyIcon, {
icon: 'ant-design:upload-outlined',
class: 'mb-1 size-4',
}),
}
/**
* 获取文件的 Base64
*/
function getBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.addEventListener('load', () => resolve(reader.result as string));
reader.addEventListener('error', reject);
});
}
/**
* 预览图片
*/
async function previewImage(
file: UploadFile,
open: Ref<boolean>,
fileList: Ref<UploadProps['fileList']>,
) {
// 非图片文件直接打开链接
if (!isImageFile(file)) {
const url = file.url || file.preview;
if (url) {
window.open(url, '_blank');
} else if (file.preview) {
window.open(file.preview, '_blank');
} else {
message.error($t('ui.formRules.previewWarning'));
}
return;
}
const [ImageComponent, PreviewGroupComponent] = await Promise.all([
Image,
PreviewGroup,
]);
// 过滤图片文件并生成预览
const imageFiles = (unref(fileList) || []).filter((f) => isImageFile(f));
for (const imgFile of imageFiles) {
if (!imgFile.url && !imgFile.preview && imgFile.originFileObj) {
imgFile.preview = await getBase64(imgFile.originFileObj);
}
}
const container = document.createElement('div');
document.body.append(container);
let isUnmounted = false;
const currentIndex = imageFiles.findIndex((f) => f.uid === file.uid);
const PreviewWrapper = {
setup() {
return () => {
if (isUnmounted) return null;
return h(
PreviewGroupComponent,
{
class: 'hidden',
preview: {
open: open.value,
current: currentIndex,
onOpenChange: (value: boolean) => {
open.value = value;
if (!value) {
setTimeout(() => {
if (!isUnmounted && container) {
isUnmounted = true;
render(null, container);
container.remove();
}
}, 300);
}
},
() => placeholder,
},
},
() =>
imageFiles.map((imgFile) =>
h(ImageComponent, {
key: imgFile.uid,
src: imgFile.url || imgFile.preview,
}),
),
};
}
}
);
};
},
};
// 构建预览图片组
const previewImage = async (
file: UploadFile,
visible: Ref<boolean>,
fileList: Ref<UploadProps['fileList']>,
) => {
// 如果当前文件不是图片,直接打开
if (!isImageFile(file)) {
if (file.url) {
window.open(file.url, '_blank');
} else if (file.preview) {
window.open(file.preview, '_blank');
} else {
message.error($t('ui.formRules.previewWarning'));
}
return;
}
// 对于图片文件,继续使用预览组
const [ImageComponent, PreviewGroupComponent] = await Promise.all([
Image,
PreviewGroup,
]);
const getBase64 = (file: File) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.addEventListener('load', () => resolve(reader.result));
reader.addEventListener('error', (error) => reject(error));
});
};
// 从fileList中过滤出所有图片文件
const imageFiles = (unref(fileList) || []).filter((element) =>
isImageFile(element),
);
// 为所有没有预览地址的图片生成预览
for (const imgFile of imageFiles) {
if (!imgFile.url && !imgFile.preview && imgFile.originFileObj) {
imgFile.preview = (await getBase64(imgFile.originFileObj)) as string;
}
}
const container: HTMLElement | null = document.createElement('div');
render(h(PreviewWrapper), container);
}
/**
* 图片裁剪操作
*/
function cropImage(file: File, aspectRatio: string | undefined) {
return new Promise<Blob | string | undefined>((resolve, reject) => {
const container = document.createElement('div');
document.body.append(container);
// 用于追踪组件是否已卸载
let isUnmounted = false;
let objectUrl: null | string = null;
const open = ref<boolean>(true);
const cropperRef = ref<InstanceType<typeof VCropper> | null>(null);
const PreviewWrapper = {
function closeModal() {
open.value = false;
setTimeout(() => {
if (!isUnmounted && container) {
if (objectUrl) {
URL.revokeObjectURL(objectUrl);
}
isUnmounted = true;
render(null, container);
container.remove();
}
}, 300);
}
const CropperWrapper = {
setup() {
return () => {
if (isUnmounted) return null;
if (!objectUrl) {
objectUrl = URL.createObjectURL(file);
}
return h(
PreviewGroupComponent,
Modal,
{
class: 'hidden',
preview: {
open: visible.value,
// 设置初始显示的图片索引
current: imageFiles.findIndex((f) => f.uid === file.uid),
onOpenChange: (value: boolean) => {
visible.value = value;
if (!value) {
// 延迟清理,确保动画完成
setTimeout(() => {
if (!isUnmounted && container) {
isUnmounted = true;
render(null, container);
container.remove();
}
}, 300);
open: open.value,
title: h('div', {}, [
$t('ui.crop.title'),
h(
'span',
{
class: `${aspectRatio ? '' : 'hidden'} ml-2 text-sm text-gray-400 font-normal`,
},
$t('ui.crop.titleTip', [aspectRatio]),
),
]),
centered: true,
width: 548,
keyboard: false,
maskClosable: false,
closable: false,
cancelText: $t('common.cancel'),
okText: $t('ui.crop.confirm'),
destroyOnHidden: true,
onOk: async () => {
const cropper = cropperRef.value;
if (!cropper) {
reject(new Error('Cropper not found'));
closeModal();
return;
}
try {
const dataUrl = await cropper.getCropImage();
if (dataUrl) {
resolve(dataUrl);
} else {
reject(new Error($t('ui.crop.errorTip')));
}
},
} catch {
reject(new Error($t('ui.crop.errorTip')));
} finally {
closeModal();
}
},
onCancel() {
resolve('');
closeModal();
},
},
() =>
// 渲染所有图片文件
imageFiles.map((imgFile) =>
h(ImageComponent, {
key: imgFile.uid,
src: imgFile.url || imgFile.preview,
}),
),
h(VCropper, {
ref: (ref: any) => (cropperRef.value = ref),
img: objectUrl as string,
aspectRatio,
}),
);
};
},
};
render(h(PreviewWrapper), container);
};
// 图片裁剪操作
const cropImage = (file: File, aspectRatio: string | undefined) => {
return new Promise((resolve, reject) => {
const container: HTMLElement | null = document.createElement('div');
document.body.append(container);
// 用于追踪组件是否已卸载
let isUnmounted = false;
let objectUrl: null | string = null;
const open = ref<boolean>(true);
const cropperRef = ref<InstanceType<typeof VCropper> | null>(null);
const closeModal = () => {
open.value = false;
// 延迟清理,确保动画完成
setTimeout(() => {
if (!isUnmounted && container) {
if (objectUrl) {
URL.revokeObjectURL(objectUrl);
}
isUnmounted = true;
render(null, container);
container.remove();
}
}, 300);
};
const CropperWrapper = {
setup() {
return () => {
if (isUnmounted) return null;
if (!objectUrl) {
objectUrl = URL.createObjectURL(file);
}
return h(
Modal,
{
open: open.value,
title: h('div', {}, [
$t('ui.crop.title'),
h(
'span',
{
class: `${aspectRatio ? '' : 'hidden'} ml-2 text-sm text-gray-400 font-normal`,
},
$t('ui.crop.titleTip', [aspectRatio]),
),
]),
centered: true,
width: 548,
keyboard: false,
maskClosable: false,
closable: false,
cancelText: $t('common.cancel'),
okText: $t('ui.crop.confirm'),
destroyOnHidden: true,
onOk: async () => {
const cropper = cropperRef.value;
if (!cropper) {
reject(new Error('Cropper not found'));
closeModal();
return;
}
try {
const dataUrl = await cropper.getCropImage();
resolve(dataUrl);
} catch {
reject(new Error($t('ui.crop.errorTip')));
} finally {
closeModal();
}
},
onCancel() {
resolve('');
closeModal();
},
},
() =>
h(VCropper, {
ref: (ref: any) => (cropperRef.value = ref),
img: objectUrl as string,
aspectRatio,
}),
);
};
},
};
render(h(CropperWrapper), container);
});
};
render(h(CropperWrapper), container);
});
}
/**
* 带预览功能的上传组件
*/
function withPreviewUpload() {
return defineComponent({
name: 'AUpload',
name: Upload.name,
emits: ['update:modelValue'],
setup: (
setup(
props: any,
{ attrs, slots, emit }: { attrs: any; emit: any; slots: any },
) => {
) {
const previewVisible = ref<boolean>(false);
const placeholder = attrs?.placeholder || $t(`ui.placeholder.upload`);
const placeholder = attrs?.placeholder || $t('ui.placeholder.upload');
const listType = attrs?.listType || attrs?.['list-type'] || 'text';
const fileList = ref<UploadProps['fileList']>(
attrs?.fileList || attrs?.['file-list'] || [],
);
@ -433,16 +448,18 @@ const withPreviewUpload = () => {
() => attrs?.aspectRatio ?? attrs?.['aspect-ratio'],
);
const handleBeforeUpload = async (
async function handleBeforeUpload(
file: UploadFile,
originFileList: Array<File>,
) => {
) {
// 文件大小限制
if (maxSize.value && (file.size || 0) / 1024 / 1024 > maxSize.value) {
message.error($t('ui.formRules.sizeLimit', [maxSize.value]));
file.status = 'removed';
return false;
}
// 多选或者非图片不唤起裁剪框
// 图片裁剪处理
if (
attrs.crop &&
!attrs.multiple &&
@ -450,27 +467,21 @@ const withPreviewUpload = () => {
isImageFile(file)
) {
file.status = 'removed';
// antd Upload组件问题 file参数获取的是UploadFile类型对象无法取到File类型 所以通过originFileList[0]获取
const blob = await cropImage(originFileList[0], aspectRatio.value);
return new Promise((resolve, reject) => {
if (!blob) {
return reject(new Error($t('ui.crop.errorTip')));
}
resolve(blob);
});
if (!blob) {
throw new Error($t('ui.crop.errorTip'));
}
return blob;
}
return attrs.beforeUpload?.(file) ?? true;
};
}
const handleChange = (event: UploadChangeParam) => {
function handleChange(event: UploadChangeParam) {
try {
// 行内写法 handleChange: (event) => {}
attrs.handleChange?.(event);
// template写法 @handle-change="(event) => {}"
attrs.onHandleChange?.(event);
} catch (error) {
// Avoid breaking internal v-model sync on user handler errors
console.error(error);
}
fileList.value = event.fileList.filter(
@ -480,28 +491,95 @@ const withPreviewUpload = () => {
'update:modelValue',
event.fileList?.length ? fileList.value : undefined,
);
};
}
const handlePreview = async (file: UploadFile) => {
function handlePreview(file: UploadFile) {
previewVisible.value = true;
await previewImage(file, previewVisible, fileList);
};
return previewImage(file, previewVisible, fileList);
}
const renderUploadButton = (): any => {
const isDisabled = attrs.disabled;
function renderUploadButton() {
if (attrs.disabled) return null;
return isEmpty(slots)
? createDefaultUploadSlots(listType, placeholder)
: slots;
}
// 如果禁用,不渲染上传按钮
if (isDisabled) {
return null;
// 拖拽排序
const draggable = computed(
() => (attrs.draggable ?? false) && !attrs.disabled,
);
const uploadId = `upload-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
const sortableInstance = ref<null | Sortable>(null);
const styleId = `upload-drag-style-${uploadId}`;
function injectDragStyle() {
if (!document.querySelector(`[id="${styleId}"]`)) {
const style = document.createElement('style');
style.id = styleId;
style.textContent = `
[data-upload-id="${uploadId}"] .ant-upload-list-item { cursor: move; }
[data-upload-id="${uploadId}"] .ant-upload-list-item:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.15); }
`;
document.head.append(style);
}
}
// 否则渲染默认上传按钮
return isEmpty(slots)
? createDefaultSlotsWithUpload(listType, placeholder)
: slots;
};
function removeDragStyle() {
document.querySelector(`[id="${styleId}"]`)?.remove();
}
async function initSortable(retryCount = 0) {
if (!draggable.value) return;
injectDragStyle();
await nextTick();
await new Promise((resolve) => setTimeout(resolve, 100));
const container = document.querySelector(
`[data-upload-id="${uploadId}"] .ant-upload-list`,
) as HTMLElement;
if (!container) {
if (retryCount < 5) {
setTimeout(() => initSortable(retryCount + 1), 200);
}
return;
}
const { initializeSortable } = useSortable(container, {
animation: 300,
delay: 400,
delayOnTouchOnly: true,
filter:
'.ant-upload-select, .ant-upload-list-item-error, .ant-upload-list-item-uploading',
onEnd: (evt) => {
const { oldIndex, newIndex } = evt;
if (
oldIndex === undefined ||
newIndex === undefined ||
oldIndex === newIndex
) {
return;
}
const list = [...(fileList.value || [])];
const [movedItem] = list.splice(oldIndex, 1);
if (movedItem) {
list.splice(newIndex, 0, movedItem);
fileList.value = list;
}
attrs.onDragSort?.(oldIndex, newIndex);
emit('update:modelValue', fileList.value);
},
});
sortableInstance.value = await initializeSortable();
}
// 可以监听到表单API设置的值
// 监听表单值变化
watch(
() => attrs.modelValue,
(res) => {
@ -509,22 +587,32 @@ const withPreviewUpload = () => {
},
);
onMounted(initSortable);
onUnmounted(() => {
sortableInstance.value?.destroy();
removeDragStyle();
});
return () =>
h(
Upload,
{
...props,
...attrs,
fileList: fileList.value,
beforeUpload: handleBeforeUpload,
onChange: handleChange,
onPreview: handlePreview,
},
renderUploadButton(),
'div',
{ 'data-upload-id': uploadId, class: 'w-full' },
h(
Upload,
{
...props,
...attrs,
fileList: fileList.value,
beforeUpload: handleBeforeUpload,
onChange: handleChange,
onPreview: handlePreview,
},
renderUploadButton() as any,
),
);
},
});
};
}
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type ComponentType =
@ -535,6 +623,7 @@ export type ComponentType =
| 'Cascader'
| 'Checkbox'
| 'CheckboxGroup'
| 'CollapsibleParams'
| 'DatePicker'
| 'DefaultButton'
| 'Divider'
@ -548,6 +637,7 @@ export type ComponentType =
| 'RadioGroup'
| 'RangePicker'
| 'Rate'
| 'RichEditor'
| 'Select'
| 'Space'
| 'Switch'
@ -568,6 +658,7 @@ export interface ComponentPropsMap {
Cascader: CascaderProps;
Checkbox: CheckboxProps;
CheckboxGroup: CheckboxGroupProps;
CollapsibleParams: CollapsibleParamsProps;
DatePicker: DatePickerProps;
DefaultButton: ButtonProps;
Divider: DividerProps;
@ -581,6 +672,7 @@ export interface ComponentPropsMap {
RadioGroup: RadioGroupProps;
RangePicker: RangePickerProps;
Rate: RateProps;
RichEditor: TipTapProps;
Select: SelectProps;
Space: SpaceProps;
Switch: SwitchProps;
@ -601,13 +693,13 @@ async function initComponentAdapter() {
fieldNames: { label: 'label', value: 'value', children: 'children' },
loadingSlot: 'suffixIcon',
modelPropName: 'value',
visibleEvent: 'onVisibleChange',
visibleEvent: 'onOpenChange',
}),
ApiSelect: withDefaultPlaceholder(ApiComponent, 'select', {
component: Select,
loadingSlot: 'suffixIcon',
modelPropName: 'value',
visibleEvent: 'onVisibleChange',
visibleEvent: 'onOpenChange',
}),
ApiTreeSelect: withDefaultPlaceholder(ApiComponent, 'select', {
component: TreeSelect,
@ -615,7 +707,7 @@ async function initComponentAdapter() {
loadingSlot: 'suffixIcon',
modelPropName: 'value',
optionsPropName: 'treeData',
visibleEvent: 'onVisibleChange',
visibleEvent: 'onOpenChange',
}),
AutoComplete,
Cascader,
@ -646,6 +738,27 @@ async function initComponentAdapter() {
RadioGroup,
RangePicker,
Rate,
RichEditor: withDefaultPlaceholder(VbenTiptap, 'input', {
imageUpload: {
upload: (file: any, onProgress: any) => {
return new Promise((resolve, reject) => {
upload_file({
file,
onProgress({ percent }) {
onProgress?.(percent);
},
onSuccess(response) {
// 从响应中提取图片URL
resolve(response?.data?.url ?? response?.url ?? '');
},
onError() {
reject(new Error($t('ui.tiptap.upload.uploadFailed')));
},
});
});
},
},
}),
Select: withDefaultPlaceholder(Select, 'select'),
Space,
Switch,
@ -653,6 +766,7 @@ async function initComponentAdapter() {
TimePicker,
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
Upload: withPreviewUpload(),
CollapsibleParams: VbenCollapsibleParams,
};
// 将组件注册到全局共享状态中

1
apps/web-antdv-next/src/api/core/index.ts

@ -1,3 +1,4 @@
export * from './auth';
export * from './menu';
export * from './upload';
export * from './user';

25
apps/web-antdv-next/src/api/core/upload.ts

@ -0,0 +1,25 @@
import { requestClient } from '#/api/request';
interface UploadFileParams {
file: File;
onError?: (error: Error) => void;
onProgress?: (progress: { percent: number }) => void;
onSuccess?: (data: any, file: File) => void;
}
export async function upload_file({
file,
onError,
onProgress,
onSuccess,
}: UploadFileParams) {
try {
onProgress?.({ percent: 0 });
const data = await requestClient.upload('/upload', { file });
onProgress?.({ percent: 100 });
onSuccess?.(data, file);
} catch (error) {
onError?.(error instanceof Error ? error : new Error(String(error)));
}
}

1
apps/web-antdv-next/src/layouts/basic.vue

@ -226,6 +226,7 @@ watch(
description="ann.vben@gmail.com"
tag-text="Pro"
@logout="handleLogout"
@clear-preferences-and-logout="handleLogout"
/>
</template>
<template #notification>

6
apps/web-antdv-next/src/preferences.ts

@ -1,4 +1,7 @@
import { defineOverridesPreferences } from '@vben/preferences';
import {
appCopyrightPreferences,
defineOverridesPreferences,
} from '@vben/preferences';
/**
* @description 项目配置文件
@ -10,4 +13,5 @@ export const overridesPreferences = defineOverridesPreferences({
app: {
name: import.meta.env.VITE_APP_TITLE,
},
copyright: appCopyrightPreferences,
});

1
apps/web-ele/src/layouts/basic.vue

@ -226,6 +226,7 @@ watch(
description="ann.vben@gmail.com"
tag-text="Pro"
@logout="handleLogout"
@clear-preferences-and-logout="handleLogout"
/>
</template>
<template #notification>

6
apps/web-ele/src/preferences.ts

@ -1,4 +1,7 @@
import { defineOverridesPreferences } from '@vben/preferences';
import {
appCopyrightPreferences,
defineOverridesPreferences,
} from '@vben/preferences';
/**
* @description 项目配置文件
@ -10,4 +13,5 @@ export const overridesPreferences = defineOverridesPreferences({
app: {
name: import.meta.env.VITE_APP_TITLE,
},
copyright: appCopyrightPreferences,
});

1
apps/web-naive/package.json

@ -26,7 +26,6 @@
"#/*": "./src/*"
},
"dependencies": {
"@vben-core/shadcn-ui": "workspace:*",
"@vben/access": "workspace:*",
"@vben/common-ui": "workspace:*",
"@vben/constants": "workspace:*",

1
apps/web-naive/src/layouts/basic.vue

@ -226,6 +226,7 @@ watch(
description="ann.vben@gmail.com"
tag-text="Pro"
@logout="handleLogout"
@clear-preferences-and-logout="handleLogout"
/>
</template>
<template #notification>

1
apps/web-naive/src/locales/langs/en-US/demos.json

@ -3,6 +3,7 @@
"naive": "Naive UI",
"table": "Table",
"form": "Form",
"arrayForm": "Array Editor",
"vben": {
"title": "Project",
"about": "About",

1
apps/web-naive/src/locales/langs/zh-CN/demos.json

@ -3,6 +3,7 @@
"naive": "Naive UI",
"table": "Table",
"form": "表单",
"arrayForm": "数组编辑器",
"vben": {
"title": "项目",
"about": "关于",

6
apps/web-naive/src/preferences.ts

@ -1,4 +1,7 @@
import { defineOverridesPreferences } from '@vben/preferences';
import {
appCopyrightPreferences,
defineOverridesPreferences,
} from '@vben/preferences';
/**
* @description 项目配置文件
@ -10,4 +13,5 @@ export const overridesPreferences = defineOverridesPreferences({
app: {
name: import.meta.env.VITE_APP_TITLE,
},
copyright: appCopyrightPreferences,
});

8
apps/web-naive/src/router/routes/modules/demos.ts

@ -37,6 +37,14 @@ const routes: RouteRecordRaw[] = [
path: '/demos/form',
component: () => import('#/views/demos/form/basic.vue'),
},
{
meta: {
title: $t('demos.arrayForm'),
},
name: 'ArrayForm',
path: '/demos/array-form',
component: () => import('#/views/demos/naive/array-form/index.vue'),
},
],
},
];

123
apps/web-naive/src/views/demos/naive/array-form/index.vue

@ -0,0 +1,123 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import { NButton, NCard, useMessage } from 'naive-ui';
import { useVbenForm } from '#/adapter/form';
const message = useMessage();
const [Form, formApi] = useVbenForm({
layout: 'vertical',
wrapperClass: 'grid-cols-1',
handleSubmit: (values) => {
message.success(`提交成功:${JSON.stringify(values)}`);
},
schema: [
{
component: 'Input',
fieldName: 'projectName',
label: '项目名称',
rules: 'required',
},
{
component: 'VbenFormFieldArray',
fieldName: 'members',
label: '项目成员',
// 初始化为空数组,供内部 useFieldArray 使用
defaultValue: [],
componentProps: {
min: 1,
max: 5,
createRow: () => ({
name: null,
age: null,
role: null,
joinDate: null,
active: true,
}),
// 每一列就是一个子字段,复用 vbenForm 的所有编辑组件
schema: [
{
component: 'Input',
fieldName: 'name',
label: '姓名',
rules: 'required',
componentProps: { placeholder: '请输入姓名' },
},
{
component: 'InputNumber',
fieldName: 'age',
label: '年龄',
componentProps: { min: 0, max: 150 },
},
{
component: 'Select',
fieldName: 'role',
label: '角色',
rules: 'selectRequired',
componentProps: {
placeholder: '请选择',
options: [
{ label: '前端', value: 'fe' },
{ label: '后端', value: 'be' },
{ label: '测试', value: 'qa' },
{ label: '产品', value: 'pm' },
],
},
},
{
component: 'DatePicker',
fieldName: 'joinDate',
label: '入职日期',
},
{
component: 'Switch',
fieldName: 'active',
label: '在职',
},
],
},
},
],
});
function setFormValues() {
formApi.setValues({
projectName: 'Vben Admin',
members: [
{ name: '张三', age: 28, role: 'fe', joinDate: Date.now(), active: true },
{
name: '李四',
age: 32,
role: 'be',
joinDate: Date.now(),
active: false,
},
],
});
}
async function getFormValues() {
const values = await formApi.getValues();
message.info(JSON.stringify(values));
}
</script>
<template>
<Page
description="基于 useVbenForm 的数组编辑器(VbenFormFieldArray):可增删行,每个单元格复用 vbenForm 注册的编辑组件,并享受逐格校验。"
title="数组编辑器表单"
>
<NCard title="数组编辑器">
<template #header-extra>
<NButton class="mr-2" @click="setFormValues">设置表单值</NButton>
<NButton class="mr-2" @click="getFormValues">获取表单值</NButton>
<NButton type="primary" @click="formApi.submitForm()">
提交校验
</NButton>
</template>
<Form />
</NCard>
</Page>
</template>

9
apps/web-tdesign/src/app.vue

@ -3,6 +3,7 @@ import type { GlobalConfigProvider } from 'tdesign-vue-next';
import { watch } from 'vue';
import { useTDesignDesignTokens } from '@vben/hooks';
import { usePreferences } from '@vben/preferences';
import { merge } from 'es-toolkit/compat';
@ -12,10 +13,16 @@ import zhConfig from 'tdesign-vue-next/es/locale/zh_CN';
defineOptions({ name: 'App' });
const { isDark } = usePreferences();
// 将 Vben 设计系统的 CSS 变量适配到 TDesign 的设计变量上
useTDesignDesignTokens();
watch(
() => isDark.value,
(dark) => {
document.documentElement.setAttribute('theme-mode', dark ? 'dark' : '');
document.documentElement.setAttribute(
'theme-mode',
dark ? 'dark' : 'light',
);
},
{ immediate: true },
);

3
apps/web-tdesign/src/bootstrap.ts

@ -5,8 +5,6 @@ import { registerLoadingDirective } from '@vben/common-ui/es/loading';
import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores';
import '@vben/styles';
// import '@vben/styles/antd';
// 引入组件库的少量全局样式变量
import { useTitle } from '@vueuse/core';
@ -17,6 +15,7 @@ import { initSetupVbenForm } from './adapter/form';
import App from './app.vue';
import { router } from './router';
// 引入组件库的少量全局样式变量
import 'tdesign-vue-next/es/style/index.css';
async function bootstrap(namespace: string) {

1
apps/web-tdesign/src/layouts/basic.vue

@ -226,6 +226,7 @@ watch(
description="ann.vben@gmail.com"
tag-text="Pro"
@logout="handleLogout"
@clear-preferences-and-logout="handleLogout"
/>
</template>
<template #notification>

6
apps/web-tdesign/src/preferences.ts

@ -1,4 +1,7 @@
import { defineOverridesPreferences } from '@vben/preferences';
import {
appCopyrightPreferences,
defineOverridesPreferences,
} from '@vben/preferences';
/**
* @description 项目配置文件
@ -10,4 +13,5 @@ export const overridesPreferences = defineOverridesPreferences({
app: {
name: import.meta.env.VITE_APP_TITLE,
},
copyright: appCopyrightPreferences,
});

2
cspell.json

@ -27,6 +27,7 @@
"execa",
"iconify",
"iconoir",
"indexeddb",
"intlify",
"isequal",
"jspm",
@ -36,6 +37,7 @@
"minw",
"mkdist",
"mockjs",
"myapp",
"naiveui",
"napi",
"nocheck",

2
docs/.vitepress/components/preview-group.vue

@ -5,7 +5,7 @@ import { computed, ref, useSlots } from 'vue';
import { VbenTooltip } from '@vben-core/shadcn-ui';
import { Code } from 'lucide-vue-next';
import { Code } from '@lucide/vue';
import {
TabsContent,
TabsIndicator,

8
docs/.vitepress/config/en.mts

@ -198,6 +198,14 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] {
link: 'common-ui/vben-ellipsis-text',
text: 'EllipsisText',
},
{
link: 'common-ui/vben-descriptions',
text: 'Descriptions',
},
{
link: 'common-ui/vben-table-action',
text: 'TableAction',
},
{
link: 'common-ui/vben-cropper',
text: 'Cropper',

8
docs/.vitepress/config/zh.mts

@ -196,6 +196,14 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] {
link: 'common-ui/vben-ellipsis-text',
text: 'EllipsisText 省略文本',
},
{
link: 'common-ui/vben-descriptions',
text: 'Descriptions 描述列表',
},
{
link: 'common-ui/vben-table-action',
text: 'TableAction 表格操作',
},
{
link: 'common-ui/vben-cropper',
text: 'Cropper 图片裁剪',

2
docs/package.json

@ -15,13 +15,13 @@
}
},
"dependencies": {
"@lucide/vue": "catalog:",
"@vben-core/shadcn-ui": "workspace:*",
"@vben/common-ui": "workspace:*",
"@vben/locales": "workspace:*",
"@vben/plugins": "workspace:*",
"@vben/styles": "workspace:*",
"antdv-next": "catalog:",
"lucide-vue-next": "catalog:",
"medium-zoom": "catalog:",
"reka-ui": "catalog:",
"vitepress-plugin-group-icons": "catalog:"

102
docs/src/components/common-ui/vben-descriptions.md

@ -0,0 +1,102 @@
---
outline: deep
---
# Vben Descriptions 描述列表
`Descriptions` 用于成组展示只读的字段信息,常用于详情页、信息预览等场景。组件基于 shadcn-ui 构建,API 参考 Ant Design Vue 的 Descriptions,支持响应式列数、跨列、边框、垂直布局等能力。
> 如果文档内没有覆盖到你需要的细节,可以结合在线示例一起查看。
::: info 写在前面
组件提供两种使用方式:通过 `items` 数据驱动(推荐),或通过子组件 `VbenDescriptionsItem` 声明列表项。两者可按需选择,`items` 优先级更高。:::
## 基础用法
通过 `items` 传入字段数组,每项包含 `label` 与 `content`。默认按断点自适应列数(`xs` 1 列、`sm` 2 列、`md` 及以上 3 列)。
<DemoPreview dir="demos/vben-descriptions/basic" />
## 带边框
设置 `bordered` 展示边框样式,配合 `title` 标题与 `#extra` 插槽(位于标题右侧的操作区域)。
<DemoPreview dir="demos/vben-descriptions/bordered" />
## 垂直布局
通过 `layout="vertical"` 让标签位于内容上方。
<DemoPreview dir="demos/vben-descriptions/vertical" />
## 不同尺寸
通过 `size` 设置 `small`、`middle`、`large` 三种尺寸。
<DemoPreview dir="demos/vben-descriptions/size" />
## 跨列与响应式
单项通过 `span` 设置跨列数,`'filled'` 表示占满当前行剩余空间;`column` 支持传入按断点配置的对象实现响应式列数。
<DemoPreview dir="demos/vben-descriptions/span" />
## 子组件用法
不传 `items` 时,可在默认插槽中使用 `VbenDescriptionsItem` 声明列表项,内容支持默认插槽或 `#content` 插槽自定义。
<DemoPreview dir="demos/vben-descriptions/custom" />
## API
### Descriptions Props
| 属性名 | 描述 | 类型 | 默认值 |
| --- | --- | --- | --- |
| items | 数据驱动的列表项;不传则读取默认插槽 | `DescriptionsItemType[]` | - |
| bordered | 是否展示边框 | `boolean` | `false` |
| column | 一行的列数,支持按断点配置 | `number \| Partial<Record<Breakpoint, number>>` | `{ xs: 1, sm: 2, md: 3, xxxl: 4 }` |
| layout | 布局方式 | `'horizontal' \| 'vertical'` | `'horizontal'` |
| size | 尺寸 | `'small' \| 'middle' \| 'large'` | `'middle'` |
| colon | 是否显示冒号(仅非边框的水平布局生效) | `boolean` | `true` |
| title | 标题 | `string` | - |
| extra | 标题右侧的操作区域 | `string` | - |
| labelStyle | 统一的标签样式 | `CSSProperties` | - |
| contentStyle | 统一的内容样式 | `CSSProperties` | - |
| class | 根节点自定义类名 | `string` | - |
### Descriptions Slots
| 插槽名 | 描述 |
| ------- | ---------------------------------- |
| title | 自定义标题 |
| extra | 自定义标题右侧操作区域 |
| default | 放置 `VbenDescriptionsItem` 子组件 |
### DescriptionsItem
`items` 数组中的每一项,或子组件 `VbenDescriptionsItem` 的属性。
| 属性名 | 描述 | 类型 | 默认值 |
| --- | --- | --- | --- |
| label | 标签 | `string \| number \| (() => VNode) \| Component` | - |
| content | 内容 | `string \| number \| (() => VNode) \| Component` | - |
| span | 跨列数,`'filled'` 占满当前行剩余 | `number \| 'filled' \| Partial<Record<Breakpoint, number>>` | `1` |
| labelStyle | 标签样式 | `CSSProperties` | - |
| contentStyle | 内容样式 | `CSSProperties` | - |
| key | 唯一标识 | `string \| number` | - |
### DescriptionsItem Slots
仅子组件用法可用。
| 插槽名 | 描述 |
| ------- | ------------------------ |
| default | 内容(等价于 `content`) |
| content | 自定义内容 |
| label | 自定义标签 |
::: tip Breakpoint
响应式断点 `Breakpoint` 取值为 `'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'xxxl'`,断点像素与 Ant Design 一致(`sm` 576、`md` 768、`lg` 992、`xl` 1200、`xxl` 1600、`xxxl` 2000)。:::

165
docs/src/components/common-ui/vben-table-action.md

@ -0,0 +1,165 @@
---
outline: deep
---
# Vben TableAction 表格操作
`TableAction` 用于在表格操作列中渲染一组操作按钮,参考 vben2 的 TableAction 设计。基于 shadcn-ui 构建,支持权限控制、气泡确认、提示、下拉「更多」、分割线等能力,可在表格内外任意场景复用。
> 如果文档内没有覆盖到你需要的细节,可以结合在线示例一起查看。
::: info 写在前面
组件本身不依赖任何业务逻辑(不直接读取权限 store),权限通过注入 `hasPermission` 实现,从而保持核心层零耦合、可跨框架复用。在 vxe-table 中推荐通过列插槽(`slots: { default: 'action' }`)在页面里渲染,不改变表格原有的渲染机制。:::
## 基础用法
通过 `actions` 传入操作项数组,每项包含 `text`、`onClick` 等;`danger` 标记危险操作,`divider` 显示按钮间分割线。
<DemoPreview dir="demos/vben-table-action/basic" />
## 提示
通过 `tooltip` 为操作项添加提示,支持字符串或 `{ content, side }` 配置。
<DemoPreview dir="demos/vben-table-action/tooltip" />
## 气泡确认
通过 `popConfirm` 开启点击前的气泡确认,常用于删除等危险操作。
<DemoPreview dir="demos/vben-table-action/popconfirm" />
## 更多下拉
通过 `dropdownActions` 将次要操作收纳到「更多」下拉中,`moreText` 可自定义按钮文案。
<DemoPreview dir="demos/vben-table-action/dropdown" />
## 权限控制
为操作项设置 `auth` 权限码,并注入 `hasPermission` 判断函数,无权限的操作会被隐藏。
<DemoPreview dir="demos/vben-table-action/permission" />
## 在 vxe-table 中使用
不改变 vxe-table 原有渲染方式,推荐在列配置中声明插槽,在页面通过插槽渲染。
::: tip 推荐:使用适配器封装的版本项目的 `#/adapter/vxe-table` 已对 `VbenTableAction` 做了二次封装,内部统一注入了 `hasPermission`(基于 `useAccess().hasAccessByCodes`)。因此从适配器引入时**无需再传入 `:has-permission`**,只需通过操作项的 `auth` 字段声明权限码即可。:::
```ts
// data.ts —— 列配置声明插槽
{
align: 'center',
field: 'operation',
fixed: 'right',
slots: { default: 'action' },
title: $t('system.user.operation'),
width: 180,
}
```
```vue
<!-- list.vue —— 从适配器引入,权限自动注入,无需传入 has-permission -->
<script setup lang="ts">
import { VbenTableAction } from '#/adapter/vxe-table';
</script>
<template>
<Grid>
<template #action="{ row }">
<template #action="{ row }">
<VbenTableAction
:actions="[
{
text: $t('common.detail'),
icon: 'lucide:eye',
onClick: () => onDetail(row),
},
{
text: $t('common.edit'),
icon: 'lucide:edit',
onClick: () => onEdit(row),
},
]"
:dropdown-actions="[
{
text: $t('common.delete'),
icon: 'lucide:trash-2',
danger: true,
onClick: () => onDelete(row),
auth: ['AC_100100'],
},
]"
align="center"
/>
</template>
</template>
</Grid>
</template>
```
若直接从 `@vben/common-ui` 引入核心组件(不经过适配器),组件不依赖任何业务逻辑,需自行注入 `hasPermission`:
```vue
<script setup lang="ts">
import { useAccess } from '@vben/access';
import { VbenTableAction } from '@vben/common-ui';
const { hasAccessByCodes } = useAccess();
function hasPermission(auth?: string | string[]) {
if (!auth) return true;
return hasAccessByCodes(Array.isArray(auth) ? auth : [auth]);
}
</script>
<template>
<VbenTableAction
v-bind="useActions(row, onActionClick)"
:has-permission="hasPermission"
align="center"
/>
</template>
```
## API
### TableAction Props
| 属性名 | 描述 | 类型 | 默认值 |
| --- | --- | --- | --- |
| actions | 主操作按钮 | `ActionItem[]` | `[]` |
| dropdownActions | 「更多」下拉中的操作 | `ActionItem[]` | `[]` |
| align | 对齐方式 | `'start' \| 'center' \| 'end'` | `'end'` |
| divider | 按钮之间是否显示分割线 | `boolean` | `false` |
| moreText | 「更多」按钮文案(提供时显示在图标右侧) | `string` | - |
| hasPermission | 权限判断函数,返回 `false` 则隐藏对应 `auth` 的操作(从 `#/adapter/vxe-table` 引入时已自动注入,无需手动传入) | `(auth?: string \| string[]) => boolean` | - |
| class | 根节点自定义类名 | `string` | - |
### ActionItem
| 属性名 | 描述 | 类型 | 默认值 |
| --- | --- | --- | --- |
| text | 按钮文本 | `string` | - |
| icon | 图标组件 | `string`\| `VbenIcon` | - |
| onClick | 点击回调 | `() => void` | - |
| auth | 权限码,配合 `hasPermission` 过滤 | `string \| string[]` | - |
| ifShow | 是否显示 | `boolean \| (() => boolean)` | `true` |
| disabled | 是否禁用 | `boolean` | `false` |
| loading | 加载状态 | `boolean` | `false` |
| danger | 危险操作(红色文字) | `boolean` | `false` |
| tooltip | 提示 | `string \| { content: string; side?: 'top' \| 'bottom' \| 'left' \| 'right' }` | - |
| popConfirm | 气泡确认 | `TableActionPopConfirm` | - |
| variant | 按钮样式变体 | `ButtonVariants['variant']` | `'link'` |
| size | 按钮尺寸 | `ButtonVariants['size']` | `'sm'` |
| key | 唯一标识 | `string \| number` | - |
### TableActionPopConfirm
| 属性名 | 描述 | 类型 | 默认值 |
| --- | --- | --- | --- |
| title | 提示标题 | `string` | `'Are you sure?'` |
| okText | 确认按钮文案 | `string` | `'OK'` |
| cancelText | 取消按钮文案 | `string` | `'Cancel'` |
| confirm | 确认回调;未提供时回退到 `action.onClick` | `() => void` | - |

2
docs/src/demos/vben-alert/prompt/index.vue

@ -3,8 +3,8 @@ import { h } from 'vue';
import { alert, prompt, useAlertContext, VbenButton } from '@vben/common-ui';
import { BadgeJapaneseYen } from '@lucide/vue';
import { Input, RadioGroup, Select } from 'antdv-next';
import { BadgeJapaneseYen } from 'lucide-vue-next';
function showPrompt() {
prompt({

18
docs/src/demos/vben-descriptions/basic/index.vue

@ -0,0 +1,18 @@
<script lang="ts" setup>
import { VbenDescriptions } from '@vben/common-ui';
const items = [
{ content: 'Vben', label: '用户名' },
{ content: '13800138000', label: '手机号' },
{ content: '中国 · 杭州', label: '居住地' },
{ content: '前端工程师', label: '职位' },
{
content: '这是一段较长的备注信息,用于演示跨列展示。',
label: '备注',
span: 3,
},
];
</script>
<template>
<VbenDescriptions :items="items" />
</template>

22
docs/src/demos/vben-descriptions/bordered/index.vue

@ -0,0 +1,22 @@
<script lang="ts" setup>
import { VbenDescriptions } from '@vben/common-ui';
const items = [
{ content: 'Vben', label: '用户名' },
{ content: '13800138000', label: '手机号' },
{ content: '正常', label: '状态' },
{ content: '中国 · 杭州', label: '居住地' },
{
content: '浙江省杭州市西湖区某某街道某某小区 1 幢 2 单元',
label: '地址',
span: 3,
},
];
</script>
<template>
<VbenDescriptions bordered title="用户信息" :items="items">
<template #extra>
<span style="color: #1677ff; cursor: pointer">编辑</span>
</template>
</VbenDescriptions>
</template>

17
docs/src/demos/vben-descriptions/custom/index.vue

@ -0,0 +1,17 @@
<script lang="ts" setup>
import { VbenDescriptions, VbenDescriptionsItem } from '@vben/common-ui';
</script>
<template>
<!-- 通过子组件 VbenDescriptionsItem 声明列表项 -->
<VbenDescriptions bordered :column="2">
<VbenDescriptionsItem label="用户名">Vben</VbenDescriptionsItem>
<VbenDescriptionsItem label="状态">
<span style="color: #52c41a">● 正常</span>
</VbenDescriptionsItem>
<VbenDescriptionsItem label="备注" :span="2">
<template #content>
<span style="color: #888">通过 #content 插槽自定义内容</span>
</template>
</VbenDescriptionsItem>
</VbenDescriptions>
</template>

35
docs/src/demos/vben-descriptions/size/index.vue

@ -0,0 +1,35 @@
<script lang="ts" setup>
import { VbenDescriptions } from '@vben/common-ui';
const items = [
{ content: 'Vben', label: '用户名' },
{ content: '13800138000', label: '手机号' },
{ content: '中国 · 杭州', label: '居住地' },
{ content: '前端工程师', label: '职位' },
];
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 16px">
<VbenDescriptions
size="small"
bordered
title="Small"
:column="2"
:items="items"
/>
<VbenDescriptions
size="middle"
bordered
title="Middle"
:column="2"
:items="items"
/>
<VbenDescriptions
size="large"
bordered
title="Large"
:column="2"
:items="items"
/>
</div>
</template>

15
docs/src/demos/vben-descriptions/span/index.vue

@ -0,0 +1,15 @@
<script lang="ts" setup>
import { VbenDescriptions } from '@vben/common-ui';
const items = [
{ content: '1', label: 'A' },
{ content: '2(span: 2)', label: 'B', span: 2 },
{ content: '3', label: 'C' },
{ content: '占满当前行剩余空间', label: 'D(span: filled)', span: 'filled' },
{ content: '5', label: 'E' },
];
</script>
<template>
<!-- 列数随断点变化:xs 1 列、sm 2 列、md 及以上 3 列 -->
<VbenDescriptions bordered :column="{ md: 3, sm: 2, xs: 1 }" :items="items" />
</template>

13
docs/src/demos/vben-descriptions/vertical/index.vue

@ -0,0 +1,13 @@
<script lang="ts" setup>
import { VbenDescriptions } from '@vben/common-ui';
const items = [
{ content: 'Vben', label: '用户名' },
{ content: '13800138000', label: '手机号' },
{ content: '中国 · 杭州', label: '居住地' },
{ content: '这是一段较长的备注信息。', label: '备注', span: 3 },
];
</script>
<template>
<VbenDescriptions bordered layout="vertical" :items="items" />
</template>

28
docs/src/demos/vben-table-action/basic/index.vue

@ -0,0 +1,28 @@
<script lang="ts" setup>
import type { ActionItem } from '@vben/common-ui';
import { ref } from 'vue';
import { VbenTableAction } from '@vben/common-ui';
const last = ref('无');
const actions: ActionItem[] = [
{ key: 'edit', onClick: () => (last.value = '编辑'), text: '编辑' },
{ key: 'detail', onClick: () => (last.value = '详情'), text: '详情' },
{
danger: true,
key: 'delete',
onClick: () => (last.value = '删除'),
text: '删除',
},
];
</script>
<template>
<div>
<VbenTableAction :actions="actions" align="start" divider />
<p style="margin-top: 8px; font-size: 13px; opacity: 0.7">
最近点击:{{ last }}
</p>
</div>
</template>

44
docs/src/demos/vben-table-action/dropdown/index.vue

@ -0,0 +1,44 @@
<script lang="ts" setup>
import type { ActionItem } from '@vben/common-ui';
import { ref } from 'vue';
import { VbenTableAction } from '@vben/common-ui';
const last = ref('无');
const actions: ActionItem[] = [
{ key: 'edit', onClick: () => (last.value = '编辑'), text: '编辑' },
];
const dropdownActions: ActionItem[] = [
{ key: 'copy', onClick: () => (last.value = '复制'), text: '复制' },
{ key: 'export', onClick: () => (last.value = '导出'), text: '导出' },
{
danger: true,
key: 'remove',
// 下拉项同样支持气泡确认
popConfirm: {
cancelText: '取消',
confirm: () => (last.value = '已移除'),
okText: '确认',
title: '确定移除吗?',
},
text: '移除',
},
];
</script>
<template>
<div>
<VbenTableAction
:actions="actions"
:dropdown-actions="dropdownActions"
align="start"
divider
more-text="更多"
/>
<p style="margin-top: 8px; font-size: 13px; opacity: 0.7">
最近点击:{{ last }}
</p>
</div>
</template>

28
docs/src/demos/vben-table-action/permission/index.vue

@ -0,0 +1,28 @@
<script lang="ts" setup>
import type { ActionItem } from '@vben/common-ui';
import { VbenTableAction } from '@vben/common-ui';
// 模拟当前用户拥有的权限码
const allow = new Set(['user:detail', 'user:edit']);
function hasPermission(auth?: string | string[]) {
if (!auth) return true;
const codes = Array.isArray(auth) ? auth : [auth];
return codes.some((code) => allow.has(code));
}
const actions: ActionItem[] = [
{ auth: 'user:edit', key: 'edit', text: '编辑' },
{ auth: 'user:detail', key: 'detail', text: '详情' },
// 无 user:delete 权限,按钮被隐藏
{ auth: 'user:delete', danger: true, key: 'delete', text: '删除(无权限)' },
];
</script>
<template>
<VbenTableAction
:actions="actions"
:has-permission="hasPermission"
align="start"
/>
</template>

32
docs/src/demos/vben-table-action/popconfirm/index.vue

@ -0,0 +1,32 @@
<script lang="ts" setup>
import type { ActionItem } from '@vben/common-ui';
import { ref } from 'vue';
import { VbenTableAction } from '@vben/common-ui';
const last = ref('无');
const actions: ActionItem[] = [
{ key: 'edit', onClick: () => (last.value = '编辑'), text: '编辑' },
{
danger: true,
key: 'delete',
popConfirm: {
cancelText: '取消',
confirm: () => (last.value = '已删除'),
okText: '确认',
title: '确定删除这一行吗?',
},
text: '删除',
},
];
</script>
<template>
<div>
<VbenTableAction :actions="actions" align="start" />
<p style="margin-top: 8px; font-size: 13px; opacity: 0.7">
最近操作:{{ last }}
</p>
</div>
</template>

17
docs/src/demos/vben-table-action/tooltip/index.vue

@ -0,0 +1,17 @@
<script lang="ts" setup>
import type { ActionItem } from '@vben/common-ui';
import { VbenTableAction } from '@vben/common-ui';
const actions: ActionItem[] = [
{ key: 'edit', text: '编辑', tooltip: '编辑这一行' },
{
key: 'detail',
text: '详情',
tooltip: { content: '查看详情', side: 'top' },
},
];
</script>
<template>
<VbenTableAction :actions="actions" align="start" />
</template>

102
docs/src/en/components/common-ui/vben-descriptions.md

@ -0,0 +1,102 @@
---
outline: deep
---
# Vben Descriptions
`Descriptions` displays a group of read-only fields, commonly used on detail pages and information previews. It is built on shadcn-ui with an API modeled after Ant Design Vue's Descriptions, supporting responsive columns, column spanning, borders, and vertical layout.
> If the documentation does not cover the details you need, please refer to the online examples.
::: info Before you start
The component supports two usages: data-driven via `items` (recommended), or declaring entries with the `VbenDescriptionsItem` child component. `items` takes precedence when both are provided. :::
## Basic Usage
Pass an array of fields via `items`, each with a `label` and `content`. Columns adapt to breakpoints by default (1 column on `xs`, 2 on `sm`, 3 on `md` and above).
<DemoPreview dir="demos/vben-descriptions/basic" />
## Bordered
Set `bordered` for a bordered style, combined with the `title` prop and the `#extra` slot (an action area on the right of the title).
<DemoPreview dir="demos/vben-descriptions/bordered" />
## Vertical Layout
Use `layout="vertical"` to place labels above their content.
<DemoPreview dir="demos/vben-descriptions/vertical" />
## Sizes
Use `size` to switch between `small`, `middle`, and `large`.
<DemoPreview dir="demos/vben-descriptions/size" />
## Span & Responsive
Set `span` on an item to span multiple columns; `'filled'` fills the remaining space of the current row. `column` accepts a breakpoint-keyed object for responsive columns.
<DemoPreview dir="demos/vben-descriptions/span" />
## Child Component Usage
When `items` is omitted, declare entries with `VbenDescriptionsItem` in the default slot. Content can be customized via the default slot or the `#content` slot.
<DemoPreview dir="demos/vben-descriptions/custom" />
## API
### Descriptions Props
| Prop | Description | Type | Default |
| --- | --- | --- | --- |
| items | Data-driven entries; reads the default slot when omitted | `DescriptionsItemType[]` | - |
| bordered | Whether to show borders | `boolean` | `false` |
| column | Columns per row, supports breakpoint config | `number \| Partial<Record<Breakpoint, number>>` | `{ xs: 1, sm: 2, md: 3, xxxl: 4 }` |
| layout | Layout direction | `'horizontal' \| 'vertical'` | `'horizontal'` |
| size | Size | `'small' \| 'middle' \| 'large'` | `'middle'` |
| colon | Show colon (only for non-bordered horizontal layout) | `boolean` | `true` |
| title | Title | `string` | - |
| extra | Action area on the right of the title | `string` | - |
| labelStyle | Shared label style | `CSSProperties` | - |
| contentStyle | Shared content style | `CSSProperties` | - |
| class | Custom class for the root node | `string` | - |
### Descriptions Slots
| Slot | Description |
| ------- | ------------------------------------- |
| title | Custom title |
| extra | Custom action area beside the title |
| default | Place `VbenDescriptionsItem` children |
### DescriptionsItem
Each entry in `items`, or the props of the `VbenDescriptionsItem` child component.
| Prop | Description | Type | Default |
| --- | --- | --- | --- |
| label | Label | `string \| number \| (() => VNode) \| Component` | - |
| content | Content | `string \| number \| (() => VNode) \| Component` | - |
| span | Columns to span, `'filled'` fills the rest of the row | `number \| 'filled' \| Partial<Record<Breakpoint, number>>` | `1` |
| labelStyle | Label style | `CSSProperties` | - |
| contentStyle | Content style | `CSSProperties` | - |
| key | Unique key | `string \| number` | - |
### DescriptionsItem Slots
Available only for the child component usage.
| Slot | Description |
| ------- | --------------------------------- |
| default | Content (equivalent to `content`) |
| content | Custom content |
| label | Custom label |
::: tip Breakpoint
The responsive `Breakpoint` is one of `'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'xxxl'`, with pixel values aligned with Ant Design (`sm` 576, `md` 768, `lg` 992, `xl` 1200, `xxl` 1600, `xxxl` 2000). :::

165
docs/src/en/components/common-ui/vben-table-action.md

@ -0,0 +1,165 @@
---
outline: deep
---
# Vben TableAction
`TableAction` renders a group of action buttons for table operation columns, inspired by the TableAction component from vben2. Built on shadcn-ui, it supports permission control, popconfirm, tooltips, a "more" dropdown, and dividers, and can be reused inside or outside tables.
> If the documentation does not cover the details you need, please refer to the online examples.
::: info Before you start
The component carries no business logic (it does not read the permission store directly); permissions are handled by injecting `hasPermission`, keeping the core layer decoupled and reusable across frameworks. Inside vxe-table, the recommended approach is to render it via a column slot (`slots: { default: 'action' }`) on the page, without changing the table's original rendering mechanism. :::
## Basic Usage
Pass an array of action items via `actions`, each with `text`, `onClick`, etc. `danger` marks destructive actions, and `divider` shows separators between buttons.
<DemoPreview dir="demos/vben-table-action/basic" />
## Tooltip
Add a tooltip to an action via `tooltip`, accepting a string or a `{ content, side }` object.
<DemoPreview dir="demos/vben-table-action/tooltip" />
## PopConfirm
Use `popConfirm` to require confirmation before the action runs, commonly used for destructive actions like delete.
<DemoPreview dir="demos/vben-table-action/popconfirm" />
## More Dropdown
Use `dropdownActions` to collapse secondary actions into a "more" dropdown. `moreText` customizes the button label.
<DemoPreview dir="demos/vben-table-action/dropdown" />
## Permission Control
Set an `auth` code on an action and inject a `hasPermission` resolver; actions without permission are hidden.
<DemoPreview dir="demos/vben-table-action/permission" />
## Usage with vxe-table
Without changing vxe-table's rendering mechanism, declare a slot in the column config and render it on the page.
::: tip Recommended: use the adapter-wrapped version The project's `#/adapter/vxe-table` re-wraps `VbenTableAction` and injects `hasPermission` internally (based on `useAccess().hasAccessByCodes`). So when you import it from the adapter, **you no longer need to pass `:has-permission`** — just declare permission codes via the `auth` field of each action. :::
```ts
// data.ts — declare a slot in the column config
{
align: 'center',
field: 'operation',
fixed: 'right',
slots: { default: 'action' },
title: $t('system.user.operation'),
width: 180,
}
```
```vue
<!-- list.vue — import from the adapter; permission is auto-injected, no has-permission needed -->
<script setup lang="ts">
import { VbenTableAction } from '#/adapter/vxe-table';
</script>
<template>
<Grid>
<template #action="{ row }">
<template #action="{ row }">
<VbenTableAction
:actions="[
{
text: $t('common.detail'),
icon: 'lucide:eye',
onClick: () => onDetail(row),
},
{
text: $t('common.edit'),
icon: 'lucide:edit',
onClick: () => onEdit(row),
},
]"
:dropdown-actions="[
{
text: $t('common.delete'),
icon: 'lucide:trash-2',
danger: true,
onClick: () => onDelete(row),
auth: ['AC_100100'],
},
]"
align="center"
/>
</template>
</template>
</Grid>
</template>
```
If you import the core component directly from `@vben/common-ui` (without going through the adapter), the component carries no business logic and you need to inject `hasPermission` yourself:
```vue
<script setup lang="ts">
import { useAccess } from '@vben/access';
import { VbenTableAction } from '@vben/common-ui';
const { hasAccessByCodes } = useAccess();
function hasPermission(auth?: string | string[]) {
if (!auth) return true;
return hasAccessByCodes(Array.isArray(auth) ? auth : [auth]);
}
</script>
<template>
<VbenTableAction
v-bind="useActions(row, onActionClick)"
:has-permission="hasPermission"
align="center"
/>
</template>
```
## API
### TableAction Props
| Prop | Description | Type | Default |
| --- | --- | --- | --- |
| actions | Main action buttons | `ActionItem[]` | `[]` |
| dropdownActions | Actions inside the "more" dropdown | `ActionItem[]` | `[]` |
| align | Alignment | `'start' \| 'center' \| 'end'` | `'end'` |
| divider | Whether to show separators between buttons | `boolean` | `false` |
| moreText | Label for the "more" button (shown beside the icon) | `string` | - |
| hasPermission | Permission resolver; returning `false` hides the action with that `auth` (auto-injected when imported from `#/adapter/vxe-table`, no need to pass manually) | `(auth?: string \| string[]) => boolean` | - |
| class | Custom class for the root node | `string` | - |
### ActionItem
| Prop | Description | Type | Default |
| --- | --- | --- | --- |
| text | Button text | `string` | - |
| icon | Icon component | `string` \| `VbenIcon` | - |
| onClick | Click callback | `() => void` | - |
| auth | Permission code, filtered by `hasPermission` | `string \| string[]` | - |
| ifShow | Whether to show | `boolean \| (() => boolean)` | `true` |
| disabled | Whether disabled | `boolean` | `false` |
| loading | Loading state | `boolean` | `false` |
| danger | Destructive action (red text) | `boolean` | `false` |
| tooltip | Tooltip | `string \| { content: string; side?: 'top' \| 'bottom' \| 'left' \| 'right' }` | - |
| popConfirm | PopConfirm | `TableActionPopConfirm` | - |
| variant | Button variant | `ButtonVariants['variant']` | `'link'` |
| size | Button size | `ButtonVariants['size']` | `'sm'` |
| key | Unique key | `string \| number` | - |
### TableActionPopConfirm
| Prop | Description | Type | Default |
| --- | --- | --- | --- |
| title | Confirm title | `string` | `'Are you sure?'` |
| okText | Confirm button text | `string` | `'OK'` |
| cancelText | Cancel button text | `string` | `'Cancel'` |
| confirm | Confirm callback; falls back to `action.onClick` if omitted | `() => void` | - |

2
docs/src/en/guide/essentials/server.md

@ -98,7 +98,7 @@ VITE_GLOB_API_URL=https://mock-napi.vben.pro/api
::: tip How to Dynamically Modify API Endpoint in Production
Variables starting with `VITE_GLOB_*` in the `.env` file are injected into the `_app.config.js` file during packaging. After packaging, you can modify the corresponding API addresses in `dist/_app.config.js` and refresh the page to apply the changes. This eliminates the need to package multiple times for different environments, allowing a single package to be deployed across multiple API environments.
Variables starting with `VITE_GLOB_*` in the `.env` file are injected into the `_app-config-{version}-{hash}.js` file during packaging. After packaging, you can modify the corresponding API addresses in `dist/_app-config-{version}-{hash}.js` and refresh the page to apply the changes. This eliminates the need to package multiple times for different environments, allowing a single package to be deployed across multiple API environments.
:::

10
docs/src/en/guide/essentials/settings.md

@ -21,7 +21,7 @@ The rules are consistent with [Vite Env Variables and Modes](https://vitejs.dev/
console.log(import.meta.env.VITE_PROT);
```
- Variables starting with `VITE_GLOB_*` will be added to the `_app.config.js` configuration file during packaging.
- Variables starting with `VITE_GLOB_*` will be added to the `_app-config-{version}-{hash}.js` configuration file during packaging.
:::
@ -87,9 +87,9 @@ VITE_ARCHIVER=true
## Dynamic Configuration in Production Environment
When executing `pnpm build` in the root directory of the monorepo, a `dist/_app.config.js` file will be automatically generated in the corresponding application and inserted into `index.html`.
When executing `pnpm build` in the root directory of the monorepo, a `dist/_app-config-{version}-{hash}.js` file will be automatically generated in the corresponding application and inserted into `index.html`.
`_app.config.js` is a dynamic configuration file that allows for modifications to the configuration dynamically based on different environments after the project has been built. The content is as follows:
`_app-config-{version}-{hash}.js` is a dynamic configuration file that allows for modifications to the configuration dynamically based on different environments after the project has been built. The content is as follows:
```ts
window._VBEN_ADMIN_PRO_APP_CONF_ = {
@ -104,11 +104,11 @@ Object.defineProperty(window, '_VBEN_ADMIN_PRO_APP_CONF_', {
### Purpose
`_app.config.js` is used for projects that need to dynamically modify configurations after packaging, such as API endpoints. There's no need to repackage; you can simply modify the variables in `/dist/_app.config.js` after packaging, and refresh to update the variables in the code. A `js` file is used to ensure that the configuration file is loaded early in the order.
`_app-config-{version}-{hash}.js` is used for projects that need to dynamically modify configurations after packaging, such as API endpoints. There's no need to repackage; you can simply modify the variables in `/dist/_app-config-{version}-{hash}.js` after packaging, and refresh to update the variables in the code. A `js` file is used to ensure that the configuration file is loaded early in the order.
### Usage
To access the variables inside `_app.config.js`, you need to use the `useAppConfig` method provided by `@vben/hooks`.
To access the variables inside `_app-config-{version}-{hash}.js`, you need to use the `useAppConfig` method provided by `@vben/hooks`.
```ts
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);

2
docs/src/en/guide/introduction/quick-start.md

@ -10,7 +10,7 @@ outline: deep
Before starting the project, ensure that your environment meets the following requirements:
- [Node.js](https://nodejs.org/en) version 20.15.0 or above. It is recommended to use [fnm](https://github.com/Schniz/fnm), [nvm](https://github.com/nvm-sh/nvm), or directly use [pnpm](https://pnpm.io/cli/env) for version management.
- [Node.js](https://nodejs.org/en) version 22.18.0 or above. It is recommended to use [fnm](https://github.com/Schniz/fnm), [nvm](https://github.com/nvm-sh/nvm), or directly use [pnpm](https://pnpm.io/cli/env) for version management.
- [Git](https://git-scm.com/) any version.
To verify if your environment meets the above requirements, you can check the versions using the following commands:

2
docs/src/en/guide/introduction/vben.md

@ -26,7 +26,7 @@
## Browser Support
- **Local development** is recommended using the **latest version of Chrome**. **Versions below Chrome 80 are not supported**.
- **Local development** is recommended using the **latest version of Chrome**. **Tailwind CSS v4.0 is designed for Safari 16.4+, Chrome 111+, and Firefox 128+**.
- **Production environment** supports modern browsers, IE is not supported.

2
docs/src/guide/essentials/server.md

@ -98,7 +98,7 @@ VITE_GLOB_API_URL=https://mock-napi.vben.pro/api
::: tip 打包如何动态修改接口地址
`.env` 文件内的 `VITE_GLOB_*` 开头的变量会在打包的时候注入 `_app.config.js` 文件内。在 `dist/_app.config.js` 修改相应的接口地址后刷新页面即可,不需要在根据不同环境打包多次,一次打包可以用于多个不同接口环境的部署。
`.env` 文件内的 `VITE_GLOB_*` 开头的变量会在打包的时候注入 `_app-config-{version}-{hash}.js` 文件内。在 `dist/_app-config-{version}-{hash}.js` 修改相应的接口地址后刷新页面即可,不需要在根据不同环境打包多次,一次打包可以用于多个不同接口环境的部署。
:::

10
docs/src/guide/essentials/settings.md

@ -21,7 +21,7 @@
console.log(import.meta.env.VITE_PROT);
```
- 以 `VITE_GLOB_*` 开头的的变量,在打包的时候,会被加入 `_app.config.js`配置文件当中.
- 以 `VITE_GLOB_*` 开头的的变量,在打包的时候,会被加入 `_app-config-{version}-{hash}.js`配置文件当中.
:::
@ -86,9 +86,9 @@ VITE_ARCHIVER=true
## 生产环境动态配置
当在大仓根目录下,执行 `pnpm build`构建项目之后,会自动在对应的应用下生成 `dist/_app.config.js`文件并插入 `index.html`。
当在大仓根目录下,执行 `pnpm build`构建项目之后,会自动在对应的应用下生成 `dist/_app-config-{version}-{hash}.js`文件并插入 `index.html`。
`_app.config.js` 是一个动态配置文件,可以在项目构建之后,根据不同的环境动态修改配置。内容如下:
`_app-config-{version}-{hash}.js` 是一个动态配置文件,可以在项目构建之后,根据不同的环境动态修改配置。内容如下:
```ts
window._VBEN_ADMIN_PRO_APP_CONF_ = {
@ -103,11 +103,11 @@ Object.defineProperty(window, '_VBEN_ADMIN_PRO_APP_CONF_', {
### 作用
`_app.config.js` 用于项目在打包后,需要动态修改配置的需求,如接口地址。不用重新进行打包,可在打包后修改 /`dist/_app.config.js` 内的变量,刷新即可更新代码内的局部变量。这里使用`js`文件,是为了确保配置文件加载顺序保持在前面。
`_app-config-{version}-{hash}.js` 用于项目在打包后,需要动态修改配置的需求,如接口地址。不用重新进行打包,可在打包后修改 /`dist/_app-config-{version}-{hash}.js` 内的变量,刷新即可更新代码内的局部变量。这里使用`js`文件,是为了确保配置文件加载顺序保持在前面。
### 使用
想要获取 `_app.config.js` 内的变量,需要使用`@vben/hooks`提供的 `useAppConfig`方法。
想要获取 `_app-config-{version}-{hash}.js` 内的变量,需要使用`@vben/hooks`提供的 `useAppConfig`方法。
```ts
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);

2
docs/src/guide/introduction/quick-start.md

@ -10,7 +10,7 @@ outline: deep
在启动项目前,你需要确保你的环境满足以下要求:
- [Node.js](https://nodejs.org/en) 20.15.0 及以上版本,推荐使用 [fnm](https://github.com/Schniz/fnm) 、 [nvm](https://github.com/nvm-sh/nvm) 或者直接使用[pnpm](https://pnpm.io/cli/env) 进行版本管理。
- [Node.js](https://nodejs.org/en) 22.18.0 及以上版本,推荐使用 [fnm](https://github.com/Schniz/fnm) 、 [nvm](https://github.com/nvm-sh/nvm) 或者直接使用[pnpm](https://pnpm.io/cli/env) 进行版本管理。
- [Git](https://git-scm.com/) 任意版本。
验证你的环境是否满足以上要求,你可以通过以下命令查看版本:

2
docs/src/guide/introduction/vben.md

@ -26,7 +26,7 @@
## 浏览器支持
- **本地开发**推荐使用`Chrome 最新版`浏览器,**不支持**`Chrome 80`以下版本。
- **本地开发**推荐使用`Chrome 最新版`浏览器,**不支持**` Tailwind CSS v4.0 is designed for Safari 16.4+, Chrome 111+, and Firefox 128+。
- **生产环境**支持现代浏览器,不支持 IE。

6
internal/lint-configs/commitlint-config/index.d.ts

@ -0,0 +1,6 @@
// eslint-disable-next-line n/no-extraneous-import
import type { UserConfig } from '@commitlint/types';
declare const userConfig: UserConfig;
export default userConfig;

1
internal/lint-configs/commitlint-config/package.json

@ -18,6 +18,7 @@
"module": "./index.mjs",
"exports": {
".": {
"types": "./index.d.ts",
"import": "./index.mjs",
"default": "./index.mjs"
}

2
internal/lint-configs/eslint-config/package.json

@ -28,7 +28,6 @@
},
"dependencies": {
"@eslint/js": "catalog:",
"@typescript-eslint/eslint-plugin": "catalog:",
"@typescript-eslint/parser": "catalog:",
"@vben/oxlint-config": "workspace:*",
"eslint": "catalog:",
@ -36,7 +35,6 @@
"eslint-plugin-n": "catalog:",
"eslint-plugin-perfectionist": "catalog:",
"eslint-plugin-pnpm": "catalog:",
"eslint-plugin-unicorn": "catalog:",
"eslint-plugin-unused-imports": "catalog:",
"eslint-plugin-vue": "catalog:",
"eslint-plugin-yml": "catalog:",

1
internal/lint-configs/eslint-config/src/configs/index.ts

@ -5,6 +5,5 @@ export * from './node';
export * from './perfectionist';
export * from './pnpm';
export * from './typescript';
export * from './unicorn';
export * from './vue';
export * from './yaml';

2
internal/lint-configs/eslint-config/src/configs/node.ts

@ -32,7 +32,7 @@ export async function node(): Promise<Linter.Config[]> {
'error',
{
ignores: [],
version: '>=20.12.0',
version: '>=22.18.0',
},
],
'n/prefer-global/buffer': ['error', 'never'],

46
internal/lint-configs/eslint-config/src/configs/typescript.ts

@ -2,24 +2,17 @@ import type { Linter } from 'eslint';
import { interopDefault } from '../util';
const rulesCoveredByOxlint = new Set([
'@typescript-eslint/ban-ts-comment',
'@typescript-eslint/no-non-null-assertion',
'@typescript-eslint/no-unused-expressions',
'@typescript-eslint/no-unused-vars',
'@typescript-eslint/triple-slash-reference',
]);
/**
* @typescript-eslint 的规则已迁移到 oxlint(typescript 插件)。
* 这里仅保留 TS 解析器,供其它 eslint 插件(perfectionist、n 等)解析 TS/TSX 文件。
* 因不再有类型感知规则,已移除 parserOptions.project,eslint 解析更快。
*
* 注意:移除 @typescript-eslint 插件后,unused-imports/no-unused-vars 会退回核心实现,
* 无法识别 TS 类型签名里的形参(会误报)。故对 TS/TSX/Vue 统一关闭该规则,
* 未使用变量改由 oxlint 的 no-unused-vars(类型感知)负责。
*/
export async function typescript(): Promise<Linter.Config[]> {
const [pluginTs, parserTs] = await Promise.all([
interopDefault(import('@typescript-eslint/eslint-plugin')),
interopDefault(import('@typescript-eslint/parser')),
] as const);
const strictRules = Object.fromEntries(
Object.entries(pluginTs.configs.strict?.rules ?? {}).filter(
([ruleName]) => !rulesCoveredByOxlint.has(ruleName),
),
);
const parserTs = await interopDefault(import('@typescript-eslint/parser'));
return [
{
@ -27,30 +20,23 @@ export async function typescript(): Promise<Linter.Config[]> {
languageOptions: {
parser: parserTs,
parserOptions: {
createDefaultProgram: false,
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 'latest',
extraFileExtensions: ['.vue'],
jsxPragma: 'React',
project: './tsconfig.*.json',
sourceType: 'module',
},
},
plugins: {
'@typescript-eslint': pluginTs as any,
rules: {
'unused-imports/no-unused-vars': 'off',
},
},
{
// Vue `<script>` 的未使用变量同样交给 oxlint,避免核心规则误报 TS 类型签名形参
files: ['**/*.vue'],
rules: {
...pluginTs.configs['eslint-recommended']?.overrides?.[0]?.rules,
...strictRules,
// '@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],
'@typescript-eslint/consistent-type-definitions': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'unused-imports/no-unused-vars': 'off',
},
},

44
internal/lint-configs/eslint-config/src/configs/unicorn.ts

@ -1,44 +0,0 @@
import type { Linter } from 'eslint';
import { interopDefault } from '../util';
const rulesCoveredByOxlint = new Set([
'unicorn/consistent-function-scoping',
'unicorn/no-process-exit',
'unicorn/prefer-global-this',
'unicorn/prefer-module',
]);
export async function unicorn(): Promise<Linter.Config[]> {
const pluginUnicorn = await interopDefault(import('eslint-plugin-unicorn'));
const recommendedRules = Object.fromEntries(
Object.entries(pluginUnicorn.configs.recommended.rules ?? {}).filter(
([ruleName]) => !rulesCoveredByOxlint.has(ruleName),
),
);
return [
{
plugins: {
unicorn: pluginUnicorn,
},
rules: {
...recommendedRules,
'unicorn/better-regex': 'off',
'unicorn/consistent-destructuring': 'off',
'unicorn/expiring-todo-comments': 'off',
'unicorn/filename-case': 'off',
'unicorn/import-style': 'off',
'unicorn/no-array-for-each': 'off',
'unicorn/no-null': 'off',
'unicorn/no-useless-undefined': 'off',
'unicorn/prefer-at': 'off',
'unicorn/prefer-dom-node-text-content': 'off',
'unicorn/prefer-export-from': ['error', { ignoreUsedVariables: true }],
'unicorn/prefer-top-level-await': 'off',
'unicorn/prevent-abbreviations': 'off',
},
},
];
}

8
internal/lint-configs/eslint-config/src/configs/yaml.ts

@ -56,6 +56,11 @@ export async function yaml(): Promise<Linter.Config[]> {
{
order: [
'packages',
'publicHoistPattern',
'strictPeerDependencies',
'autoInstallPeers',
'dedupePeerDependents',
'verifyDepsBeforeRun',
'overrides',
'patchedDependencies',
'hoistPattern',
@ -63,6 +68,7 @@ export async function yaml(): Promise<Linter.Config[]> {
'catalogs',
'allowedDeprecatedVersions',
'allowBuilds',
'allowNonAppliedPatches',
'configDependencies',
'ignoredBuiltDependencies',
@ -78,7 +84,7 @@ export async function yaml(): Promise<Linter.Config[]> {
},
{
order: { type: 'asc' },
pathPattern: '.*',
pathPattern: '^.+$',
},
],
},

2
internal/lint-configs/eslint-config/src/index.ts

@ -8,7 +8,6 @@ import {
perfectionist,
pnpm,
typescript,
unicorn,
vue,
yaml,
} from './configs';
@ -31,7 +30,6 @@ async function defineConfig(config: FlatConfig[] = []) {
jsonc(),
node(),
perfectionist(),
unicorn(),
yaml(),
pnpm(),
...customConfig,

96
internal/lint-configs/oxfmt-config/src/index.ts

@ -2,13 +2,107 @@ import { defineConfig as defineOxfmtConfig } from 'oxfmt';
type OxfmtConfig = Parameters<typeof defineOxfmtConfig>[0];
/**
* oxfmt 配置文件,详见下方链接
* https://oxc.rs/docs/guide/usage/formatter/config-file-reference.html
*/
const oxfmtConfig: OxfmtConfig = defineOxfmtConfig({
/**
* 单行长度 oxfmt,适配 prettier 的 80
* Default:100
*/
printWidth: 80,
/**
* 缩进宽度
* Default:2
*/
tabWidth: 2,
/**
* Markdown、MDX、YAML 文件格式化包裹
* type: always | never | preserve
* Default: preserve
*/
proseWrap: 'never',
/**
* 结尾添加分号
* Default:true
*/
semi: true,
/**
* 使用单引号
* Default:false
*/
singleQuote: true,
/**
* 对象属性添加引号
* Default:as-needed
*/
quoteProps: 'as-needed',
/**
* 将多行元素的 > 放在最后一行的末尾,而不是单独放在下一行
* Default:false
*/
bracketSameLine: false,
/**
* 对象字面量的大括号间添加空格
* Default:true
*/
bracketSpacing: true,
/**
* 箭头函数参数总是使用括号
* type: always | avoid
* Default:always
*/
arrowParens: 'always',
/**
* 配置 package.json 排序,但是 oxfmt 不支持 pnpm-workspace
* 现使用 eslint 搭配 eslint-plugin-pnpm eslint-plugin-yml 支持 package.json 和 pnpm-workspace.yaml 但排序风格不太一致
* Default:true
*/
sortPackageJson: false,
/**
* 配置 import 排序,现在 使用 eslint-plugin-perfectionist,但是 oxfmt 不支持 export 等
* 并且 customGroups 不支持 ts-equals-import
* Default:false
*/
sortImports: false,
/**
* 多行结构中的后置逗号
* Default:all
*/
trailingComma: 'all',
/**
* 行尾换行符
* type: lf | crlf | cr
* Default: lf
*/
endOfLine: 'lf',
/**
* 在文件最后插入一个换行
* Default:true
*/
insertFinalNewline: true,
/**
* 控制格式化文件中例如,CSS-in-JS 或 JS-in-Vue 等
* Default:auto
*/
embeddedLanguageFormatting: 'auto',
/**
* Vue/HTML/Angular/Handlebars 的空白敏感度(oxfmt 现会格式化 <template>)
* type: css | strict | ignore
* Default:css
*/
htmlWhitespaceSensitivity: 'css',
/**
* 暂时关闭,改动较多,后续可以考虑开启,支持 vue 但与现有的 eslint-plugin-better-tailwindcss 格式化会冲突
* eslint-plugin-better-tailwindcss配置在 oxlint,在 vue文 件暂时不生效,ts等正常
* Default:关闭
*/
// sortTailwindcss: {
// functions: ['clsx', 'cn', 'cva', 'tw'],
// stylesheet: './internal/tailwind-config/src/theme.css',
// preserveWhitespace: true,
// },
overrides: [
{
files: [
@ -23,6 +117,8 @@ const oxfmtConfig: OxfmtConfig = defineOxfmtConfig({
],
options: {
trailingComma: 'none',
quoteProps: 'preserve',
singleQuote: false,
},
},
],

9
internal/lint-configs/oxlint-config/src/configs/overrides.ts

@ -9,6 +9,15 @@ const overrides: OxlintConfig = {
'typescript/triple-slash-reference': 'off',
},
},
{
// 这些 @typescript-eslint 规则此前不作用于 .vue(旧 eslint glob 不含 .vue)。
// Vue 组件惯用 `interface Props extends XxxProps {}` 声明 props,保持迁移前行为放行。
files: ['*.vue', '**/*.vue'],
rules: {
'typescript/no-empty-object-type': 'off',
'typescript/no-unsafe-function-type': 'off',
},
},
{
files: [
'**/__tests__/**/*.js',

19
internal/lint-configs/oxlint-config/src/configs/plugins.ts

@ -1,7 +1,24 @@
import type { OxlintConfig } from 'oxlint';
const plugins: OxlintConfig = {
plugins: ['import', 'node', 'oxc', 'typescript', 'unicorn', 'vitest', 'vue'],
/**
* oxlint 支持的插件,将默认开启的也显示配置
* type: eslint | react | unicorn | typescript | oxc |
* import | jsdoc | jest | vitest | jsx-a11y | nextjs |
* react-perf | promise | node | vue
*
* Default: eslint,typescript,unicorn,oxc
*/
plugins: [
'eslint',
'import',
'node',
'oxc',
'typescript',
'unicorn',
'vitest',
'vue',
],
};
export { plugins };

38
internal/lint-configs/oxlint-config/src/configs/typescript.ts

@ -2,25 +2,49 @@ import type { OxlintConfig } from 'oxlint';
const typescript: OxlintConfig = {
rules: {
// —— 从 @typescript-eslint strict 预设迁移而来(非类型感知,oxlint 原生支持)——
// no-array-constructor / no-useless-constructor 已由 javascript 配置以核心规则覆盖。
'typescript/ban-ts-comment': 'error',
// Keep the first type-aware rollout conservative. These rules currently
// produce high-volume diagnostics and need file-by-file cleanup later.
'typescript/no-duplicate-enum-values': 'error',
'typescript/no-dynamic-delete': 'error',
'typescript/no-empty-object-type': 'error',
'typescript/no-extra-non-null-assertion': 'error',
'typescript/no-extraneous-class': 'error',
'typescript/no-invalid-void-type': 'error',
'typescript/no-misused-new': 'error',
'typescript/no-non-null-asserted-nullish-coalescing': 'error',
'typescript/no-non-null-asserted-optional-chain': 'error',
'typescript/no-non-null-assertion': 'error',
'typescript/no-require-imports': 'error',
'typescript/no-this-alias': 'error',
'typescript/no-unnecessary-type-constraint': 'error',
'typescript/no-unsafe-declaration-merging': 'error',
'typescript/no-unsafe-function-type': 'error',
'typescript/no-var-requires': 'error',
'typescript/no-wrapper-object-types': 'error',
'typescript/prefer-as-const': 'error',
'typescript/prefer-literal-enum-member': 'error',
'typescript/prefer-namespace-keyword': 'error',
'typescript/triple-slash-reference': 'error',
'typescript/unified-signatures': 'error',
'typescript/await-thenable': 'off',
'typescript/consistent-return': 'off',
'typescript/no-base-to-string': 'off',
'typescript/no-duplicate-type-constituents': 'off',
'typescript/no-floating-promises': 'off',
'typescript/no-misused-spread': 'off',
'typescript/no-non-null-assertion': 'error',
'typescript/no-redundant-type-constituents': 'off',
'typescript/no-unnecessary-boolean-literal-compare': 'off',
'typescript/no-unnecessary-type-assertion': 'off',
'typescript/no-unnecessary-type-arguments': 'off',
'typescript/no-unnecessary-template-expression': 'off',
'typescript/no-unnecessary-type-arguments': 'off',
'typescript/no-unnecessary-type-assertion': 'off',
'typescript/no-unnecessary-type-conversion': 'off',
'typescript/no-unnecessary-type-parameters': 'off',
'typescript/no-unsafe-enum-comparison': 'off',
'typescript/no-unsafe-type-assertion': 'off',
'typescript/no-var-requires': 'error',
'typescript/no-useless-default-assignment': 'off',
'typescript/restrict-template-expressions': 'off',
'typescript/triple-slash-reference': 'error',
'typescript/unbound-method': 'off',
},
};

117
internal/lint-configs/oxlint-config/src/configs/unicorn.ts

@ -8,6 +8,123 @@ const unicorn: OxlintConfig = {
'unicorn/no-useless-spread': 'off',
'unicorn/prefer-global-this': 'off',
'unicorn/prefer-module': 'error',
'unicorn/catch-error-name': 'error',
'unicorn/consistent-assert': 'error',
'unicorn/consistent-date-clone': 'error',
'unicorn/consistent-empty-array-spread': 'error',
'unicorn/consistent-existence-index-check': 'error',
'unicorn/consistent-template-literal-escape': 'error',
'unicorn/empty-brace-spaces': 'error',
'unicorn/error-message': 'error',
'unicorn/escape-case': 'error',
'unicorn/explicit-length-check': 'error',
'unicorn/new-for-builtins': 'error',
'unicorn/no-abusive-eslint-disable': 'error',
'unicorn/no-accessor-recursion': 'error',
'unicorn/no-anonymous-default-export': 'error',
'unicorn/no-array-callback-reference': 'error',
'unicorn/no-array-method-this-argument': 'error',
'unicorn/no-array-reduce': 'error',
'unicorn/no-array-reverse': 'error',
'unicorn/no-array-sort': 'error',
'unicorn/no-await-expression-member': 'error',
'unicorn/no-await-in-promise-methods': 'error',
'unicorn/no-console-spaces': 'error',
'unicorn/no-document-cookie': 'error',
'unicorn/no-empty-file': 'error',
'unicorn/no-hex-escape': 'error',
// oxlint 实现较 eslint 更严格,会误报既有代码,暂关闭
'unicorn/no-immediate-mutation': 'off',
'unicorn/no-instanceof-builtins': 'error',
'unicorn/no-invalid-fetch-options': 'error',
'unicorn/no-invalid-remove-event-listener': 'error',
'unicorn/no-lonely-if': 'error',
'unicorn/no-magic-array-flat-depth': 'error',
'unicorn/no-negated-condition': 'error',
'unicorn/no-negation-in-equality-check': 'error',
'unicorn/no-nested-ternary': 'error',
'unicorn/no-new-array': 'error',
'unicorn/no-new-buffer': 'error',
'unicorn/no-object-as-default-parameter': 'error',
'unicorn/no-static-only-class': 'error',
'unicorn/no-thenable': 'error',
'unicorn/no-this-assignment': 'error',
'unicorn/no-typeof-undefined': 'error',
'unicorn/no-unnecessary-array-flat-depth': 'error',
'unicorn/no-unnecessary-array-splice-count': 'error',
'unicorn/no-unnecessary-await': 'error',
'unicorn/no-unnecessary-slice-end': 'error',
'unicorn/no-unreadable-array-destructuring': 'error',
'unicorn/no-unreadable-iife': 'error',
'unicorn/no-useless-collection-argument': 'error',
'unicorn/no-useless-error-capture-stack-trace': 'error',
'unicorn/no-useless-fallback-in-spread': 'error',
'unicorn/no-useless-iterator-to-array': 'error',
'unicorn/no-useless-length-check': 'error',
'unicorn/no-useless-promise-resolve-reject': 'error',
'unicorn/no-useless-switch-case': 'error',
'unicorn/no-zero-fractions': 'error',
'unicorn/number-literal-case': 'error',
'unicorn/numeric-separators-style': 'error',
'unicorn/prefer-add-event-listener': 'error',
'unicorn/prefer-array-find': 'error',
'unicorn/prefer-array-flat': 'error',
'unicorn/prefer-array-flat-map': 'error',
'unicorn/prefer-array-index-of': 'error',
'unicorn/prefer-array-some': 'error',
'unicorn/prefer-bigint-literals': 'error',
'unicorn/prefer-blob-reading-methods': 'error',
'unicorn/prefer-class-fields': 'error',
'unicorn/prefer-classlist-toggle': 'error',
'unicorn/prefer-code-point': 'error',
'unicorn/prefer-date-now': 'error',
'unicorn/prefer-default-parameters': 'error',
'unicorn/prefer-dom-node-append': 'error',
'unicorn/prefer-dom-node-dataset': 'error',
'unicorn/prefer-dom-node-remove': 'error',
'unicorn/prefer-event-target': 'error',
'unicorn/prefer-export-from': ['error', { checkUsedVariables: false }],
'unicorn/prefer-includes': 'error',
'unicorn/prefer-keyboard-event-key': 'error',
'unicorn/prefer-logical-operator-over-ternary': 'error',
'unicorn/prefer-math-min-max': 'error',
'unicorn/prefer-math-trunc': 'error',
'unicorn/prefer-modern-dom-apis': 'error',
'unicorn/prefer-modern-math-apis': 'error',
'unicorn/prefer-native-coercion-functions': 'error',
'unicorn/prefer-negative-index': 'error',
'unicorn/prefer-node-protocol': 'error',
'unicorn/prefer-number-properties': 'error',
'unicorn/prefer-object-from-entries': 'error',
'unicorn/prefer-optional-catch-binding': 'error',
'unicorn/prefer-prototype-methods': 'error',
'unicorn/prefer-query-selector': 'error',
'unicorn/prefer-reflect-apply': 'error',
'unicorn/prefer-regexp-test': 'error',
'unicorn/prefer-response-static-json': 'error',
'unicorn/prefer-set-has': 'error',
'unicorn/prefer-set-size': 'error',
'unicorn/prefer-single-call': 'error',
'unicorn/prefer-spread': 'error',
'unicorn/prefer-string-raw': 'error',
'unicorn/prefer-string-replace-all': 'error',
'unicorn/prefer-string-slice': 'error',
'unicorn/prefer-string-starts-ends-with': 'error',
'unicorn/prefer-string-trim-start-end': 'error',
// oxlint 实现较 eslint 更严格(含 cloneDeep),暂关闭以保持迁移前行为
'unicorn/prefer-structured-clone': 'off',
'unicorn/prefer-ternary': 'error',
'unicorn/prefer-type-error': 'error',
'unicorn/relative-url-style': 'error',
'unicorn/require-array-join-separator': 'error',
'unicorn/require-module-attributes': 'error',
'unicorn/require-module-specifiers': 'error',
'unicorn/require-number-to-fixed-digits-argument': 'error',
'unicorn/switch-case-braces': 'error',
'unicorn/switch-case-break-position': 'error',
'unicorn/text-encoding-identifier-case': 'error',
'unicorn/throw-new-error': 'error',
},
};

1
internal/vite-config/package.json

@ -48,7 +48,6 @@
"@vitejs/plugin-vue-jsx": "catalog:",
"dayjs": "catalog:",
"dotenv": "catalog:",
"rolldown": "catalog:",
"rollup-plugin-visualizer": "catalog:",
"sass": "catalog:",
"sass-embedded": "catalog:",

5
internal/vite-config/src/plugins/archiver.ts

@ -6,7 +6,7 @@ import fs from 'node:fs';
import fsp from 'node:fs/promises';
import { join } from 'node:path';
import archiver from 'archiver';
import { ZipArchive } from 'archiver';
export const viteArchiverPlugin = (
options: ArchiverPluginOptions = {},
@ -49,7 +49,8 @@ async function zipFolder(
): Promise<void> {
return new Promise((resolve, reject) => {
const output = fs.createWriteStream(outputPath);
const archive = archiver('zip', {
const archive = new ZipArchive({
zlib: { level: 9 }, // 设置压缩级别为 9 以实现最高压缩率
});

10
internal/vite-config/src/plugins/extra-app-config.ts

@ -13,7 +13,7 @@ interface PluginOptions {
root: string;
}
const GLOBAL_CONFIG_FILE_NAME = '_app.config.js';
const GLOBAL_CONFIG_FILE_NAME = '_app-config';
const VBEN_ADMIN_PRO_APP_CONF = '_VBEN_ADMIN_PRO_APP_CONF_';
/**
@ -27,6 +27,7 @@ async function viteExtraAppConfigPlugin({
}: PluginOptions): Promise<PluginOption | undefined> {
let publicPath: string;
let source: string;
let hash: string;
if (!isBuild) {
return;
@ -38,11 +39,12 @@ async function viteExtraAppConfigPlugin({
async configResolved(config) {
publicPath = ensureTrailingSlash(config.base);
source = await getConfigSource();
hash = generatorContentHash(source, 8);
},
async generateBundle() {
try {
this.emitFile({
fileName: GLOBAL_CONFIG_FILE_NAME,
fileName: `${GLOBAL_CONFIG_FILE_NAME}-${version}-${hash}.js`,
source,
type: 'asset',
});
@ -58,9 +60,7 @@ async function viteExtraAppConfigPlugin({
},
name: 'vite:extra-app-config',
async transformIndexHtml(html) {
const hash = `v=${version}-${generatorContentHash(source, 8)}`;
const appConfigSrc = `${publicPath}${GLOBAL_CONFIG_FILE_NAME}?${hash}`;
const appConfigSrc = `${publicPath}${GLOBAL_CONFIG_FILE_NAME}-${version}-${hash}.js`;
return {
html,

14
internal/vite-config/src/plugins/license.ts

@ -1,8 +1,3 @@
import type {
NormalizedOutputOptions,
OutputBundle,
OutputChunk,
} from 'rolldown';
import type { PluginOption } from 'vite';
import { EOL } from 'node:os';
@ -13,7 +8,6 @@ import { dateUtil, readPackageJSON } from '@vben/node-utils';
* 用于注入版权信息
* @returns
*/
async function viteLicensePlugin(
root = process.cwd(),
): Promise<PluginOption | undefined> {
@ -27,7 +21,7 @@ async function viteLicensePlugin(
apply: 'build',
enforce: 'post',
generateBundle: {
handler: (_options: NormalizedOutputOptions, bundle: OutputBundle) => {
handler(_options, bundle) {
const date = dateUtil().format('YYYY-MM-DD ');
const copyrightText = `/*!
* Vben Admin
@ -44,13 +38,11 @@ async function viteLicensePlugin(
for (const [, fileContent] of Object.entries(bundle)) {
if (fileContent.type === 'chunk' && fileContent.isEntry) {
const chunkContent = fileContent as OutputChunk;
// 插入版权信息
const content = chunkContent.code;
const content = fileContent.code;
const updatedContent = `${copyrightText}${EOL}${content}`;
// 更新bundle
(fileContent as OutputChunk).code = updatedContent;
fileContent.code = updatedContent;
}
}
},

2
internal/vite-config/tsconfig.json

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

43
lefthook.yml

@ -42,28 +42,27 @@
# runner: go run
pre-commit:
parallel: true
commands:
code-workspace:
run: pnpm vsh code-workspace --auto-commit
lint-md:
run: pnpm oxfmt {staged_files}
glob: '*.md'
lint-vue:
run: pnpm oxfmt {staged_files} && pnpm oxlint --fix --no-error-on-unmatched-pattern {staged_files} && pnpm eslint --cache --fix {staged_files} && pnpm stylelint --fix --allow-empty-input {staged_files}
glob: '*.vue'
lint-js:
run: pnpm oxfmt {staged_files} && pnpm oxlint --fix --no-error-on-unmatched-pattern {staged_files} && pnpm eslint --cache --fix {staged_files}
glob: '*.{js,jsx,ts,tsx}'
lint-style:
run: pnpm oxfmt {staged_files} && pnpm stylelint --fix --allow-empty-input {staged_files}
glob: '*.{scss,less,styl,html,vue,css}'
lint-package:
run: pnpm oxfmt {staged_files}
glob: 'package.json'
lint-json:
run: pnpm oxfmt {staged_files}
glob: '{!(package)*.json,*.code-snippets,.!(browserslist)*rc}'
# 串行执行(不设 parallel),避免与 check:type 同时运行导致低配机内存/CPU 瞬时飙升。
# lint 仅检查暂存文件并自动修复后回填,大幅降低负载;check:type 仍需全量。
jobs:
- name: oxlint
glob: '*.{js,jsx,ts,tsx,vue,cjs,mjs,cts,mts}'
run: pnpm exec oxlint --fix --type-aware --threads=4 --no-error-on-unmatched-pattern {staged_files}
stage_fixed: true
- name: oxfmt
glob: '*.{js,jsx,ts,tsx,vue,cjs,mjs,cts,mts,json,jsonc}'
run: pnpm exec oxfmt --threads=4 --no-error-on-unmatched-pattern {staged_files}
stage_fixed: true
- name: eslint
glob: '*.{js,jsx,ts,tsx,vue,cjs,mjs,cts,mts}'
run: pnpm exec eslint --fix {staged_files}
stage_fixed: true
- name: stylelint
glob: '*.{vue,css,less,scss}'
run: pnpm exec stylelint --fix {staged_files}
stage_fixed: true
- name: checkType
run: pnpm check:type
post-merge:
commands:

11
package.json

@ -53,17 +53,17 @@
"dev:play": "pnpm -F @vben/playground run dev",
"format": "vsh lint --format",
"lint": "vsh lint",
"postinstall": "pnpm -r run stub --if-present",
"postinstall": "pnpm -r run --if-present stub",
"preinstall": "npx only-allow pnpm",
"prepare": "is-ci || pnpm exec lefthook install",
"preview": "turbo-run preview",
"publint": "vsh publint",
"reinstall": "pnpm clean --del-lock && pnpm install",
"reinstall": "pnpm run clean -- --del-lock && pnpm install",
"test:unit": "vitest run --dom",
"test:e2e": "turbo run test:e2e",
"update:deps": "npx taze -r -w",
"version": "pnpm exec changeset version && pnpm install --no-frozen-lockfile",
"catalog": "pnpx codemod pnpm/catalog"
"catalog": "pnpm dlx codemod pnpm/catalog"
},
"devDependencies": {
"@changesets/changelog-github": "catalog:",
@ -84,6 +84,7 @@
"@vitejs/plugin-vue-jsx": "catalog:",
"cross-env": "catalog:",
"cspell": "catalog:",
"eslint": "catalog:",
"happy-dom": "catalog:",
"is-ci": "catalog:",
"lefthook": "catalog:",
@ -103,7 +104,7 @@
},
"engines": {
"node": "^22.18.0 || ^24.0.0",
"pnpm": ">=10.0.0"
"pnpm": ">=11.0.0"
},
"packageManager": "pnpm@10.33.4"
"packageManager": "pnpm@11.7.0"
}

2
packages/@core/base/icons/package.json

@ -36,7 +36,7 @@
},
"dependencies": {
"@iconify/vue": "catalog:",
"lucide-vue-next": "catalog:",
"@lucide/vue": "catalog:",
"vue": "catalog:"
}
}

3
packages/@core/base/icons/src/lucide.ts

@ -35,7 +35,6 @@ export {
EyeOff,
FoldHorizontal,
Fullscreen,
Github,
Grid,
Grip,
GripVertical,
@ -93,4 +92,4 @@ export {
Unlink2,
UserRoundPen,
X,
} from 'lucide-vue-next';
} from '@lucide/vue';

3
packages/@core/base/shared/src/cache/local-storage-driver.ts

@ -18,8 +18,7 @@ class LocalStorageDriver implements IStorageDriver {
storageType = 'localStorage',
}: LocalStorageDriverOptions = {}) {
if (typeof window === 'undefined') {
// eslint-disable-next-line unicorn/prefer-type-error -- not a type check, it's an environment check
throw new Error(
throw new TypeError(
'LocalStorageDriver is not available in non-browser environments. Use MemoryStorageDriver instead.',
);
}

18
packages/@core/base/shared/src/utils/__tests__/update-css-variables.test.ts

@ -28,3 +28,21 @@ it('updateCSSVariables should update CSS variables in :root selector', () => {
updatedStyleContent?.includes('fontSize: 16px;'),
).toBe(true);
});
it('updateCSSVariables should support a custom selector', () => {
document.head.innerHTML = `<style id="tdesign-styles"></style>`;
// 使用自定义选择器(如 TDesign 的 theme-mode 选择器)更新 CSS 变量
updateCSSVariables(
{ '--td-brand-color': 'rgb(0, 82, 217)' },
'tdesign-styles',
":root[theme-mode='dark']",
);
const styleElement = document.querySelector('#tdesign-styles');
const content = styleElement?.textContent ?? '';
// 选择器与变量都应正确写入
expect(content.startsWith(":root[theme-mode='dark'] {")).toBe(true);
expect(content.includes('--td-brand-color: rgb(0, 82, 217);')).toBe(true);
});

2
packages/@core/base/shared/src/utils/index.ts

@ -16,6 +16,6 @@ export * from './unique';
export * from './update-css-variables';
export * from './util';
export * from './window';
export { get, isEqual, set } from 'es-toolkit/compat';
export { debounce, get, isEqual, set } from 'es-toolkit/compat';
// export { cloneDeep } from 'es-toolkit/object';
export { default as cloneDeep } from 'lodash.clonedeep';

7
packages/@core/base/shared/src/utils/update-css-variables.ts

@ -1,10 +1,15 @@
/**
* 更新 CSS 变量的函数
* @param variables 要更新的 CSS 变量与其新值的映射
* @param id 内联样式表的 id,便于复用与覆盖
* @param selector CSS 变量挂载的选择器,默认 `:root`。
* 对于像 TDesign 这种将变量定义在 `:root[theme-mode='dark']` 等更高优先级选择器下的组件库,
* 需要传入相同(或更高)优先级的选择器才能正确覆盖。
*/
function updateCSSVariables(
variables: { [key: string]: string },
id = '__vben-styles__',
selector = ':root',
): void {
// 获取或创建内联样式表元素
const styleElement =
@ -13,7 +18,7 @@ function updateCSSVariables(
styleElement.id = id;
// 构建要更新的 CSS 变量的样式文本
let cssText = ':root {';
let cssText = `${selector} {`;
for (const key in variables) {
if (Object.prototype.hasOwnProperty.call(variables, key)) {
cssText += `${key}: ${variables[key]};`;

9
packages/@core/base/typings/src/app.d.ts

@ -10,12 +10,17 @@ type LayoutType =
type ThemeModeType = 'auto' | 'dark' | 'light';
/**
* 偏好设置按钮位置
* 按钮位置
* user-dropdown 用户的下拉弹出框中
* fixed 固定在右侧
* header 顶栏
* auto 自动
*/
type PreferencesButtonPositionType = 'auto' | 'fixed' | 'header';
type PreferencesButtonPositionType =
| 'auto'
| 'fixed'
| 'header'
| 'user-dropdown';
type BuiltinThemeType =
| 'custom'

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

Loading…
Cancel
Save