diff --git a/.changeset/.prettierrc.json b/.changeset/.prettierrc.json
new file mode 100644
index 00000000..544138be
--- /dev/null
+++ b/.changeset/.prettierrc.json
@@ -0,0 +1,3 @@
+{
+ "singleQuote": true
+}
diff --git a/.changeset/fancy-ears-walk.md b/.changeset/fancy-ears-walk.md
new file mode 100644
index 00000000..f73cb596
--- /dev/null
+++ b/.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
diff --git a/.gitattributes b/.gitattributes
index d4e5bd3e..775caa34 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -4,8 +4,18 @@
* text=auto eol=lf
# Declare files that will always have CRLF line endings on checkout.
-*.{cmd,[cC][mM][dD]} text eol=crlf
-*.{bat,[bB][aA][tT]} text eol=crlf
+*.cmd text eol=crlf
+*.[cC][mM][dD] text eol=crlf
+*.bat text eol=crlf
+*.[bB][aA][tT] text eol=crlf
# Denote all files that are truly binary and should not be modified.
-*.{ico,png,jpg,jpeg,gif,webp,svg,woff,woff2} binary
\ No newline at end of file
+*.ico binary
+*.png binary
+*.jpg binary
+*.jpeg binary
+*.gif binary
+*.webp binary
+*.svg binary
+*.woff binary
+*.woff2 binary
\ No newline at end of file
diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml
index 35fa41c8..445f30be 100644
--- a/.github/actions/setup-node/action.yml
+++ b/.github/actions/setup-node/action.yml
@@ -6,10 +6,10 @@ runs:
using: 'composite'
steps:
- name: Install pnpm
- uses: pnpm/action-setup@v4
+ uses: pnpm/action-setup@v6
- name: Install Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@v6
with:
node-version-file: .node-version
cache: 'pnpm'
diff --git a/.github/config.yml b/.github/config.yml
index 52454840..248da6a5 100644
--- a/.github/config.yml
+++ b/.github/config.yml
@@ -21,12 +21,10 @@ newPRWelcomeComment: |
firstPRMergeComment: >
Thanks for your contribution! 🎉🎉🎉
-
# Comment to be posted to on first time issues
newIssueWelcomeComment: >
Thanks for opening your first issue! Be sure to follow the issue template and provide every bit of information to help the developers!
-
# *OPTIONAL* default titles to check against for lack of descriptiveness
# MUST BE ALL LOWERCASE
requestInfoDefaultTitles:
@@ -36,4 +34,3 @@ requestInfoDefaultTitles:
# *Required* Comment to reply with
requestInfoReplyComment: >
Thanks for filing this issue/PR! It would be much appreciated if you could provide us with more information so we can effectively analyze the situation in context.
-
diff --git a/.github/contributing.md b/.github/contributing.md
index f22370f3..304c5192 100644
--- a/.github/contributing.md
+++ b/.github/contributing.md
@@ -19,11 +19,9 @@ Project maintainers have the right and responsibility to remove, edit, or reject
- Checkout a topic branch from the relevant branch, e.g. main, and merge back against that branch.
- If adding a new feature:
-
- Provide a convincing reason to add this feature. Ideally, you should open a suggestion issue first and have it approved before working on it.
- If fixing bug:
-
- Provide a detailed description of the bug in the PR. Live demo preferred.
- It's OK to have multiple small commits as you work on the PR - GitHub can automatically squash them before merging.
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index d9f48e4a..b81bc4ad 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -30,7 +30,7 @@ jobs:
- windows-latest
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
with:
fetch-depth: 0
diff --git a/.github/workflows/changeset-version.yml b/.github/workflows/changeset-version.yml
index 373d9af2..32421e26 100644
--- a/.github/workflows/changeset-version.yml
+++ b/.github/workflows/changeset-version.yml
@@ -25,7 +25,7 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
with:
fetch-depth: 0
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 00750949..254ffd8e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -28,12 +28,12 @@ jobs:
timeout-minutes: 20
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Install pnpm
- uses: pnpm/action-setup@v4
+ uses: pnpm/action-setup@v6
with:
run_install: false
@@ -67,7 +67,7 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
with:
fetch-depth: 0
@@ -90,7 +90,7 @@ jobs:
- windows-latest
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
with:
fetch-depth: 0
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 69a990f0..4824318e 100644
--- a/.github/workflows/codeql.yml
+++ b/.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@v4
+ 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}}'
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index b4afdd93..8c12d408 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
with:
fetch-depth: 0
@@ -30,7 +30,7 @@ jobs:
run: pnpm build:play
- name: Sync Playground files
- uses: SamKirkland/FTP-Deploy-Action@v4.3.5
+ uses: SamKirkland/FTP-Deploy-Action@v4.4.0
with:
server: ${{ secrets.PRO_FTP_HOST }}
username: ${{ secrets.WEB_PLAYGROUND_FTP_ACCOUNT }}
@@ -43,7 +43,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
with:
fetch-depth: 0
@@ -54,7 +54,7 @@ jobs:
run: pnpm build:docs
- name: Sync Docs files
- uses: SamKirkland/FTP-Deploy-Action@v4.3.5
+ uses: SamKirkland/FTP-Deploy-Action@v4.4.0
with:
server: ${{ secrets.PRO_FTP_HOST }}
username: ${{ secrets.WEBSITE_FTP_ACCOUNT }}
@@ -67,7 +67,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
with:
fetch-depth: 0
@@ -85,7 +85,7 @@ jobs:
run: pnpm run build:antd
- name: Sync files
- uses: SamKirkland/FTP-Deploy-Action@v4.3.5
+ uses: SamKirkland/FTP-Deploy-Action@v4.4.0
with:
server: ${{ secrets.PRO_FTP_HOST }}
username: ${{ secrets.WEB_ANTD_FTP_ACCOUNT }}
@@ -98,7 +98,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
with:
fetch-depth: 0
@@ -116,7 +116,7 @@ jobs:
run: pnpm run build:ele
- name: Sync files
- uses: SamKirkland/FTP-Deploy-Action@v4.3.5
+ uses: SamKirkland/FTP-Deploy-Action@v4.4.0
with:
server: ${{ secrets.PRO_FTP_HOST }}
username: ${{ secrets.WEB_ELE_FTP_ACCOUNT }}
@@ -129,7 +129,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
with:
fetch-depth: 0
@@ -147,7 +147,7 @@ jobs:
run: pnpm run build:naive
- name: Sync files
- uses: SamKirkland/FTP-Deploy-Action@v4.3.5
+ uses: SamKirkland/FTP-Deploy-Action@v4.4.0
with:
server: ${{ secrets.PRO_FTP_HOST }}
username: ${{ secrets.WEB_NAIVE_FTP_ACCOUNT }}
diff --git a/.github/workflows/draft.yml b/.github/workflows/draft.yml
index 20b4148a..babca2d3 100644
--- a/.github/workflows/draft.yml
+++ b/.github/workflows/draft.yml
@@ -20,6 +20,6 @@ jobs:
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest
steps:
- - uses: release-drafter/release-drafter@v6
+ - uses: release-drafter/release-drafter@v7
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/issue-close-require.yml b/.github/workflows/issue-close-require.yml
index 18c007bf..794292f4 100644
--- a/.github/workflows/issue-close-require.yml
+++ b/.github/workflows/issue-close-require.yml
@@ -19,7 +19,7 @@ jobs:
steps:
# 关闭未活动的 Issues
- name: Close Inactive Issues
- uses: actions/stale@v9
+ uses: actions/stale@v11
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.
diff --git a/.github/workflows/issue-labeled.yml b/.github/workflows/issue-labeled.yml
index 2feda4ef..7a461e3b 100644
--- a/.github/workflows/issue-labeled.yml
+++ b/.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 }}
diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml
index bd73d533..a12227ef 100644
--- a/.github/workflows/lock.yml
+++ b/.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'
diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml
index 7f524f90..d87a7011 100644
--- a/.github/workflows/release-tag.yml
+++ b/.github/workflows/release-tag.yml
@@ -3,78 +3,48 @@ 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: [20]
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
with:
fetch-depth: 0
- # - name: Checkout code
- # uses: actions/checkout@v4
- # 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@v6
+ - uses: release-drafter/release-drafter@v7
with:
version: ${{ steps.version.outputs.version }}
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.
diff --git a/.github/workflows/semantic-pull-request.yml b/.github/workflows/semantic-pull-request.yml
index db8e9e09..02db08de 100644
--- a/.github/workflows/semantic-pull-request.yml
+++ b/.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]).+$
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index 991e1331..a6fd1c0e 100644
--- a/.github/workflows/stale.yml
+++ b/.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@v11
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'
diff --git a/.gitignore b/.gitignore
index c2a8a771..3a7e7ced 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,7 +22,8 @@ yarn.lock
package-lock.json
.VSCodeCounter
**/backend-mock/data
-
+.omx
+.pnpm-store
# local env files
.env.local
.env.*.local
@@ -49,3 +50,14 @@ vite.config.ts.*
*.sln
*.sw?
.history
+.cursor
+
+# AI
+.agent
+.agents
+.claude
+.codex
+skills-lock.json
+.atomcode
+datalog
+.playwright-mcp
diff --git a/.node-version b/.node-version
index ee5c2446..b832e400 100644
--- a/.node-version
+++ b/.node-version
@@ -1 +1 @@
-22.1.0
+24.16.0
diff --git a/.npmrc b/.npmrc
index 21147aff..7549542d 100644
--- a/.npmrc
+++ b/.npmrc
@@ -1,13 +1 @@
-registry = "https://registry.npmmirror.com"
-public-hoist-pattern[]=lefthook
-public-hoist-pattern[]=eslint
-public-hoist-pattern[]=prettier
-public-hoist-pattern[]=prettier-plugin-tailwindcss
-public-hoist-pattern[]=stylelint
-public-hoist-pattern[]=*postcss*
-public-hoist-pattern[]=@commitlint/*
-public-hoist-pattern[]=czg
-
-strict-peer-dependencies=false
-auto-install-peers=true
-dedupe-peer-dependents=true
+registry=https://registry.npmmirror.com
diff --git a/.prettierignore b/.prettierignore
deleted file mode 100644
index d0b0ca13..00000000
--- a/.prettierignore
+++ /dev/null
@@ -1,18 +0,0 @@
-dist
-dev-dist
-.local
-.output.js
-node_modules
-.nvmrc
-coverage
-CODEOWNERS
-.nitro
-.output
-
-
-**/*.svg
-**/*.sh
-
-public
-.npmrc
-*-lock.yaml
diff --git a/.prettierrc.mjs b/.prettierrc.mjs
deleted file mode 100644
index 3e25d2cf..00000000
--- a/.prettierrc.mjs
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from '@vben/prettier-config';
diff --git a/.stylelintignore b/.stylelintignore
index f4b2db2c..3adb33b2 100644
--- a/.stylelintignore
+++ b/.stylelintignore
@@ -2,3 +2,7 @@ dist
public
__tests__
coverage
+.codex
+.claude
+.agent
+.agents
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index e8dc9ed9..d4e28662 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -2,14 +2,18 @@
"recommendations": [
// Vue 3 的语言支持
"Vue.volar",
- // 将 ESLint JavaScript 集成到 VS Code 中。
+ // 将 eslint 集成到 VS Code 中。
"dbaeumer.vscode-eslint",
+ // 将 oxlint 集成到 VS Code 中。
+ "oxc.oxc-vscode",
// Visual Studio Code 的官方 Stylelint 扩展
"stylelint.vscode-stylelint",
- // 使用 Prettier 的代码格式化程序
- "esbenp.prettier-vscode",
+ // 使用 oxfmt 的代码格式化程序
+ "oxc.oxc-vscode",
// 支持 dotenv 文件语法
"mikestead.dotenv",
+ // YAML 语言支持,供 ESLint 校验 pnpm-workspace.yaml 等文件
+ "redhat.vscode-yaml",
// 源代码的拼写检查器
"streetsidesoftware.code-spell-checker",
// Tailwind CSS 的官方 VS Code 插件
diff --git a/.vscode/settings.json b/.vscode/settings.json
index f38c4278..993b5203 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,5 +1,6 @@
{
- "tailwindCSS.experimental.configFile": "internal/tailwind-config/src/index.ts",
+ "tailwindCSS.experimental.configFile": "internal/tailwind-config/src/theme.css",
+ "tailwindCSS.lint.suggestCanonicalClasses": "ignore",
// workbench
"workbench.list.smoothScrolling": true,
"workbench.startupEditor": "newUntitledFile",
@@ -31,39 +32,51 @@
"editor.autoClosingOvertype": "always",
"editor.autoClosingQuotes": "beforeWhitespace",
"editor.wordSeparators": "`~!@#%^&*()=+[{]}\\|;:'\",.<>/?",
+ "editor.quickSuggestions": {
+ "strings": "on"
+ },
+
+ // lint && format
+ "oxc.enable": true,
+ "oxc.typeAware": true,
+ "oxc.configPath": "oxlint.config.ts",
+ "oxc.fmt.configPath": "oxfmt.config.ts",
+ "eslint.useFlatConfig": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
+ "source.fixAll.oxc": "explicit",
"source.fixAll.stylelint": "explicit",
"source.organizeImports": "never"
},
- "editor.defaultFormatter": "esbenp.prettier-vscode",
+ "editor.defaultFormatter": "oxc.oxc-vscode",
"[html]": {
- "editor.defaultFormatter": "esbenp.prettier-vscode"
+ "editor.defaultFormatter": "oxc.oxc-vscode"
},
"[css]": {
- "editor.defaultFormatter": "esbenp.prettier-vscode"
+ "editor.defaultFormatter": "oxc.oxc-vscode"
},
"[scss]": {
- "editor.defaultFormatter": "esbenp.prettier-vscode"
+ "editor.defaultFormatter": "oxc.oxc-vscode"
},
"[javascript]": {
- "editor.defaultFormatter": "esbenp.prettier-vscode"
+ "editor.defaultFormatter": "oxc.oxc-vscode"
},
"[typescript]": {
- "editor.defaultFormatter": "esbenp.prettier-vscode"
+ "editor.defaultFormatter": "oxc.oxc-vscode"
},
"[json]": {
- "editor.defaultFormatter": "esbenp.prettier-vscode"
+ "editor.defaultFormatter": "oxc.oxc-vscode"
},
"[markdown]": {
- "editor.defaultFormatter": "esbenp.prettier-vscode"
+ "editor.defaultFormatter": "oxc.oxc-vscode"
},
"[jsonc]": {
- "editor.defaultFormatter": "esbenp.prettier-vscode"
+ "editor.defaultFormatter": "oxc.oxc-vscode"
},
"[vue]": {
- "editor.defaultFormatter": "esbenp.prettier-vscode"
+ "editor.defaultFormatter": "oxc.oxc-vscode"
},
+
// extensions
"extensions.ignoreRecommendations": true,
@@ -79,6 +92,7 @@
"files.insertFinalNewline": true,
"files.simpleDialog.enable": true,
"files.associations": {
+ "*.css": "tailwindcss",
"*.ejs": "html",
"*.art": "html",
"**/tsconfig.json": "jsonc",
@@ -118,7 +132,7 @@
// search
"search.searchEditor.singleClickBehaviour": "peekDefinition",
"search.followSymlinks": false,
- // 在使用搜索功能时,将这些文件夹/文件排除在外
+ // 使用搜索功能时,将这些文件和文件夹排除在外
"search.exclude": {
"**/node_modules": true,
"**/*.log": true,
@@ -158,7 +172,7 @@
"emmet.triggerExpansionOnTab": false,
"errorLens.enabledDiagnosticLevels": ["warning", "error"],
- "errorLens.excludeBySource": ["cSpell", "Grammarly", "eslint"],
+ "errorLens.excludeBySource": ["cSpell", "Grammarly"],
"stylelint.enable": true,
"stylelint.packageManager": "pnpm",
@@ -166,9 +180,10 @@
"stylelint.customSyntax": "postcss-html",
"stylelint.snippet": ["css", "less", "postcss", "scss", "vue"],
- "typescript.inlayHints.enumMemberValues.enabled": true,
- "typescript.preferences.preferTypeOnlyAutoImports": true,
- "typescript.preferences.includePackageJsonAutoImports": "on",
+ "js/ts.tsdk.path": "node_modules/typescript/lib",
+ "js/ts.inlayHints.enumMemberValues.enabled": true,
+ "js/ts.preferences.preferTypeOnlyAutoImports": true,
+ "js/ts.preferences.includePackageJsonAutoImports": "on",
"eslint.validate": [
"javascript",
@@ -180,7 +195,8 @@
"markdown",
"json",
"jsonc",
- "json5"
+ "json5",
+ "yaml"
],
"tailwindCSS.experimental.classRegex": [
@@ -194,7 +210,7 @@
"yaml": false
},
- "cssVariables.lookupFiles": ["packages/core/base/design/src/**/*.css"],
+ "cssVariables.lookupFiles": ["packages/@core/base/design/src/**/*.css"],
"i18n-ally.localesPaths": [
"packages/locales/src/langs",
@@ -219,23 +235,9 @@
"*.env": "$(capture).env.*",
"README.md": "README*,CHANGELOG*,LICENSE,CNAME",
"package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,.gitattributes,.gitignore,.gitpod.yml,.npmrc,.browserslistrc,.node-version,.git*,.tazerc.json",
- "eslint.config.mjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json,lefthook.yml",
- "tailwind.config.mjs": "postcss.*"
+ "oxlint.config.ts": ".eslintignore,.stylelintignore,.commitlintrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json,lefthook.yml,oxfmt.config.*,eslint.config.*"
},
"commentTranslate.hover.enabled": false,
"commentTranslate.multiLineMerge": true,
- "vue.server.hybridMode": true,
- "typescript.tsdk": "node_modules/typescript/lib",
- "oxc.enable": false,
- "cSpell.words": [
- "archiver",
- "axios",
- "dotenv",
- "isequal",
- "jspm",
- "napi",
- "nolebase",
- "rollup",
- "vitest"
- ]
+ "vue.server.hybridMode": true
}
diff --git a/README.ja-JP.md b/README.ja-JP.md
index f7847a1d..ca0a9775 100644
--- a/README.ja-JP.md
+++ b/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はサポートしません
@@ -140,8 +140,12 @@ pnpm build
## 貢献者
+
+
+
+
-
+
## Discord
diff --git a/README.md b/README.md
index 0580d49d..e1c9f29c 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,157 @@
-# 奶酪云后台管理脚手架
+
+
+
+
+
+
+
+[](LICENSE)
+
+
Vue Vben Admin
+
+
+[](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin) [](https://github.com/vbenjs/vue-vben-admin/actions/workflows/codeql.yml) [](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml) [](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml) [](https://github.com/vbenjs/vue-vben-admin/actions/workflows/deploy.yml)
+
+**English** | [中文](./README.zh-CN.md) | [日本語](./README.ja-JP.md)
+
+## Introduction
+
+Vue Vben Admin is a free and open source middle and back-end template. Using the latest `vue3`, `vite`, `TypeScript` and other mainstream technology development, the out-of-the-box middle and back-end front-end solutions can also be used for learning reference.
+
+## Upgrade Notice
+
+This is the latest version, 5.0, and it is not compatible with previous versions. If you are starting a new project, it is recommended to use the latest version. If you wish to view the old version, please use the [v2 branch](https://github.com/vbenjs/vue-vben-admin/tree/v2).
+
+## Features
+
+- **Latest Technology Stack**: Developed with cutting-edge front-end technologies like Vue 3 and Vite
+- **TypeScript**: A language for application-scale JavaScript
+- **Themes**: Multiple theme colors available with customizable options
+- **Internationalization**: Comprehensive built-in internationalization support
+- **Permissions**: Built-in solution for dynamic route-based permission generation
+
+## Preview
+
+- [Vben Admin](https://vben.pro/) - Full version Chinese site
+
+Test Account: vben/123456
+
+
+
+### Use Gitpod
+
+Open the project in Gitpod (free online dev environment for GitHub) and start coding immediately.
+
+[](https://gitpod.io/#https://github.com/vbenjs/vue-vben-admin)
+
+## Documentation
+
+[Document](https://doc.vben.pro/)
+
+## Install and Use
+
+1. Get the project code
+
+```bash
+git clone https://github.com/vbenjs/vue-vben-admin.git
+```
+
+2. Install dependencies
+
+```bash
+cd vue-vben-admin
+npm i -g corepack
+pnpm install
+```
+
+3. Run
+
+```bash
+pnpm dev
+```
+
+4. Build
+
+```bash
+pnpm build
+```
+
+## Change Log
+
+[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases)
+
+## How to Contribute
+
+You are very welcome to join! [Raise an issue](https://github.com/anncwb/vue-vben-admin/issues/new/choose) or submit a Pull Request.
+
+**Pull Request Process:**
+
+1. Fork the code
+2. Create your branch: `git checkout -b feat/xxxx`
+3. Submit your changes: `git commit -am 'feat(function): add xxxxx'`
+4. Push your branch: `git push origin feat/xxxx`
+5. Submit `pull request`
+
+## Git Contribution Submission Specification
+
+Reference [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) specification ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
+
+- `feat` Add new features
+- `fix` Fix the problem/BUG
+- `style` The code style is related and does not affect the running result
+- `perf` Optimization/performance improvement
+- `refactor` Refactor
+- `revert` Undo edit
+- `test` Test related
+- `docs` Documentation/notes
+- `chore` Dependency update/scaffolding configuration modification etc.
+- `ci` Continuous integration
+- `types` Type definition file changes
+
+## Browser Support
+
+Tailwind CSS v4.0 is designed for Safari 16.4+, Chrome 111+, and Firefox 128+
+
+Support modern browsers, not IE
+
+| [ ](http://godban.github.io/browsers-support-badges/)Edge | [ ](http://godban.github.io/browsers-support-badges/)Firefox | [ ](http://godban.github.io/browsers-support-badges/)Chrome | [ ](http://godban.github.io/browsers-support-badges/)Safari |
+| :-: | :-: | :-: | :-: |
+| last 2 versions | last 2 versions | last 2 versions | last 2 versions |
+
+## Maintainer
+
+[@Vben](https://github.com/anncwb)
+
+## Star History
+
+[](https://star-history.com/#vbenjs/vue-vben-admin&Date)
+
+## Donate
+
+If you think this project is helpful to you, you can help the author buy a cup of coffee to show your support!
+
+
+
+Paypal Me
+
+## Contributors
+
+
+
+
+
+
+
+
+
+## Discord
+
+- [Github Discussions](https://github.com/anncwb/vue-vben-admin/discussions)
+
+## License
+
+[MIT © Vben-2020](./LICENSE)
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 5a6b191b..da7ec5fa 100644
--- a/README.zh-CN.md
+++ b/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
@@ -140,8 +140,12 @@ pnpm build
## 贡献者
+
+
+
+
-
+
## Discord
diff --git a/apps/backend-mock/api/auth/codes.ts b/apps/backend-mock/api/auth/codes.ts
index 7ba01270..e610b338 100644
--- a/apps/backend-mock/api/auth/codes.ts
+++ b/apps/backend-mock/api/auth/codes.ts
@@ -1,5 +1,7 @@
+import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
-import { unAuthorizedResponse } from '~/utils/response';
+import { MOCK_CODES } from '~/utils/mock-data';
+import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);
diff --git a/apps/backend-mock/api/auth/login.post.ts b/apps/backend-mock/api/auth/login.post.ts
index df5737a2..e23942c4 100644
--- a/apps/backend-mock/api/auth/login.post.ts
+++ b/apps/backend-mock/api/auth/login.post.ts
@@ -1,9 +1,15 @@
+import { defineEventHandler, readBody, setResponseStatus } from 'h3';
import {
clearRefreshTokenCookie,
setRefreshTokenCookie,
} from '~/utils/cookie-utils';
import { generateAccessToken, generateRefreshToken } from '~/utils/jwt-utils';
-import { forbiddenResponse } from '~/utils/response';
+import { MOCK_USERS } from '~/utils/mock-data';
+import {
+ forbiddenResponse,
+ useResponseError,
+ useResponseSuccess,
+} from '~/utils/response';
export default defineEventHandler(async (event) => {
const { password, username } = await readBody(event);
diff --git a/apps/backend-mock/api/auth/logout.post.ts b/apps/backend-mock/api/auth/logout.post.ts
index ac6afe94..74c8d315 100644
--- a/apps/backend-mock/api/auth/logout.post.ts
+++ b/apps/backend-mock/api/auth/logout.post.ts
@@ -1,7 +1,9 @@
+import { defineEventHandler } from 'h3';
import {
clearRefreshTokenCookie,
getRefreshTokenFromCookie,
} from '~/utils/cookie-utils';
+import { useResponseSuccess } from '~/utils/response';
export default defineEventHandler(async (event) => {
const refreshToken = getRefreshTokenFromCookie(event);
diff --git a/apps/backend-mock/api/auth/refresh.post.ts b/apps/backend-mock/api/auth/refresh.post.ts
index 7df4d34f..7d8d3a51 100644
--- a/apps/backend-mock/api/auth/refresh.post.ts
+++ b/apps/backend-mock/api/auth/refresh.post.ts
@@ -1,9 +1,11 @@
+import { defineEventHandler } from 'h3';
import {
clearRefreshTokenCookie,
getRefreshTokenFromCookie,
setRefreshTokenCookie,
} from '~/utils/cookie-utils';
-import { verifyRefreshToken } from '~/utils/jwt-utils';
+import { generateAccessToken, verifyRefreshToken } from '~/utils/jwt-utils';
+import { MOCK_USERS } from '~/utils/mock-data';
import { forbiddenResponse } from '~/utils/response';
export default defineEventHandler(async (event) => {
diff --git a/apps/backend-mock/api/demo/bigint.ts b/apps/backend-mock/api/demo/bigint.ts
new file mode 100644
index 00000000..00d6c28c
--- /dev/null
+++ b/apps/backend-mock/api/demo/bigint.ts
@@ -0,0 +1,32 @@
+import { eventHandler, setHeader } from 'h3';
+import { verifyAccessToken } from '~/utils/jwt-utils';
+import { unAuthorizedResponse } from '~/utils/response';
+
+export default eventHandler(async (event) => {
+ const userinfo = verifyAccessToken(event);
+ if (!userinfo) {
+ return unAuthorizedResponse(event);
+ }
+ const data = `
+ {
+ "code": 0,
+ "message": "success",
+ "data": [
+ {
+ "id": 123456789012345678901234567890123456789012345678901234567890,
+ "name": "John Doe",
+ "age": 30,
+ "email": "john-doe@demo.com"
+ },
+ {
+ "id": 987654321098765432109876543210987654321098765432109876543210,
+ "name": "Jane Smith",
+ "age": 25,
+ "email": "jane@demo.com"
+ }
+ ]
+ }
+ `;
+ setHeader(event, 'Content-Type', 'application/json');
+ return data;
+});
diff --git a/apps/backend-mock/api/menu/all.ts b/apps/backend-mock/api/menu/all.ts
index 580cee4f..7923f7ca 100644
--- a/apps/backend-mock/api/menu/all.ts
+++ b/apps/backend-mock/api/menu/all.ts
@@ -1,5 +1,7 @@
+import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
-import { unAuthorizedResponse } from '~/utils/response';
+import { MOCK_MENUS } from '~/utils/mock-data';
+import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
diff --git a/apps/backend-mock/api/status.ts b/apps/backend-mock/api/status.ts
index 41773e1d..43782095 100644
--- a/apps/backend-mock/api/status.ts
+++ b/apps/backend-mock/api/status.ts
@@ -1,3 +1,6 @@
+import { eventHandler, getQuery, setResponseStatus } from 'h3';
+import { useResponseError } from '~/utils/response';
+
export default eventHandler((event) => {
const { status } = getQuery(event);
setResponseStatus(event, Number(status));
diff --git a/apps/backend-mock/api/system/dept/.post.ts b/apps/backend-mock/api/system/dept/.post.ts
index c529ea1b..9a4896af 100644
--- a/apps/backend-mock/api/system/dept/.post.ts
+++ b/apps/backend-mock/api/system/dept/.post.ts
@@ -1,3 +1,4 @@
+import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,
diff --git a/apps/backend-mock/api/system/dept/[id].delete.ts b/apps/backend-mock/api/system/dept/[id].delete.ts
index e48f051c..eac0f584 100644
--- a/apps/backend-mock/api/system/dept/[id].delete.ts
+++ b/apps/backend-mock/api/system/dept/[id].delete.ts
@@ -1,3 +1,4 @@
+import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,
diff --git a/apps/backend-mock/api/system/dept/[id].put.ts b/apps/backend-mock/api/system/dept/[id].put.ts
index aa55c085..6805e139 100644
--- a/apps/backend-mock/api/system/dept/[id].put.ts
+++ b/apps/backend-mock/api/system/dept/[id].put.ts
@@ -1,3 +1,4 @@
+import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,
diff --git a/apps/backend-mock/api/system/dept/list.ts b/apps/backend-mock/api/system/dept/list.ts
index ae819b62..a649a0d2 100644
--- a/apps/backend-mock/api/system/dept/list.ts
+++ b/apps/backend-mock/api/system/dept/list.ts
@@ -1,4 +1,5 @@
import { faker } from '@faker-js/faker';
+import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
diff --git a/apps/backend-mock/api/system/menu/list.ts b/apps/backend-mock/api/system/menu/list.ts
index 5328b2fd..ce96bb14 100644
--- a/apps/backend-mock/api/system/menu/list.ts
+++ b/apps/backend-mock/api/system/menu/list.ts
@@ -1,3 +1,4 @@
+import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
diff --git a/apps/backend-mock/api/system/menu/name-exists.ts b/apps/backend-mock/api/system/menu/name-exists.ts
index 5599c22b..7d5551b3 100644
--- a/apps/backend-mock/api/system/menu/name-exists.ts
+++ b/apps/backend-mock/api/system/menu/name-exists.ts
@@ -1,6 +1,7 @@
+import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data';
-import { unAuthorizedResponse } from '~/utils/response';
+import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
const namesMap: Record = {};
diff --git a/apps/backend-mock/api/system/menu/path-exists.ts b/apps/backend-mock/api/system/menu/path-exists.ts
index 64774f79..f3c3be99 100644
--- a/apps/backend-mock/api/system/menu/path-exists.ts
+++ b/apps/backend-mock/api/system/menu/path-exists.ts
@@ -1,6 +1,7 @@
+import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data';
-import { unAuthorizedResponse } from '~/utils/response';
+import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
const pathMap: Record = { '/': 0 };
diff --git a/apps/backend-mock/api/system/role/list.ts b/apps/backend-mock/api/system/role/list.ts
index 4d5f923e..bad29a51 100644
--- a/apps/backend-mock/api/system/role/list.ts
+++ b/apps/backend-mock/api/system/role/list.ts
@@ -1,4 +1,5 @@
import { faker } from '@faker-js/faker';
+import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { getMenuIds, MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
diff --git a/apps/backend-mock/api/system/user/list.ts b/apps/backend-mock/api/system/user/list.ts
new file mode 100644
index 00000000..21d44c8b
--- /dev/null
+++ b/apps/backend-mock/api/system/user/list.ts
@@ -0,0 +1,85 @@
+import { faker } from '@faker-js/faker';
+import { eventHandler, getQuery } from 'h3';
+import { verifyAccessToken } from '~/utils/jwt-utils';
+import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
+
+const formatterCN = new Intl.DateTimeFormat('zh-CN', {
+ timeZone: 'Asia/Shanghai',
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+});
+
+function generateMockDataList(count: number) {
+ const dataList = [];
+
+ for (let i = 0; i < count; i++) {
+ const dataItem: Record = {
+ id: faker.string.uuid(),
+ name: faker.commerce.product(),
+ status: faker.helpers.arrayElement([0, 1]),
+ createTime: formatterCN.format(
+ faker.date.between({ from: '2022-01-01', to: '2025-01-01' }),
+ ),
+ deptId: faker.string.uuid(),
+ remark: faker.lorem.sentence(),
+ };
+
+ dataList.push(dataItem);
+ }
+
+ return dataList;
+}
+
+const mockData = generateMockDataList(100);
+
+export default eventHandler(async (event) => {
+ const userinfo = verifyAccessToken(event);
+ if (!userinfo) {
+ return unAuthorizedResponse(event);
+ }
+
+ const {
+ page = 1,
+ pageSize = 20,
+ name,
+ id,
+ remark,
+ startTime,
+ endTime,
+ deptId,
+ status,
+ } = getQuery(event);
+ let listData = structuredClone(mockData);
+ if (name) {
+ listData = listData.filter((item) =>
+ item.name.toLowerCase().includes(String(name).toLowerCase()),
+ );
+ }
+ if (id) {
+ listData = listData.filter((item) =>
+ item.id.toLowerCase().includes(String(id).toLowerCase()),
+ );
+ }
+ if (remark) {
+ listData = listData.filter((item) =>
+ item.remark?.toLowerCase()?.includes(String(remark).toLowerCase()),
+ );
+ }
+ if (startTime) {
+ listData = listData.filter((item) => item.createTime >= startTime);
+ }
+ if (endTime) {
+ listData = listData.filter((item) => item.createTime <= endTime);
+ }
+ if (['0', '1'].includes(status as string)) {
+ listData = listData.filter((item) => item.status === Number(status));
+ }
+ if (deptId) {
+ listData = listData.filter((item) => item.deptId === deptId);
+ }
+ return usePageResponseSuccess(page as string, pageSize as string, listData);
+});
diff --git a/apps/backend-mock/api/table/list.ts b/apps/backend-mock/api/table/list.ts
index 3e6f705b..f81571e3 100644
--- a/apps/backend-mock/api/table/list.ts
+++ b/apps/backend-mock/api/table/list.ts
@@ -1,6 +1,11 @@
import { faker } from '@faker-js/faker';
+import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
-import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
+import {
+ sleep,
+ unAuthorizedResponse,
+ usePageResponseSuccess,
+} from '~/utils/response';
function generateMockDataList(count: number) {
const dataList = [];
@@ -44,30 +49,69 @@ export default eventHandler(async (event) => {
await sleep(600);
const { page, pageSize, sortBy, sortOrder } = getQuery(event);
+ // 规范化分页参数,处理 string[]
+ const pageRaw = Array.isArray(page) ? page[0] : page;
+ const pageSizeRaw = Array.isArray(pageSize) ? pageSize[0] : pageSize;
+ const pageNumber = Math.max(
+ 1,
+ Number.parseInt(String(pageRaw ?? '1'), 10) || 1,
+ );
+ const pageSizeNumber = Math.min(
+ 100,
+ Math.max(1, Number.parseInt(String(pageSizeRaw ?? '10'), 10) || 10),
+ );
const listData = structuredClone(mockData);
- if (sortBy && Reflect.has(listData[0], sortBy as string)) {
+
+ // 规范化 query 入参,兼容 string[]
+ const sortKeyRaw = Array.isArray(sortBy) ? sortBy[0] : sortBy;
+ const sortOrderRaw = Array.isArray(sortOrder) ? sortOrder[0] : sortOrder;
+ // 检查 sortBy 是否是 listData 元素的合法属性键
+ if (
+ typeof sortKeyRaw === 'string' &&
+ listData[0] &&
+ Object.prototype.hasOwnProperty.call(listData[0], sortKeyRaw)
+ ) {
+ // 定义数组元素的类型
+ type ItemType = (typeof listData)[0];
+ const sortKey = sortKeyRaw as keyof ItemType; // 将 sortBy 断言为合法键
+ const isDesc = sortOrderRaw === 'desc';
listData.sort((a, b) => {
- if (sortOrder === 'asc') {
- if (sortBy === 'price') {
- return (
- Number.parseFloat(a[sortBy as string]) -
- Number.parseFloat(b[sortBy as string])
- );
+ const aValue = a[sortKey] as unknown;
+ const bValue = b[sortKey] as unknown;
+
+ let result: number;
+
+ if (typeof aValue === 'number' && typeof bValue === 'number') {
+ result = aValue - bValue;
+ } else if (aValue instanceof Date && bValue instanceof Date) {
+ result = aValue.getTime() - bValue.getTime();
+ } else if (typeof aValue === 'boolean' && typeof bValue === 'boolean') {
+ if (aValue === bValue) {
+ result = 0;
} else {
- return a[sortBy as string] > b[sortBy as string] ? 1 : -1;
+ result = aValue ? 1 : -1;
}
} else {
- if (sortBy === 'price') {
- return (
- Number.parseFloat(b[sortBy as string]) -
- Number.parseFloat(a[sortBy as string])
- );
- } else {
- return a[sortBy as string] < b[sortBy as string] ? 1 : -1;
- }
+ const aStr = String(aValue);
+ const bStr = String(bValue);
+ const aNum = Number(aStr);
+ const bNum = Number(bStr);
+ result =
+ Number.isFinite(aNum) && Number.isFinite(bNum)
+ ? aNum - bNum
+ : aStr.localeCompare(bStr, undefined, {
+ numeric: true,
+ sensitivity: 'base',
+ });
}
+
+ return isDesc ? -result : result;
});
}
- return usePageResponseSuccess(page as string, pageSize as string, listData);
+ return usePageResponseSuccess(
+ String(pageNumber),
+ String(pageSizeNumber),
+ listData,
+ );
});
diff --git a/apps/backend-mock/api/test.get.ts b/apps/backend-mock/api/test.get.ts
index ca4a500b..dc2ceef7 100644
--- a/apps/backend-mock/api/test.get.ts
+++ b/apps/backend-mock/api/test.get.ts
@@ -1 +1,3 @@
+import { defineEventHandler } from 'h3';
+
export default defineEventHandler(() => 'Test get handler');
diff --git a/apps/backend-mock/api/test.post.ts b/apps/backend-mock/api/test.post.ts
index 698cf211..0e9e337a 100644
--- a/apps/backend-mock/api/test.post.ts
+++ b/apps/backend-mock/api/test.post.ts
@@ -1 +1,3 @@
+import { defineEventHandler } from 'h3';
+
export default defineEventHandler(() => 'Test post handler');
diff --git a/apps/backend-mock/api/timezone/getTimezone.ts b/apps/backend-mock/api/timezone/getTimezone.ts
new file mode 100644
index 00000000..0cbcb6ec
--- /dev/null
+++ b/apps/backend-mock/api/timezone/getTimezone.ts
@@ -0,0 +1,12 @@
+import { eventHandler } from 'h3';
+import { verifyAccessToken } from '~/utils/jwt-utils';
+import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
+import { getTimezone } from '~/utils/timezone-utils';
+
+export default eventHandler((event) => {
+ const userinfo = verifyAccessToken(event);
+ if (!userinfo) {
+ return unAuthorizedResponse(event);
+ }
+ return useResponseSuccess(getTimezone());
+});
diff --git a/apps/backend-mock/api/timezone/getTimezoneOptions.ts b/apps/backend-mock/api/timezone/getTimezoneOptions.ts
new file mode 100644
index 00000000..6c241864
--- /dev/null
+++ b/apps/backend-mock/api/timezone/getTimezoneOptions.ts
@@ -0,0 +1,11 @@
+import { eventHandler } from 'h3';
+import { TIME_ZONE_OPTIONS } from '~/utils/mock-data';
+import { useResponseSuccess } from '~/utils/response';
+
+export default eventHandler(() => {
+ const data = TIME_ZONE_OPTIONS.map((o) => ({
+ label: `${o.timezone} (GMT${o.offset >= 0 ? `+${o.offset}` : o.offset})`,
+ value: o.timezone,
+ }));
+ return useResponseSuccess(data);
+});
diff --git a/apps/backend-mock/api/timezone/setTimezone.ts b/apps/backend-mock/api/timezone/setTimezone.ts
new file mode 100644
index 00000000..851e4351
--- /dev/null
+++ b/apps/backend-mock/api/timezone/setTimezone.ts
@@ -0,0 +1,26 @@
+import { eventHandler, readBody, setResponseStatus } from 'h3';
+import { verifyAccessToken } from '~/utils/jwt-utils';
+import { TIME_ZONE_OPTIONS } from '~/utils/mock-data';
+import {
+ unAuthorizedResponse,
+ useResponseError,
+ useResponseSuccess,
+} from '~/utils/response';
+import { setTimezone } from '~/utils/timezone-utils';
+
+export default eventHandler(async (event) => {
+ const userinfo = verifyAccessToken(event);
+ if (!userinfo) {
+ return unAuthorizedResponse(event);
+ }
+ const body = await readBody<{ timezone?: unknown }>(event);
+ const timezone =
+ typeof body?.timezone === 'string' ? body.timezone : undefined;
+ const allowed = TIME_ZONE_OPTIONS.some((o) => o.timezone === timezone);
+ if (!timezone || !allowed) {
+ setResponseStatus(event, 400);
+ return useResponseError('Bad Request', 'Invalid timezone');
+ }
+ setTimezone(timezone);
+ return useResponseSuccess({});
+});
diff --git a/apps/backend-mock/api/upload.ts b/apps/backend-mock/api/upload.ts
index 1bb9e602..436b63cb 100644
--- a/apps/backend-mock/api/upload.ts
+++ b/apps/backend-mock/api/upload.ts
@@ -1,5 +1,6 @@
+import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
-import { unAuthorizedResponse } from '~/utils/response';
+import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);
diff --git a/apps/backend-mock/api/user/info.ts b/apps/backend-mock/api/user/info.ts
index cfa2346c..138cb433 100644
--- a/apps/backend-mock/api/user/info.ts
+++ b/apps/backend-mock/api/user/info.ts
@@ -1,5 +1,6 @@
+import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
-import { unAuthorizedResponse } from '~/utils/response';
+import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);
diff --git a/apps/backend-mock/middleware/1.api.ts b/apps/backend-mock/middleware/1.api.ts
index bad9a41a..339cda4d 100644
--- a/apps/backend-mock/middleware/1.api.ts
+++ b/apps/backend-mock/middleware/1.api.ts
@@ -1,3 +1,4 @@
+import { defineEventHandler } from 'h3';
import { forbiddenResponse, sleep } from '~/utils/response';
export default defineEventHandler(async (event) => {
diff --git a/apps/backend-mock/nitro.config.ts b/apps/backend-mock/nitro.config.ts
index c0fc13e2..9cc3acdb 100644
--- a/apps/backend-mock/nitro.config.ts
+++ b/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();
diff --git a/apps/backend-mock/package.json b/apps/backend-mock/package.json
index cc0b8d53..66961d4d 100644
--- a/apps/backend-mock/package.json
+++ b/apps/backend-mock/package.json
@@ -1,6 +1,6 @@
{
"name": "@vben/backend-mock",
- "version": "0.0.1",
+ "version": "5.7.0",
"description": "",
"private": true,
"license": "MIT",
diff --git a/apps/backend-mock/routes/[...].ts b/apps/backend-mock/routes/[...].ts
index 99f544b6..44038cf7 100644
--- a/apps/backend-mock/routes/[...].ts
+++ b/apps/backend-mock/routes/[...].ts
@@ -1,13 +1,15 @@
+import { defineEventHandler } from 'h3';
+
export default defineEventHandler(() => {
return `
Hello Vben Admin
-Mock service is starting
+Mock service is running
`;
});
diff --git a/apps/backend-mock/tsconfig.json b/apps/backend-mock/tsconfig.json
index 43008af1..4417b73d 100644
--- a/apps/backend-mock/tsconfig.json
+++ b/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"]
}
diff --git a/apps/backend-mock/utils/cookie-utils.ts b/apps/backend-mock/utils/cookie-utils.ts
index 78f3aab7..187ce2f0 100644
--- a/apps/backend-mock/utils/cookie-utils.ts
+++ b/apps/backend-mock/utils/cookie-utils.ts
@@ -1,5 +1,7 @@
import type { EventHandlerRequest, H3Event } from 'h3';
+import { deleteCookie, getCookie, setCookie } from 'h3';
+
export function clearRefreshTokenCookie(event: H3Event) {
deleteCookie(event, 'jwt', {
httpOnly: true,
diff --git a/apps/backend-mock/utils/jwt-utils.ts b/apps/backend-mock/utils/jwt-utils.ts
index 8cfc6843..71858307 100644
--- a/apps/backend-mock/utils/jwt-utils.ts
+++ b/apps/backend-mock/utils/jwt-utils.ts
@@ -1,8 +1,11 @@
import type { EventHandlerRequest, H3Event } from 'h3';
+import type { UserInfo } from './mock-data';
+
+import { getHeader } from 'h3';
import jwt from 'jsonwebtoken';
-import { UserInfo } from './mock-data';
+import { MOCK_USERS } from './mock-data';
// TODO: Replace with your own secret key
const ACCESS_TOKEN_SECRET = 'access_token_secret';
@@ -31,12 +34,22 @@ export function verifyAccessToken(
return null;
}
- const token = authHeader.split(' ')[1];
+ const tokenParts = authHeader.split(' ');
+ if (tokenParts.length !== 2) {
+ return null;
+ }
+ const token = tokenParts[1] as string;
try {
- const decoded = jwt.verify(token, ACCESS_TOKEN_SECRET) as UserPayload;
+ const decoded = jwt.verify(
+ token,
+ ACCESS_TOKEN_SECRET,
+ ) as unknown as UserPayload;
const username = decoded.username;
const user = MOCK_USERS.find((item) => item.username === username);
+ if (!user) {
+ return null;
+ }
const { password: _pwd, ...userinfo } = user;
return userinfo;
} catch {
@@ -50,7 +63,12 @@ export function verifyRefreshToken(
try {
const decoded = jwt.verify(token, REFRESH_TOKEN_SECRET) as UserPayload;
const username = decoded.username;
- const user = MOCK_USERS.find((item) => item.username === username);
+ const user = MOCK_USERS.find(
+ (item) => item.username === username,
+ ) as UserInfo;
+ if (!user) {
+ return null;
+ }
const { password: _pwd, ...userinfo } = user;
return userinfo;
} catch {
diff --git a/apps/backend-mock/utils/mock-data.ts b/apps/backend-mock/utils/mock-data.ts
index 192f30a0..82a50dd6 100644
--- a/apps/backend-mock/utils/mock-data.ts
+++ b/apps/backend-mock/utils/mock-data.ts
@@ -7,6 +7,11 @@ export interface UserInfo {
homePath?: string;
}
+export interface TimezoneOption {
+ offset: number;
+ timezone: string;
+}
+
export const MOCK_USERS: UserInfo[] = [
{
id: 0,
@@ -14,6 +19,7 @@ export const MOCK_USERS: UserInfo[] = [
realName: 'Vben',
roles: ['super'],
username: 'vben',
+ homePath: '/dashboard/workspace',
},
{
id: 1,
@@ -21,7 +27,7 @@ export const MOCK_USERS: UserInfo[] = [
realName: 'Admin',
roles: ['admin'],
username: 'admin',
- homePath: '/workspace',
+ homePath: '/dashboard/workspace',
},
{
id: 2,
@@ -29,7 +35,7 @@ export const MOCK_USERS: UserInfo[] = [
realName: 'Jack',
roles: ['user'],
username: 'jack',
- homePath: '/analytics',
+ homePath: '/dashboard/analytics',
},
];
@@ -59,11 +65,11 @@ const dashboardMenus = [
},
name: 'Dashboard',
path: '/dashboard',
- redirect: '/analytics',
+ redirect: '/dashboard/analytics',
children: [
{
name: 'Analytics',
- path: '/analytics',
+ path: 'analytics',
component: '/dashboard/analytics/index',
meta: {
affixTab: true,
@@ -72,7 +78,7 @@ const dashboardMenus = [
},
{
name: 'Workspace',
- path: '/workspace',
+ path: 'workspace',
component: '/dashboard/workspace/index',
meta: {
title: 'page.dashboard.workspace',
@@ -91,7 +97,7 @@ const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
title: 'demos.access.adminVisible',
},
name: 'AccessAdminVisibleDemo',
- path: '/demos/access/admin-visible',
+ path: 'admin-visible',
},
super: {
component: '/demos/access/super-visible',
@@ -100,7 +106,7 @@ const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
title: 'demos.access.superVisible',
},
name: 'AccessSuperVisibleDemo',
- path: '/demos/access/super-visible',
+ path: 'super-visible',
},
user: {
component: '/demos/access/user-visible',
@@ -109,7 +115,7 @@ const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
title: 'demos.access.userVisible',
},
name: 'AccessUserVisibleDemo',
- path: '/demos/access/user-visible',
+ path: 'user-visible',
},
};
@@ -127,7 +133,7 @@ const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
children: [
{
name: 'AccessDemos',
- path: '/demosaccess',
+ path: 'access',
meta: {
icon: 'mdi:cloud-key-outline',
title: 'demos.access.backendPermissions',
@@ -136,7 +142,7 @@ const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
children: [
{
name: 'AccessPageControlDemo',
- path: '/demos/access/page-control',
+ path: 'page-control',
component: '/demos/access/index',
meta: {
icon: 'mdi:page-previous-outline',
@@ -145,7 +151,7 @@ const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
},
{
name: 'AccessButtonControlDemo',
- path: '/demos/access/button-control',
+ path: 'button-control',
component: '/demos/access/button-control',
meta: {
icon: 'mdi:button-cursor',
@@ -154,7 +160,7 @@ const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
},
{
name: 'AccessMenuVisible403Demo',
- path: '/demos/access/menu-visible-403',
+ path: 'menu-visible-403',
component: '/demos/access/menu-visible-403',
meta: {
authority: ['no-body'],
@@ -189,18 +195,46 @@ export const MOCK_MENUS = [
export const MOCK_MENU_LIST = [
{
id: 1,
- name: 'Workspace',
+ name: 'Dashboard',
status: 1,
- type: 'menu',
- icon: 'mdi:dashboard',
- path: '/workspace',
- component: '/dashboard/workspace/index',
+ type: 'catalog',
+ icon: 'lucide:layout-dashboard',
+ path: '/dashboard',
meta: {
- icon: 'carbon:workspace',
- title: 'page.dashboard.workspace',
- affixTab: true,
- order: 0,
+ icon: 'lucide:layout-dashboard',
+ order: -1,
+ title: 'page.dashboard.title',
},
+ children: [
+ {
+ id: 101,
+ pid: 1,
+ status: 1,
+ type: 'menu',
+ name: 'Analytics',
+ path: 'analytics',
+ component: '/dashboard/analytics/index',
+ meta: {
+ affixTab: true,
+ icon: 'lucide:area-chart',
+ title: 'page.dashboard.analytics',
+ keepAlive: true,
+ },
+ },
+ {
+ id: 102,
+ pid: 1,
+ status: 1,
+ type: 'menu',
+ name: 'Workspace',
+ path: 'workspace',
+ component: '/views/dashboard/workspace/index',
+ meta: {
+ icon: 'carbon:workspace',
+ title: 'page.dashboard.workspace',
+ },
+ },
+ ],
},
{
id: 2,
@@ -276,7 +310,7 @@ export const MOCK_MENU_LIST = [
children: [
{
id: 20_401,
- pid: 201,
+ pid: 202,
name: 'SystemDeptCreate',
status: 1,
type: 'button',
@@ -285,7 +319,7 @@ export const MOCK_MENU_LIST = [
},
{
id: 20_402,
- pid: 201,
+ pid: 202,
name: 'SystemDeptEdit',
status: 1,
type: 'button',
@@ -294,7 +328,7 @@ export const MOCK_MENU_LIST = [
},
{
id: 20_403,
- pid: 201,
+ pid: 202,
name: 'SystemDeptDelete',
status: 1,
type: 'button',
@@ -388,3 +422,29 @@ export function getMenuIds(menus: any[]) {
});
return ids;
}
+
+/**
+ * 时区选项
+ */
+export const TIME_ZONE_OPTIONS: TimezoneOption[] = [
+ {
+ offset: -5,
+ timezone: 'America/New_York',
+ },
+ {
+ offset: 0,
+ timezone: 'Europe/London',
+ },
+ {
+ offset: 8,
+ timezone: 'Asia/Shanghai',
+ },
+ {
+ offset: 9,
+ timezone: 'Asia/Tokyo',
+ },
+ {
+ offset: 9,
+ timezone: 'Asia/Seoul',
+ },
+];
diff --git a/apps/backend-mock/utils/response.ts b/apps/backend-mock/utils/response.ts
index 2a5a908f..2d4242e9 100644
--- a/apps/backend-mock/utils/response.ts
+++ b/apps/backend-mock/utils/response.ts
@@ -1,5 +1,7 @@
import type { EventHandlerRequest, H3Event } from 'h3';
+import { setResponseStatus } from 'h3';
+
export function useResponseSuccess(data: T) {
return {
code: 0,
diff --git a/apps/backend-mock/utils/timezone-utils.ts b/apps/backend-mock/utils/timezone-utils.ts
new file mode 100644
index 00000000..da35f920
--- /dev/null
+++ b/apps/backend-mock/utils/timezone-utils.ts
@@ -0,0 +1,9 @@
+let mockTimeZone: null | string = null;
+
+export const setTimezone = (timeZone: string) => {
+ mockTimeZone = timeZone;
+};
+
+export const getTimezone = () => {
+ return mockTimeZone;
+};
diff --git a/apps/web-antd/index.html b/apps/web-antd/index.html
index 480eb84d..e3b9ae5c 100644
--- a/apps/web-antd/index.html
+++ b/apps/web-antd/index.html
@@ -12,7 +12,7 @@
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
/>
- <%= VITE_APP_TITLE %>
+ %VITE_APP_TITLE%
diff --git a/apps/web-antd/src/layouts/basic.vue b/apps/web-antd/src/layouts/basic.vue
index 0101a3db..4a7c7bdf 100644
--- a/apps/web-antd/src/layouts/basic.vue
+++ b/apps/web-antd/src/layouts/basic.vue
@@ -2,18 +2,19 @@
import type { NotificationItem } from '@vben/layouts';
import { computed, ref, watch } from 'vue';
+import { useRouter } from 'vue-router';
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
import { useWatermark } from '@vben/hooks';
-import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
+import { BookOpenText, CircleHelp, SvgGithubIcon } from '@vben/icons';
import {
BasicLayout,
LockScreen,
Notification,
UserDropdown,
} from '@vben/layouts';
-import { preferences } from '@vben/preferences';
+import { preferences, usePreferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores';
import { openWindow } from '@vben/utils';
@@ -23,6 +24,7 @@ import LoginForm from '#/views/_core/authentication/login.vue';
const notifications = ref([
{
+ id: 1,
avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
date: '3小时前',
isRead: true,
@@ -30,6 +32,7 @@ const notifications = ref([
title: '收到了 14 份新周报',
},
{
+ id: 2,
avatar: 'https://avatar.vercel.sh/1',
date: '刚刚',
isRead: false,
@@ -37,6 +40,7 @@ const notifications = ref([
title: '朱偏右 回复了你',
},
{
+ id: 3,
avatar: 'https://avatar.vercel.sh/1',
date: '2024-01-01',
isRead: false,
@@ -44,23 +48,51 @@ const notifications = ref([
title: '曲丽丽 评论了你',
},
{
+ id: 4,
avatar: 'https://avatar.vercel.sh/satori',
date: '1天前',
isRead: false,
message: '描述信息描述信息描述信息',
title: '代办提醒',
},
+ {
+ id: 5,
+ avatar: 'https://avatar.vercel.sh/satori',
+ date: '1天前',
+ isRead: false,
+ message: '描述信息描述信息描述信息',
+ title: '跳转Workspace示例',
+ link: '/workspace',
+ },
+ {
+ id: 6,
+ avatar: 'https://avatar.vercel.sh/satori',
+ date: '1天前',
+ isRead: false,
+ message: '描述信息描述信息描述信息',
+ title: '跳转外部链接示例',
+ link: 'https://doc.vben.pro',
+ },
]);
+const router = useRouter();
const userStore = useUserStore();
const authStore = useAuthStore();
const accessStore = useAccessStore();
const { destroyWatermark, updateWatermark } = useWatermark();
+const { isDark } = usePreferences();
const showDot = computed(() =>
notifications.value.some((item) => !item.isRead),
);
const menus = computed(() => [
+ {
+ handler: () => {
+ router.push({ name: 'Profile' });
+ },
+ icon: 'lucide:user',
+ text: $t('page.auth.profile'),
+ },
{
handler: () => {
openWindow(VBEN_DOC_URL, {
@@ -76,7 +108,7 @@ const menus = computed(() => [
target: '_blank',
});
},
- icon: MdiGithub,
+ icon: SvgGithubIcon,
text: 'GitHub',
},
{
@@ -102,15 +134,77 @@ function handleNoticeClear() {
notifications.value = [];
}
+function markRead(id: number | string) {
+ const item = notifications.value.find((item) => item.id === id);
+ if (item) {
+ item.isRead = true;
+ }
+}
+
+function remove(id: number | string) {
+ notifications.value = notifications.value.filter((item) => item.id !== id);
+}
+
function handleMakeAll() {
notifications.value.forEach((item) => (item.isRead = true));
}
+
+const viewAll = () => {};
+
+const handleClick = (item: NotificationItem) => {
+ // 如果通知项有链接,点击时跳转
+ if (item.link) {
+ navigateTo(item.link, item.query, item.state);
+ }
+};
+
+function navigateTo(
+ link: string,
+ query?: Record,
+ state?: Record,
+) {
+ if (link.startsWith('http://') || link.startsWith('https://')) {
+ // 外部链接,在新标签页打开
+ window.open(link, '_blank');
+ } else {
+ // 内部路由链接,支持 query 参数和 state
+ router.push({
+ path: link,
+ query: query || {},
+ state,
+ });
+ }
+}
+
watch(
- () => preferences.app.watermark,
- async (enable) => {
+ () => ({
+ enable: preferences.app.watermark,
+ content: preferences.app.watermarkContent,
+ isDark: isDark.value,
+ }),
+ async ({ enable, content, isDark: isDarkValue }) => {
if (enable) {
+ const watermarkColor = isDarkValue
+ ? 'rgba(255, 255, 255, 0.12)'
+ : 'rgba(0, 0, 0, 0.12)';
+
await updateWatermark({
- content: `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
+ advancedStyle: {
+ colorStops: [
+ {
+ color: watermarkColor,
+ offset: 0,
+ },
+ {
+ color: watermarkColor,
+ offset: 1,
+ },
+ ],
+ type: 'linear',
+ },
+ content:
+ content ||
+ `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
});
} else {
destroyWatermark();
@@ -123,7 +217,12 @@ watch(
-
+
item.id && markRead(item.id)"
+ @remove="(item) => item.id && remove(item.id)"
@make-all="handleMakeAll"
+ @on-click="handleClick"
+ @view-all="viewAll"
/>
diff --git a/apps/web-antd/src/locales/langs/en-US/demos.json b/apps/web-antd/src/locales/langs/en-US/demos.json
index 07156434..859718e1 100644
--- a/apps/web-antd/src/locales/langs/en-US/demos.json
+++ b/apps/web-antd/src/locales/langs/en-US/demos.json
@@ -6,7 +6,9 @@
"about": "About",
"document": "Document",
"antdv": "Ant Design Vue Version",
+ "antdv-next": "Antdv Next Version",
"naive-ui": "Naive UI Version",
- "element-plus": "Element Plus Version"
+ "element-plus": "Element Plus Version",
+ "tdesign": "TDesign Vue Version"
}
}
diff --git a/apps/web-antd/src/locales/langs/en-US/page.json b/apps/web-antd/src/locales/langs/en-US/page.json
index 618a258c..39f1641c 100644
--- a/apps/web-antd/src/locales/langs/en-US/page.json
+++ b/apps/web-antd/src/locales/langs/en-US/page.json
@@ -4,7 +4,8 @@
"register": "Register",
"codeLogin": "Code Login",
"qrcodeLogin": "Qr Code Login",
- "forgetPassword": "Forget Password"
+ "forgetPassword": "Forget Password",
+ "profile": "Profile"
},
"dashboard": {
"title": "Dashboard",
diff --git a/apps/web-antd/src/locales/langs/zh-CN/demos.json b/apps/web-antd/src/locales/langs/zh-CN/demos.json
index 93ee722f..29910cdf 100644
--- a/apps/web-antd/src/locales/langs/zh-CN/demos.json
+++ b/apps/web-antd/src/locales/langs/zh-CN/demos.json
@@ -6,7 +6,9 @@
"about": "关于",
"document": "文档",
"antdv": "Ant Design Vue 版本",
+ "antdv-next": "Antdv Next 版本",
"naive-ui": "Naive UI 版本",
- "element-plus": "Element Plus 版本"
+ "element-plus": "Element Plus 版本",
+ "tdesign": "TDesign Vue 版本"
}
}
diff --git a/apps/web-antd/src/locales/langs/zh-CN/page.json b/apps/web-antd/src/locales/langs/zh-CN/page.json
index 4cb67081..2192d1d5 100644
--- a/apps/web-antd/src/locales/langs/zh-CN/page.json
+++ b/apps/web-antd/src/locales/langs/zh-CN/page.json
@@ -4,7 +4,8 @@
"register": "注册",
"codeLogin": "验证码登录",
"qrcodeLogin": "二维码登录",
- "forgetPassword": "忘记密码"
+ "forgetPassword": "忘记密码",
+ "profile": "个人中心"
},
"dashboard": {
"title": "概览",
diff --git a/apps/web-antd/src/main.ts b/apps/web-antd/src/main.ts
index 5d728a02..110acbd7 100644
--- a/apps/web-antd/src/main.ts
+++ b/apps/web-antd/src/main.ts
@@ -1,7 +1,7 @@
import { initPreferences } from '@vben/preferences';
import { unmountGlobalLoading } from '@vben/utils';
-import { overridesPreferences } from './preferences';
+import { overridesPreferences, preferencesExtension } from './preferences';
/**
* 应用初始化完成之后再进行页面加载渲染
@@ -15,6 +15,7 @@ async function initApplication() {
// app偏好设置初始化
await initPreferences({
+ extension: preferencesExtension,
namespace,
overrides: overridesPreferences,
});
diff --git a/apps/web-antd/src/preferences.ts b/apps/web-antd/src/preferences.ts
index 8841f754..75d94e23 100644
--- a/apps/web-antd/src/preferences.ts
+++ b/apps/web-antd/src/preferences.ts
@@ -1,4 +1,15 @@
-import { defineOverridesPreferences } from '@vben/preferences';
+import {
+ appCopyrightPreferences,
+ defineOverridesPreferences,
+ definePreferencesExtension,
+} from '@vben/preferences';
+
+interface WebAntdPreferencesExtension {
+ defaultTableSize: number;
+ enableFormFullscreen: boolean;
+ reportTitle: string;
+ tenantMode: 'multi' | 'single';
+}
/**
* @description 项目配置文件
@@ -32,4 +43,54 @@ export const overridesPreferences = defineOverridesPreferences({
colorPrimary: 'hsl(245 82% 67%)',
mode: 'auto',
},
+ copyright: appCopyrightPreferences,
});
+
+export const preferencesExtension =
+ definePreferencesExtension({
+ tabLabel: 'preferences.antd.tabLabel',
+ title: 'preferences.antd.title',
+ fields: [
+ {
+ component: 'switch',
+ defaultValue: true,
+ key: 'enableFormFullscreen',
+ label: 'preferences.antd.fields.enableFormFullscreen.label',
+ tip: 'preferences.antd.fields.enableFormFullscreen.tip',
+ },
+ {
+ component: 'select',
+ defaultValue: 'single',
+ key: 'tenantMode',
+ label: 'preferences.antd.fields.tenantMode.label',
+ options: [
+ {
+ label: 'preferences.antd.fields.tenantMode.options.single.label',
+ value: 'single',
+ },
+ {
+ label: 'preferences.antd.fields.tenantMode.options.multi.label',
+ value: 'multi',
+ },
+ ],
+ },
+ {
+ component: 'number',
+ componentProps: {
+ max: 200,
+ min: 10,
+ step: 10,
+ },
+ defaultValue: 20,
+ key: 'defaultTableSize',
+ label: 'preferences.antd.fields.defaultTableSize.label',
+ },
+ {
+ component: 'input',
+ defaultValue: '',
+ key: 'reportTitle',
+ label: 'preferences.antd.fields.reportTitle.label',
+ placeholder: 'preferences.antd.fields.reportTitle.placeholder',
+ },
+ ],
+ });
diff --git a/apps/web-antd/src/router/routes/modules/dashboard.ts b/apps/web-antd/src/router/routes/modules/dashboard.ts
index 5254dc65..44f15ef9 100644
--- a/apps/web-antd/src/router/routes/modules/dashboard.ts
+++ b/apps/web-antd/src/router/routes/modules/dashboard.ts
@@ -14,7 +14,7 @@ const routes: RouteRecordRaw[] = [
children: [
{
name: 'Analytics',
- path: '/analytics',
+ path: 'analytics',
component: () => import('#/views/dashboard/analytics/index.vue'),
meta: {
affixTab: true,
@@ -24,7 +24,7 @@ const routes: RouteRecordRaw[] = [
},
{
name: 'Workspace',
- path: '/workspace',
+ path: 'workspace',
component: () => import('#/views/dashboard/workspace/index.vue'),
meta: {
icon: 'carbon:workspace',
diff --git a/apps/web-antd/src/router/routes/modules/demos.ts b/apps/web-antd/src/router/routes/modules/demos.ts
new file mode 100644
index 00000000..0f9a0a93
--- /dev/null
+++ b/apps/web-antd/src/router/routes/modules/demos.ts
@@ -0,0 +1,28 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import { $t } from '#/locales';
+
+const routes: RouteRecordRaw[] = [
+ {
+ meta: {
+ icon: 'ic:baseline-view-in-ar',
+ keepAlive: true,
+ order: 1000,
+ title: $t('demos.title'),
+ },
+ name: 'Demos',
+ path: '/demos',
+ children: [
+ {
+ meta: {
+ title: $t('demos.antd'),
+ },
+ name: 'AntDesignDemos',
+ path: 'ant-design',
+ component: () => import('#/views/demos/antd/index.vue'),
+ },
+ ],
+ },
+];
+
+export default routes;
diff --git a/apps/web-antd/src/router/routes/modules/vben.ts b/apps/web-antd/src/router/routes/modules/vben.ts
index 98acf582..abcbee3e 100644
--- a/apps/web-antd/src/router/routes/modules/vben.ts
+++ b/apps/web-antd/src/router/routes/modules/vben.ts
@@ -1,12 +1,15 @@
import type { RouteRecordRaw } from 'vue-router';
import {
+ VBEN_ANTDV_NEXT_PREVIEW_URL,
VBEN_DOC_URL,
VBEN_ELE_PREVIEW_URL,
VBEN_GITHUB_URL,
VBEN_LOGO_URL,
VBEN_NAIVE_PREVIEW_URL,
+ VBEN_TD_PREVIEW_URL,
} from '@vben/constants';
+import { SvgAntdvNextLogoIcon, SvgTDesignIcon } from '@vben/icons';
import { IFrameView } from '#/layouts';
import { $t } from '#/locales';
@@ -24,7 +27,7 @@ const routes: RouteRecordRaw[] = [
children: [
{
name: 'VbenDocument',
- path: '/vben-admin/document',
+ path: 'document',
component: IFrameView,
meta: {
icon: 'lucide:book-open-text',
@@ -34,7 +37,7 @@ const routes: RouteRecordRaw[] = [
},
{
name: 'VbenGithub',
- path: '/vben-admin/github',
+ path: 'github',
component: IFrameView,
meta: {
icon: 'mdi:github',
@@ -42,9 +45,20 @@ const routes: RouteRecordRaw[] = [
title: 'Github',
},
},
+ {
+ name: 'VbenAntdVNext',
+ path: 'antdv-next',
+ component: IFrameView,
+ meta: {
+ badgeType: 'dot',
+ icon: SvgAntdvNextLogoIcon,
+ link: VBEN_ANTDV_NEXT_PREVIEW_URL,
+ title: $t('demos.vben.antdv-next'),
+ },
+ },
{
name: 'VbenNaive',
- path: '/vben-admin/naive',
+ path: 'naive',
component: IFrameView,
meta: {
badgeType: 'dot',
@@ -53,9 +67,20 @@ const routes: RouteRecordRaw[] = [
title: $t('demos.vben.naive-ui'),
},
},
+ {
+ name: 'VbenTDesign',
+ path: 'tdesign',
+ component: IFrameView,
+ meta: {
+ badgeType: 'dot',
+ icon: SvgTDesignIcon,
+ link: VBEN_TD_PREVIEW_URL,
+ title: $t('demos.vben.tdesign'),
+ },
+ },
{
name: 'VbenElementPlus',
- path: '/vben-admin/ele',
+ path: 'ele',
component: IFrameView,
meta: {
badgeType: 'dot',
@@ -76,6 +101,16 @@ const routes: RouteRecordRaw[] = [
order: 9999,
},
},
+ {
+ name: 'Profile',
+ path: '/profile',
+ component: () => import('#/views/_core/profile/index.vue'),
+ meta: {
+ icon: 'lucide:user',
+ hideInMenu: true,
+ title: $t('page.auth.profile'),
+ },
+ },
];
export default routes;
diff --git a/apps/web-antd/src/store/auth.ts b/apps/web-antd/src/store/auth.ts
index 099a873e..7f594daa 100644
--- a/apps/web-antd/src/store/auth.ts
+++ b/apps/web-antd/src/store/auth.ts
@@ -98,8 +98,7 @@ export const useAuthStore = defineStore('auth', () => {
}
async function fetchUserInfo() {
- let userInfo: null | UserInfo = null;
- userInfo = await getUserInfoApi();
+ const userInfo = await getUserInfoApi();
userStore.setUserInfo(userInfo);
return userInfo;
}
diff --git a/apps/web-antd/src/views/_core/authentication/code-login.vue b/apps/web-antd/src/views/_core/authentication/code-login.vue
index acfd1fd7..02370f7d 100644
--- a/apps/web-antd/src/views/_core/authentication/code-login.vue
+++ b/apps/web-antd/src/views/_core/authentication/code-login.vue
@@ -55,8 +55,7 @@ const formSchema = computed((): VbenFormSchema[] => {
* @param values 登录表单数据
*/
async function handleLogin(values: Recordable) {
- // eslint-disable-next-line no-console
- console.log(values);
+ void values;
}
diff --git a/apps/web-antd/src/views/_core/authentication/forget-password.vue b/apps/web-antd/src/views/_core/authentication/forget-password.vue
index fef0d427..10444d0a 100644
--- a/apps/web-antd/src/views/_core/authentication/forget-password.vue
+++ b/apps/web-antd/src/views/_core/authentication/forget-password.vue
@@ -29,8 +29,7 @@ const formSchema = computed((): VbenFormSchema[] => {
});
function handleSubmit(value: Recordable) {
- // eslint-disable-next-line no-console
- console.log('reset email:', value);
+ void value;
}
diff --git a/apps/web-antd/src/views/_core/authentication/register.vue b/apps/web-antd/src/views/_core/authentication/register.vue
index b1a5de72..a1175357 100644
--- a/apps/web-antd/src/views/_core/authentication/register.vue
+++ b/apps/web-antd/src/views/_core/authentication/register.vue
@@ -46,7 +46,7 @@ const formSchema = computed((): VbenFormSchema[] => {
rules(values) {
const { password } = values;
return z
- .string({ required_error: $t('authentication.passwordTip') })
+ .string({ error: $t('authentication.passwordTip') })
.min(1, { message: $t('authentication.passwordTip') })
.refine((value) => value === password, {
message: $t('authentication.confirmPasswordTip'),
@@ -82,8 +82,7 @@ const formSchema = computed((): VbenFormSchema[] => {
});
function handleSubmit(value: Recordable) {
- // eslint-disable-next-line no-console
- console.log('register submit:', value);
+ void value;
}
diff --git a/apps/web-antd/src/views/_core/profile/base-setting.vue b/apps/web-antd/src/views/_core/profile/base-setting.vue
new file mode 100644
index 00000000..aa8a4c26
--- /dev/null
+++ b/apps/web-antd/src/views/_core/profile/base-setting.vue
@@ -0,0 +1,65 @@
+
+
+
+
diff --git a/apps/web-antd/src/views/_core/profile/index.vue b/apps/web-antd/src/views/_core/profile/index.vue
new file mode 100644
index 00000000..8740894e
--- /dev/null
+++ b/apps/web-antd/src/views/_core/profile/index.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antd/src/views/_core/profile/notification-setting.vue b/apps/web-antd/src/views/_core/profile/notification-setting.vue
new file mode 100644
index 00000000..324a4b39
--- /dev/null
+++ b/apps/web-antd/src/views/_core/profile/notification-setting.vue
@@ -0,0 +1,31 @@
+
+
+
+
diff --git a/apps/web-antd/src/views/_core/profile/password-setting.vue b/apps/web-antd/src/views/_core/profile/password-setting.vue
new file mode 100644
index 00000000..2f3527d4
--- /dev/null
+++ b/apps/web-antd/src/views/_core/profile/password-setting.vue
@@ -0,0 +1,63 @@
+
+
+
+
diff --git a/apps/web-antd/src/views/_core/profile/security-setting.vue b/apps/web-antd/src/views/_core/profile/security-setting.vue
new file mode 100644
index 00000000..be30db58
--- /dev/null
+++ b/apps/web-antd/src/views/_core/profile/security-setting.vue
@@ -0,0 +1,43 @@
+
+
+
+
diff --git a/apps/web-antd/src/views/dashboard/analytics/analytics-visits-sales.vue b/apps/web-antd/src/views/dashboard/analytics/analytics-visits-sales.vue
index 02f50912..6ff52086 100644
--- a/apps/web-antd/src/views/dashboard/analytics/analytics-visits-sales.vue
+++ b/apps/web-antd/src/views/dashboard/analytics/analytics-visits-sales.vue
@@ -24,7 +24,7 @@ onMounted(() => {
{ name: '定制', value: 310 },
{ name: '技术支持', value: 274 },
{ name: '远程', value: 400 },
- ].sort((a, b) => {
+ ].toSorted((a, b) => {
return a.value - b.value;
}),
name: '商业占比',
diff --git a/apps/web-antd/src/views/dashboard/analytics/index.vue b/apps/web-antd/src/views/dashboard/analytics/index.vue
index 5e3d6d28..e794c99a 100644
--- a/apps/web-antd/src/views/dashboard/analytics/index.vue
+++ b/apps/web-antd/src/views/dashboard/analytics/index.vue
@@ -76,10 +76,10 @@ const chartTabs: TabOption[] = [
-
+
-
+
diff --git a/apps/web-antd/src/views/dashboard/workspace/index.vue b/apps/web-antd/src/views/dashboard/workspace/index.vue
index b95d6138..e99a37cd 100644
--- a/apps/web-antd/src/views/dashboard/workspace/index.vue
+++ b/apps/web-antd/src/views/dashboard/workspace/index.vue
@@ -244,7 +244,7 @@ function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
今日晴,20℃ - 32℃!
-
+
@@ -252,7 +252,7 @@ function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
diff --git a/apps/web-antd/src/views/demos/antd/index.vue b/apps/web-antd/src/views/demos/antd/index.vue
new file mode 100644
index 00000000..a1ff1ae2
--- /dev/null
+++ b/apps/web-antd/src/views/demos/antd/index.vue
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+ Default
+ Primary
+ Info
+ Error
+
+
+
+
+ 信息
+ 错误
+ 警告
+ 成功
+
+
+
+
+
+ 信息
+ 错误
+ 警告
+ 成功
+
+
+
+
diff --git a/apps/web-antd/tailwind.config.mjs b/apps/web-antd/tailwind.config.mjs
deleted file mode 100644
index f17f556f..00000000
--- a/apps/web-antd/tailwind.config.mjs
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from '@vben/tailwind-config';
diff --git a/apps/web-antd/tsconfig.json b/apps/web-antd/tsconfig.json
index 02c287fe..858a0ec0 100644
--- a/apps/web-antd/tsconfig.json
+++ b/apps/web-antd/tsconfig.json
@@ -2,7 +2,6 @@
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/web-app.json",
"compilerOptions": {
- "baseUrl": ".",
"paths": {
"#/*": ["./src/*"]
}
diff --git a/apps/web-antd/tsconfig.node.json b/apps/web-antd/tsconfig.node.json
index c2f0d86c..36e9fb5f 100644
--- a/apps/web-antd/tsconfig.node.json
+++ b/apps/web-antd/tsconfig.node.json
@@ -6,5 +6,5 @@
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"noEmit": false
},
- "include": ["vite.config.mts"]
+ "include": ["vite.config.ts"]
}
diff --git a/apps/web-antd/vite.config.mts b/apps/web-antd/vite.config.ts
similarity index 100%
rename from apps/web-antd/vite.config.mts
rename to apps/web-antd/vite.config.ts
diff --git a/apps/web-antdv-next/.env b/apps/web-antdv-next/.env
new file mode 100644
index 00000000..40b046d2
--- /dev/null
+++ b/apps/web-antdv-next/.env
@@ -0,0 +1,8 @@
+# 应用标题
+VITE_APP_TITLE=Vben Admin Antdv Next
+
+# 应用命名空间,用于缓存、store等功能的前缀,确保隔离
+VITE_APP_NAMESPACE=vben-web-antdv-next
+
+# 对store进行加密的密钥,在将store持久化到localStorage时会使用该密钥进行加密
+VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key
diff --git a/apps/web-antdv-next/.env.analyze b/apps/web-antdv-next/.env.analyze
new file mode 100644
index 00000000..ffafa8dd
--- /dev/null
+++ b/apps/web-antdv-next/.env.analyze
@@ -0,0 +1,7 @@
+# public path
+VITE_BASE=/
+
+# Basic interface address SPA
+VITE_GLOB_API_URL=/api
+
+VITE_VISUALIZER=true
diff --git a/apps/web-antdv-next/.env.development b/apps/web-antdv-next/.env.development
new file mode 100644
index 00000000..f2b44428
--- /dev/null
+++ b/apps/web-antdv-next/.env.development
@@ -0,0 +1,16 @@
+# 端口号
+VITE_PORT=5999
+
+VITE_BASE=/
+
+# 接口地址
+VITE_GLOB_API_URL=/api
+
+# 是否开启 Nitro Mock服务,true 为开启,false 为关闭
+VITE_NITRO_MOCK=true
+
+# 是否打开 devtools,true 为打开,false 为关闭
+VITE_DEVTOOLS=false
+
+# 是否注入全局loading
+VITE_INJECT_APP_LOADING=true
diff --git a/apps/web-antdv-next/.env.production b/apps/web-antdv-next/.env.production
new file mode 100644
index 00000000..5375847a
--- /dev/null
+++ b/apps/web-antdv-next/.env.production
@@ -0,0 +1,19 @@
+VITE_BASE=/
+
+# 接口地址
+VITE_GLOB_API_URL=https://mock-napi.vben.pro/api
+
+# 是否开启压缩,可以设置为 none, brotli, gzip
+VITE_COMPRESS=none
+
+# 是否开启 PWA
+VITE_PWA=false
+
+# vue-router 的模式
+VITE_ROUTER_HISTORY=hash
+
+# 是否注入全局loading
+VITE_INJECT_APP_LOADING=true
+
+# 打包后是否生成dist.zip
+VITE_ARCHIVER=true
diff --git a/apps/web-antdv-next/index.html b/apps/web-antdv-next/index.html
new file mode 100644
index 00000000..e3b9ae5c
--- /dev/null
+++ b/apps/web-antdv-next/index.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
%VITE_APP_TITLE%
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/package.json b/apps/web-antdv-next/package.json
new file mode 100644
index 00000000..928084d9
--- /dev/null
+++ b/apps/web-antdv-next/package.json
@@ -0,0 +1,50 @@
+{
+ "name": "@vben/web-antdv-next",
+ "version": "5.7.0",
+ "homepage": "https://vben.pro",
+ "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/vbenjs/vue-vben-admin.git",
+ "directory": "apps/web-antdv-next"
+ },
+ "license": "MIT",
+ "author": {
+ "name": "vben",
+ "email": "ann.vben@gmail.com",
+ "url": "https://github.com/anncwb"
+ },
+ "type": "module",
+ "scripts": {
+ "build": "pnpm vite build --mode production",
+ "build:analyze": "pnpm vite build --mode analyze",
+ "dev": "pnpm vite --mode development",
+ "preview": "vite preview",
+ "typecheck": "vue-tsc --noEmit --skipLibCheck"
+ },
+ "imports": {
+ "#/*": "./src/*"
+ },
+ "dependencies": {
+ "@vben/access": "workspace:*",
+ "@vben/common-ui": "workspace:*",
+ "@vben/constants": "workspace:*",
+ "@vben/hooks": "workspace:*",
+ "@vben/icons": "workspace:*",
+ "@vben/layouts": "workspace:*",
+ "@vben/locales": "workspace:*",
+ "@vben/plugins": "workspace:*",
+ "@vben/preferences": "workspace:*",
+ "@vben/request": "workspace:*",
+ "@vben/stores": "workspace:*",
+ "@vben/styles": "workspace:*",
+ "@vben/types": "workspace:*",
+ "@vben/utils": "workspace:*",
+ "@vueuse/core": "catalog:",
+ "antdv-next": "catalog:",
+ "dayjs": "catalog:",
+ "pinia": "catalog:",
+ "vue": "catalog:",
+ "vue-router": "catalog:"
+ }
+}
diff --git a/apps/web-antdv-next/public/favicon.ico b/apps/web-antdv-next/public/favicon.ico
new file mode 100644
index 00000000..fcf9818e
Binary files /dev/null and b/apps/web-antdv-next/public/favicon.ico differ
diff --git a/apps/web-antdv-next/src/adapter/component/index.ts b/apps/web-antdv-next/src/adapter/component/index.ts
new file mode 100644
index 00000000..6f40a6a4
--- /dev/null
+++ b/apps/web-antdv-next/src/adapter/component/index.ts
@@ -0,0 +1,787 @@
+/**
+ * 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
+ * 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
+ */
+
+/* eslint-disable vue/one-component-per-file */
+
+import type {
+ AutoCompleteProps,
+ ButtonProps,
+ CascaderProps,
+ CheckboxGroupProps,
+ CheckboxProps,
+ DatePickerProps,
+ DividerProps,
+ InputNumberProps,
+ InputProps,
+ MentionsProps,
+ RadioGroupProps,
+ RadioProps,
+ RangePickerProps,
+ RateProps,
+ SelectProps,
+ SpaceProps,
+ SwitchProps,
+ TextAreaProps,
+ TimePickerProps,
+ TreeSelectProps,
+ UploadChangeParam,
+ UploadFile,
+ UploadProps,
+} from 'antdv-next';
+
+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 {
+ computed,
+ defineAsyncComponent,
+ defineComponent,
+ h,
+ nextTick,
+ onMounted,
+ onUnmounted,
+ ref,
+ render,
+ unref,
+ watch,
+} from 'vue';
+
+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;
+};
+
+const AutoComplete = defineAsyncComponent(
+ () => import('antdv-next/dist/auto-complete/index'),
+);
+const Button = defineAsyncComponent(
+ () => import('antdv-next/dist/button/index'),
+);
+const Checkbox = defineAsyncComponent(
+ () => import('antdv-next/dist/checkbox/index'),
+);
+const CheckboxGroup = defineAsyncComponent(() =>
+ import('antdv-next/dist/checkbox/index').then((res) => res.CheckboxGroup),
+);
+const DatePicker = defineAsyncComponent(
+ () => import('antdv-next/dist/date-picker/index'),
+);
+const Divider = defineAsyncComponent(
+ () => import('antdv-next/dist/divider/index'),
+);
+const Input = defineAsyncComponent(() => import('antdv-next/dist/input/index'));
+const InputNumber = defineAsyncComponent(
+ () => import('antdv-next/dist/input-number/index'),
+);
+const InputPassword = defineAsyncComponent(() =>
+ import('antdv-next/dist/input/index').then((res) => res.InputPassword),
+);
+const Mentions = defineAsyncComponent(
+ () => import('antdv-next/dist/mentions/index'),
+);
+const Radio = defineAsyncComponent(() => import('antdv-next/dist/radio/index'));
+const RadioGroup = defineAsyncComponent(() =>
+ import('antdv-next/dist/radio/index').then((res) => res.RadioGroup),
+);
+const RangePicker = defineAsyncComponent(() =>
+ import('antdv-next/dist/date-picker/index').then(
+ (res) => res.DateRangePicker,
+ ),
+);
+const Rate = defineAsyncComponent(() => import('antdv-next/dist/rate/index'));
+const Select = defineAsyncComponent(
+ () => import('antdv-next/dist/select/index'),
+);
+const Space = defineAsyncComponent(() => import('antdv-next/dist/space/index'));
+const Switch = defineAsyncComponent(
+ () => import('antdv-next/dist/switch/index'),
+);
+const Textarea = defineAsyncComponent(
+ () => import('antdv-next/dist/input/TextArea'),
+);
+const TimePicker = defineAsyncComponent(
+ () => import('antdv-next/dist/time-picker/index'),
+);
+const TreeSelect = defineAsyncComponent(
+ () => import('antdv-next/dist/tree-select/index'),
+);
+const Cascader = defineAsyncComponent(
+ () => import('antdv-next/dist/cascader/index'),
+);
+const Upload = defineAsyncComponent(
+ () => import('antdv-next/dist/upload/index'),
+);
+const Image = defineAsyncComponent(() => import('antdv-next/dist/image/index'));
+const PreviewGroup = defineAsyncComponent(() =>
+ import('antdv-next/dist/image/index').then((res) => res.ImagePreviewGroup),
+);
+
+const withDefaultPlaceholder = (
+ component: Component,
+ type: 'input' | 'select',
+ componentProps: Recordable
= {},
+) => {
+ return defineComponent({
+ name: component.name,
+ inheritAttrs: false,
+ setup: (props: any, { attrs, expose, slots }) => {
+ const placeholder =
+ props?.placeholder ||
+ attrs?.placeholder ||
+ $t(`ui.placeholder.${type}`);
+ // 透传组件暴露的方法
+ const innerRef = ref();
+ expose(
+ new Proxy(
+ {},
+ {
+ get: (_target, key) => innerRef.value?.[key],
+ has: (_target, key) => key in (innerRef.value || {}),
+ },
+ ),
+ );
+ return () =>
+ h(
+ component,
+ { ...componentProps, placeholder, ...props, ...attrs, ref: innerRef },
+ slots,
+ );
+ },
+ });
+};
+
+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;
+ }
+ }
+ 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,
+ ),
+ };
+}
+
+/**
+ * 获取文件的 Base64
+ */
+function getBase64(file: File): Promise {
+ 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,
+ fileList: Ref,
+) {
+ // 非图片文件直接打开链接
+ 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,
+ {
+ classes: { popup: { root: '!z-2000' } },
+ 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);
+ }
+ },
+ },
+ },
+ () =>
+ imageFiles.map((imgFile) =>
+ h(ImageComponent, {
+ key: imgFile.uid,
+ src: imgFile.url || imgFile.preview,
+ }),
+ ),
+ );
+ };
+ },
+ };
+
+ render(h(PreviewWrapper), container);
+}
+
+/**
+ * 图片裁剪操作
+ */
+function cropImage(file: File, aspectRatio: string | undefined) {
+ return new Promise((resolve, reject) => {
+ const container = document.createElement('div');
+ document.body.append(container);
+
+ let isUnmounted = false;
+ let objectUrl: null | string = null;
+
+ const open = ref(true);
+ const cropperRef = ref | null>(null);
+
+ 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(
+ 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,
+ zIndex: 9999,
+ 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();
+ },
+ },
+ () =>
+ h(VCropper, {
+ ref: (ref: any) => (cropperRef.value = ref),
+ img: objectUrl as string,
+ aspectRatio,
+ }),
+ );
+ };
+ },
+ };
+
+ render(h(CropperWrapper), container);
+ });
+}
+
+/**
+ * 带预览功能的上传组件
+ */
+function withPreviewUpload() {
+ return defineComponent({
+ name: Upload.name,
+ emits: ['update:modelValue'],
+ setup(
+ props: any,
+ { attrs, slots, emit }: { attrs: any; emit: any; slots: any },
+ ) {
+ const previewVisible = ref(false);
+ const placeholder = attrs?.placeholder || $t('ui.placeholder.upload');
+ const listType = attrs?.listType || attrs?.['list-type'] || 'text';
+ const fileList = ref(
+ attrs?.fileList || attrs?.['file-list'] || [],
+ );
+
+ const maxSize = computed(() => attrs?.maxSize ?? attrs?.['max-size']);
+ const aspectRatio = computed(
+ () => attrs?.aspectRatio ?? attrs?.['aspect-ratio'],
+ );
+
+ async function handleBeforeUpload(
+ file: UploadFile,
+ originFileList: Array,
+ ) {
+ // 文件大小限制
+ 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 &&
+ originFileList[0] &&
+ isImageFile(file)
+ ) {
+ file.status = 'removed';
+ const blob = await cropImage(originFileList[0], aspectRatio.value);
+ if (!blob) {
+ throw new Error($t('ui.crop.errorTip'));
+ }
+ return blob;
+ }
+
+ return attrs.beforeUpload?.(file) ?? true;
+ }
+
+ function handleChange(event: UploadChangeParam) {
+ try {
+ attrs.handleChange?.(event);
+ attrs.onHandleChange?.(event);
+ } catch (error) {
+ console.error(error);
+ }
+ fileList.value = event.fileList.filter(
+ (file) => file.status !== 'removed',
+ );
+ emit(
+ 'update:modelValue',
+ event.fileList?.length ? fileList.value : undefined,
+ );
+ }
+
+ function handlePreview(file: UploadFile) {
+ previewVisible.value = true;
+ return previewImage(file, previewVisible, fileList);
+ }
+
+ function renderUploadButton() {
+ if (attrs.disabled) return null;
+ return isEmpty(slots)
+ ? createDefaultUploadSlots(listType, placeholder)
+ : slots;
+ }
+
+ // 拖拽排序
+ const draggable = computed(
+ () => (attrs.draggable ?? false) && !attrs.disabled,
+ );
+ const uploadId = `upload-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
+ const sortableInstance = ref(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);
+ }
+ }
+
+ 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();
+ }
+
+ // 监听表单值变化
+ watch(
+ () => attrs.modelValue,
+ (res) => {
+ fileList.value = res;
+ },
+ );
+
+ onMounted(initSortable);
+ onUnmounted(() => {
+ sortableInstance.value?.destroy();
+ removeDragStyle();
+ });
+
+ return () =>
+ h(
+ '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 =
+ | 'ApiCascader'
+ | 'ApiSelect'
+ | 'ApiTreeSelect'
+ | 'AutoComplete'
+ | 'Cascader'
+ | 'Checkbox'
+ | 'CheckboxGroup'
+ | 'CollapsibleParams'
+ | 'DatePicker'
+ | 'DefaultButton'
+ | 'Divider'
+ | 'IconPicker'
+ | 'Input'
+ | 'InputNumber'
+ | 'InputPassword'
+ | 'Mentions'
+ | 'PrimaryButton'
+ | 'Radio'
+ | 'RadioGroup'
+ | 'RangePicker'
+ | 'Rate'
+ | 'RichEditor'
+ | 'Select'
+ | 'Space'
+ | 'Switch'
+ | 'Textarea'
+ | 'TimePicker'
+ | 'TreeSelect'
+ | 'Upload'
+ | BaseFormComponentType;
+
+/**
+ * 与 {@link ComponentType} 中注册的组件名一一对应,便于 Schema 上 `component` + `componentProps` 联动提示
+ */
+export interface ComponentPropsMap {
+ ApiCascader: ApiComponentSharedProps & CascaderProps;
+ ApiSelect: ApiComponentSharedProps & SelectProps;
+ ApiTreeSelect: ApiComponentSharedProps & TreeSelectProps;
+ AutoComplete: AutoCompleteProps;
+ Cascader: CascaderProps;
+ Checkbox: CheckboxProps;
+ CheckboxGroup: CheckboxGroupProps;
+ CollapsibleParams: CollapsibleParamsProps;
+ DatePicker: DatePickerProps;
+ DefaultButton: ButtonProps;
+ Divider: DividerProps;
+ IconPicker: IconPickerProps;
+ Input: InputProps;
+ InputNumber: InputNumberProps;
+ InputPassword: InputProps;
+ Mentions: MentionsProps;
+ PrimaryButton: ButtonProps;
+ Radio: RadioProps;
+ RadioGroup: RadioGroupProps;
+ RangePicker: RangePickerProps;
+ Rate: RateProps;
+ RichEditor: TipTapProps;
+ Select: SelectProps;
+ Space: SpaceProps;
+ Switch: SwitchProps;
+ Textarea: TextAreaProps;
+ TimePicker: TimePickerProps;
+ TreeSelect: TreeSelectProps;
+ Upload: AdapterUploadProps;
+}
+
+async function initComponentAdapter() {
+ const components: Partial> = {
+ // 如果你的组件体积比较大,可以使用异步加载
+ // Button: () =>
+ // import('xxx').then((res) => res.Button),
+
+ ApiCascader: withDefaultPlaceholder(ApiComponent, 'select', {
+ component: Cascader,
+ fieldNames: { label: 'label', value: 'value', children: 'children' },
+ loadingSlot: 'suffixIcon',
+ modelPropName: 'value',
+ visibleEvent: 'onOpenChange',
+ }),
+ ApiSelect: withDefaultPlaceholder(ApiComponent, 'select', {
+ component: Select,
+ loadingSlot: 'suffixIcon',
+ modelPropName: 'value',
+ visibleEvent: 'onOpenChange',
+ }),
+ ApiTreeSelect: withDefaultPlaceholder(ApiComponent, 'select', {
+ component: TreeSelect,
+ fieldNames: { label: 'label', value: 'value', children: 'children' },
+ loadingSlot: 'suffixIcon',
+ modelPropName: 'value',
+ optionsPropName: 'treeData',
+ visibleEvent: 'onOpenChange',
+ }),
+ AutoComplete,
+ Cascader,
+ Checkbox,
+ CheckboxGroup,
+ DatePicker,
+ // 自定义默认按钮
+ DefaultButton: (props, { attrs, slots }) => {
+ return h(Button, { ...props, attrs, type: 'default' }, slots);
+ },
+ Divider,
+ IconPicker: withDefaultPlaceholder(IconPicker, 'select', {
+ iconSlot: 'addonAfter',
+ inputComponent: Input,
+ modelValueProp: 'value',
+ }),
+ Input: withDefaultPlaceholder(Input, 'input'),
+ InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
+ InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
+ Mentions: withDefaultPlaceholder(Mentions, 'input'),
+ // 自定义主要按钮
+ PrimaryButton: (props, { attrs, slots }) => {
+ return h(Button, { ...props, attrs, type: 'primary' }, slots);
+ },
+ Radio,
+ 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,
+ Textarea: withDefaultPlaceholder(Textarea, 'input'),
+ TimePicker,
+ TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
+ Upload: withPreviewUpload(),
+ CollapsibleParams: VbenCollapsibleParams,
+ };
+
+ // 将组件注册到全局共享状态中
+ globalShareState.setComponents(components);
+
+ // 定义全局共享状态中的消息提示
+ globalShareState.defineMessage({
+ // 复制成功消息提示
+ copyPreferencesSuccess: (title, content) => {
+ notification.success({
+ description: content,
+ title,
+ placement: 'bottomRight',
+ });
+ },
+ });
+}
+
+export { initComponentAdapter };
diff --git a/apps/web-antdv-next/src/adapter/form.ts b/apps/web-antdv-next/src/adapter/form.ts
new file mode 100644
index 00000000..9bbfd5c1
--- /dev/null
+++ b/apps/web-antdv-next/src/adapter/form.ts
@@ -0,0 +1,67 @@
+import type {
+ VbenFormProps as FormProps,
+ VbenFormSchema as FormSchema,
+ FormValues,
+} from '@vben/common-ui';
+
+import type { ComponentPropsMap, ComponentType } from './component';
+
+import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
+import { $t } from '@vben/locales';
+
+async function initSetupVbenForm() {
+ setupVbenForm({
+ config: {
+ // ant design vue组件库默认都是 v-model:value
+ baseModelPropName: 'value',
+
+ // 一些组件是 v-model:checked 或者 v-model:fileList
+ modelPropNameMap: {
+ Checkbox: 'checked',
+ Radio: 'checked',
+ Switch: 'checked',
+ Upload: 'fileList',
+ },
+ },
+ rules: {
+ // 输入项目必填国际化适配
+ required: (value, _params, ctx) => {
+ if (value === undefined || value === null || value.length === 0) {
+ return $t('ui.formRules.required', [ctx.label]);
+ }
+ return true;
+ },
+ // 选择项目必填国际化适配
+ selectRequired: (value, _params, ctx) => {
+ if (value === undefined || value === null) {
+ return $t('ui.formRules.selectRequired', [ctx.label]);
+ }
+ return true;
+ },
+ },
+ });
+}
+function useVbenForm<
+ TFormValues extends FormValues = FormValues,
+ TSubmitValues extends FormValues = TFormValues,
+>(
+ options: FormProps<
+ ComponentType,
+ ComponentPropsMap,
+ TFormValues,
+ TSubmitValues
+ >,
+) {
+ return useForm(
+ options,
+ );
+}
+
+export { initSetupVbenForm, useVbenForm, z };
+
+export type VbenFormSchema =
+ FormSchema;
+export type VbenFormProps<
+ TFormValues extends FormValues = FormValues,
+ TSubmitValues extends FormValues = TFormValues,
+> = FormProps;
diff --git a/apps/web-antdv-next/src/adapter/vxe-table.ts b/apps/web-antdv-next/src/adapter/vxe-table.ts
new file mode 100644
index 00000000..5f492158
--- /dev/null
+++ b/apps/web-antdv-next/src/adapter/vxe-table.ts
@@ -0,0 +1,93 @@
+import type { FormValues } from '@vben/common-ui';
+import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
+
+import type { ComponentPropsMap, ComponentType } from './component';
+
+import { h } from 'vue';
+
+import {
+ setupVbenVxeTable,
+ useVbenVxeGrid as useGrid,
+} from '@vben/plugins/vxe-table';
+
+import { Button, Image } from 'antdv-next';
+
+import { useVbenForm } from './form';
+
+setupVbenVxeTable({
+ configVxeTable: (vxeUI) => {
+ vxeUI.setConfig({
+ grid: {
+ align: 'center',
+ border: false,
+ columnConfig: {
+ resizable: true,
+ },
+ minHeight: 180,
+ formConfig: {
+ // 全局禁用vxe-table的表单配置,使用formOptions
+ enabled: false,
+ },
+ proxyConfig: {
+ autoLoad: true,
+ response: {
+ result: 'items',
+ total: 'total',
+ list: 'items',
+ },
+ showActiveMsg: true,
+ showResponseMsg: false,
+ },
+ round: true,
+ showOverflow: true,
+ size: 'small',
+ } as VxeTableGridOptions,
+ });
+
+ // 表格配置项可以用 cellRender: { name: 'CellImage' },
+ vxeUI.renderer.add('CellImage', {
+ renderTableDefault(renderOpts, params) {
+ const { props } = renderOpts;
+ const { column, row } = params;
+ return h(Image, { src: row[column.field], ...props });
+ },
+ });
+
+ // 表格配置项可以用 cellRender: { name: 'CellLink' },
+ vxeUI.renderer.add('CellLink', {
+ renderTableDefault(renderOpts) {
+ const { props } = renderOpts;
+ return h(
+ Button,
+ { size: 'small', type: 'link' },
+ { default: () => props?.text },
+ );
+ },
+ });
+
+ // 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
+ // vxeUI.formats.add
+ },
+ useVbenForm,
+});
+
+export const useVbenVxeGrid = <
+ T extends Record,
+ TFormValues extends FormValues = FormValues,
+ TSubmitValues extends FormValues = TFormValues,
+>(
+ ...rest: Parameters<
+ typeof useGrid<
+ T,
+ ComponentType,
+ ComponentPropsMap,
+ TFormValues,
+ TSubmitValues
+ >
+ >
+) =>
+ useGrid(
+ ...rest,
+ );
+
+export type * from '@vben/plugins/vxe-table';
diff --git a/apps/web-antdv-next/src/api/core/auth.ts b/apps/web-antdv-next/src/api/core/auth.ts
new file mode 100644
index 00000000..71d9f994
--- /dev/null
+++ b/apps/web-antdv-next/src/api/core/auth.ts
@@ -0,0 +1,51 @@
+import { baseRequestClient, requestClient } from '#/api/request';
+
+export namespace AuthApi {
+ /** 登录接口参数 */
+ export interface LoginParams {
+ password?: string;
+ username?: string;
+ }
+
+ /** 登录接口返回值 */
+ export interface LoginResult {
+ accessToken: string;
+ }
+
+ export interface RefreshTokenResult {
+ data: string;
+ status: number;
+ }
+}
+
+/**
+ * 登录
+ */
+export async function loginApi(data: AuthApi.LoginParams) {
+ return requestClient.post('/auth/login', data);
+}
+
+/**
+ * 刷新accessToken
+ */
+export async function refreshTokenApi() {
+ return baseRequestClient.post('/auth/refresh', {
+ withCredentials: true,
+ });
+}
+
+/**
+ * 退出登录
+ */
+export async function logoutApi() {
+ return baseRequestClient.post('/auth/logout', {
+ withCredentials: true,
+ });
+}
+
+/**
+ * 获取用户权限码
+ */
+export async function getAccessCodesApi() {
+ return requestClient.get('/auth/codes');
+}
diff --git a/apps/web-antdv-next/src/api/core/index.ts b/apps/web-antdv-next/src/api/core/index.ts
new file mode 100644
index 00000000..04256867
--- /dev/null
+++ b/apps/web-antdv-next/src/api/core/index.ts
@@ -0,0 +1,4 @@
+export * from './auth';
+export * from './menu';
+export * from './upload';
+export * from './user';
diff --git a/apps/web-antdv-next/src/api/core/menu.ts b/apps/web-antdv-next/src/api/core/menu.ts
new file mode 100644
index 00000000..9ef60b11
--- /dev/null
+++ b/apps/web-antdv-next/src/api/core/menu.ts
@@ -0,0 +1,10 @@
+import type { RouteRecordStringComponent } from '@vben/types';
+
+import { requestClient } from '#/api/request';
+
+/**
+ * 获取用户所有菜单
+ */
+export async function getAllMenusApi() {
+ return requestClient.get('/menu/all');
+}
diff --git a/apps/web-antdv-next/src/api/core/upload.ts b/apps/web-antdv-next/src/api/core/upload.ts
new file mode 100644
index 00000000..246d4f26
--- /dev/null
+++ b/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)));
+ }
+}
diff --git a/apps/web-antdv-next/src/api/core/user.ts b/apps/web-antdv-next/src/api/core/user.ts
new file mode 100644
index 00000000..7e28ea84
--- /dev/null
+++ b/apps/web-antdv-next/src/api/core/user.ts
@@ -0,0 +1,10 @@
+import type { UserInfo } from '@vben/types';
+
+import { requestClient } from '#/api/request';
+
+/**
+ * 获取用户信息
+ */
+export async function getUserInfoApi() {
+ return requestClient.get('/user/info');
+}
diff --git a/apps/web-antdv-next/src/api/index.ts b/apps/web-antdv-next/src/api/index.ts
new file mode 100644
index 00000000..4b0e0413
--- /dev/null
+++ b/apps/web-antdv-next/src/api/index.ts
@@ -0,0 +1 @@
+export * from './core';
diff --git a/apps/web-antdv-next/src/api/request.ts b/apps/web-antdv-next/src/api/request.ts
new file mode 100644
index 00000000..d97a2163
--- /dev/null
+++ b/apps/web-antdv-next/src/api/request.ts
@@ -0,0 +1,113 @@
+/**
+ * 该文件可自行根据业务逻辑进行调整
+ */
+import type { RequestClientOptions } from '@vben/request';
+
+import { useAppConfig } from '@vben/hooks';
+import { preferences } from '@vben/preferences';
+import {
+ authenticateResponseInterceptor,
+ defaultResponseInterceptor,
+ errorMessageResponseInterceptor,
+ RequestClient,
+} from '@vben/request';
+import { useAccessStore } from '@vben/stores';
+
+import { message } from 'antdv-next';
+
+import { useAuthStore } from '#/store';
+
+import { refreshTokenApi } from './core';
+
+const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
+
+function createRequestClient(baseURL: string, options?: RequestClientOptions) {
+ const client = new RequestClient({
+ ...options,
+ baseURL,
+ });
+
+ /**
+ * 重新认证逻辑
+ */
+ async function doReAuthenticate() {
+ console.warn('Access token or refresh token is invalid or expired. ');
+ const accessStore = useAccessStore();
+ const authStore = useAuthStore();
+ accessStore.setAccessToken(null);
+ if (
+ preferences.app.loginExpiredMode === 'modal' &&
+ accessStore.isAccessChecked
+ ) {
+ accessStore.setLoginExpired(true);
+ } else {
+ await authStore.logout();
+ }
+ }
+
+ /**
+ * 刷新token逻辑
+ */
+ async function doRefreshToken() {
+ const accessStore = useAccessStore();
+ const resp = await refreshTokenApi();
+ const newToken = resp.data;
+ accessStore.setAccessToken(newToken);
+ return newToken;
+ }
+
+ function formatToken(token: null | string) {
+ return token ? `Bearer ${token}` : null;
+ }
+
+ // 请求头处理
+ client.addRequestInterceptor({
+ fulfilled: async (config) => {
+ const accessStore = useAccessStore();
+
+ config.headers.Authorization = formatToken(accessStore.accessToken);
+ config.headers['Accept-Language'] = preferences.app.locale;
+ return config;
+ },
+ });
+
+ // 处理返回的响应数据格式
+ client.addResponseInterceptor(
+ defaultResponseInterceptor({
+ codeField: 'code',
+ dataField: 'data',
+ successCode: 0,
+ }),
+ );
+
+ // token过期的处理
+ client.addResponseInterceptor(
+ authenticateResponseInterceptor({
+ client,
+ doReAuthenticate,
+ doRefreshToken,
+ enableRefreshToken: preferences.app.enableRefreshToken,
+ formatToken,
+ }),
+ );
+
+ // 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
+ client.addResponseInterceptor(
+ errorMessageResponseInterceptor((msg: string, error) => {
+ // 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
+ // 当前mock接口返回的错误字段是 error 或者 message
+ const responseData = error?.response?.data ?? {};
+ const errorMessage = responseData?.error ?? responseData?.message ?? '';
+ // 如果没有错误信息,则会根据状态码进行提示
+ message.error(errorMessage || msg);
+ }),
+ );
+
+ return client;
+}
+
+export const requestClient = createRequestClient(apiURL, {
+ responseReturn: 'data',
+});
+
+export const baseRequestClient = new RequestClient({ baseURL: apiURL });
diff --git a/apps/web-antdv-next/src/app.vue b/apps/web-antdv-next/src/app.vue
new file mode 100644
index 00000000..46366e91
--- /dev/null
+++ b/apps/web-antdv-next/src/app.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/bootstrap.ts b/apps/web-antdv-next/src/bootstrap.ts
new file mode 100644
index 00000000..8c161703
--- /dev/null
+++ b/apps/web-antdv-next/src/bootstrap.ts
@@ -0,0 +1,76 @@
+import { createApp, watchEffect } from 'vue';
+
+import { registerAccessDirective } from '@vben/access';
+import { registerLoadingDirective } from '@vben/common-ui/es/loading';
+import { preferences } from '@vben/preferences';
+import { initStores } from '@vben/stores';
+import '@vben/styles';
+import '@vben/styles/antdv-next';
+
+import { useTitle } from '@vueuse/core';
+
+import { $t, setupI18n } from '#/locales';
+
+import { initComponentAdapter } from './adapter/component';
+import { initSetupVbenForm } from './adapter/form';
+import App from './app.vue';
+import { router } from './router';
+
+async function bootstrap(namespace: string) {
+ // 初始化组件适配器
+ await initComponentAdapter();
+
+ // 初始化表单组件
+ await initSetupVbenForm();
+
+ // // 设置弹窗的默认配置
+ // setDefaultModalProps({
+ // fullscreenButton: false,
+ // });
+ // // 设置抽屉的默认配置
+ // setDefaultDrawerProps({
+ // zIndex: 1020,
+ // });
+
+ const app = createApp(App);
+
+ // 注册v-loading指令
+ registerLoadingDirective(app, {
+ loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令
+ spinning: 'spinning',
+ });
+
+ // 国际化 i18n 配置
+ await setupI18n(app);
+
+ // 配置 pinia-tore
+ await initStores(app, { namespace });
+
+ // 安装权限指令
+ registerAccessDirective(app);
+
+ // 初始化 tippy
+ const { initTippy } = await import('@vben/common-ui/es/tippy');
+ initTippy(app);
+
+ // 配置路由及路由守卫
+ app.use(router);
+
+ // 配置Motion插件
+ const { MotionPlugin } = await import('@vben/plugins/motion');
+ app.use(MotionPlugin);
+
+ // 动态更新标题
+ watchEffect(() => {
+ if (preferences.app.dynamicTitle) {
+ const routeTitle = router.currentRoute.value.meta?.title;
+ const pageTitle =
+ (routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name;
+ useTitle(pageTitle);
+ }
+ });
+
+ app.mount('#app');
+}
+
+export { bootstrap };
diff --git a/apps/web-antdv-next/src/layouts/auth.vue b/apps/web-antdv-next/src/layouts/auth.vue
new file mode 100644
index 00000000..8ba66e85
--- /dev/null
+++ b/apps/web-antdv-next/src/layouts/auth.vue
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/layouts/basic.vue b/apps/web-antdv-next/src/layouts/basic.vue
new file mode 100644
index 00000000..e85dd1cf
--- /dev/null
+++ b/apps/web-antdv-next/src/layouts/basic.vue
@@ -0,0 +1,261 @@
+
+
+
+
+
+
+
+
+ item.id && markRead(item.id)"
+ @remove="(item) => item.id && remove(item.id)"
+ @make-all="handleMakeAll"
+ @on-click="handleClick"
+ @view-all="viewAll"
+ />
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/layouts/index.ts b/apps/web-antdv-next/src/layouts/index.ts
new file mode 100644
index 00000000..a4320780
--- /dev/null
+++ b/apps/web-antdv-next/src/layouts/index.ts
@@ -0,0 +1,6 @@
+const BasicLayout = () => import('./basic.vue');
+const AuthPageLayout = () => import('./auth.vue');
+
+const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);
+
+export { AuthPageLayout, BasicLayout, IFrameView };
diff --git a/apps/web-antdv-next/src/locales/README.md b/apps/web-antdv-next/src/locales/README.md
new file mode 100644
index 00000000..7b451032
--- /dev/null
+++ b/apps/web-antdv-next/src/locales/README.md
@@ -0,0 +1,3 @@
+# locale
+
+每个app使用的国际化可能不同,这里用于扩展国际化的功能,例如扩展 dayjs、antd组件库的多语言切换,以及app本身的国际化文件。
diff --git a/apps/web-antdv-next/src/locales/index.ts b/apps/web-antdv-next/src/locales/index.ts
new file mode 100644
index 00000000..2586d859
--- /dev/null
+++ b/apps/web-antdv-next/src/locales/index.ts
@@ -0,0 +1,102 @@
+import type { Locale } from 'antdv-next/dist/locale/index';
+
+import type { App } from 'vue';
+
+import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
+
+import { ref } from 'vue';
+
+import {
+ $t,
+ setupI18n as coreSetup,
+ loadLocalesMapFromDir,
+} from '@vben/locales';
+import { preferences } from '@vben/preferences';
+
+import antdEnLocale from 'antdv-next/dist/locale/en_US';
+import antdDefaultLocale from 'antdv-next/dist/locale/zh_CN';
+import dayjs from 'dayjs';
+
+const antdLocale = ref(antdDefaultLocale);
+
+const modules = import.meta.glob('./langs/**/*.json');
+
+const localesMap = loadLocalesMapFromDir(
+ /\.\/langs\/([^/]+)\/(.*)\.json$/,
+ modules,
+);
+/**
+ * 加载应用特有的语言包
+ * 这里也可以改造为从服务端获取翻译数据
+ * @param lang
+ */
+async function loadMessages(lang: SupportedLanguagesType) {
+ const [appLocaleMessages] = await Promise.all([
+ localesMap[lang]?.(),
+ loadThirdPartyMessage(lang),
+ ]);
+ return appLocaleMessages?.default;
+}
+
+/**
+ * 加载第三方组件库的语言包
+ * @param lang
+ */
+async function loadThirdPartyMessage(lang: SupportedLanguagesType) {
+ await Promise.all([loadAntdLocale(lang), loadDayjsLocale(lang)]);
+}
+
+/**
+ * 加载dayjs的语言包
+ * @param lang
+ */
+async function loadDayjsLocale(lang: SupportedLanguagesType) {
+ let locale;
+ switch (lang) {
+ case 'en-US': {
+ locale = await import('dayjs/locale/en');
+ break;
+ }
+ case 'zh-CN': {
+ locale = await import('dayjs/locale/zh-cn');
+ break;
+ }
+ // 默认使用英语
+ default: {
+ locale = await import('dayjs/locale/en');
+ }
+ }
+ if (locale) {
+ dayjs.locale(locale);
+ } else {
+ console.error(`Failed to load dayjs locale for ${lang}`);
+ }
+}
+
+/**
+ * 加载antd的语言包
+ * @param lang
+ */
+async function loadAntdLocale(lang: SupportedLanguagesType) {
+ switch (lang) {
+ case 'en-US': {
+ antdLocale.value = antdEnLocale;
+ break;
+ }
+ case 'zh-CN': {
+ antdLocale.value = antdDefaultLocale;
+ break;
+ }
+ }
+}
+
+async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
+ await coreSetup(app, {
+ defaultLocale: preferences.app.locale,
+ loadMessages,
+ missingWarn: !import.meta.env.PROD,
+ ...options,
+ });
+}
+
+export { $t, antdLocale, setupI18n };
diff --git a/apps/web-antdv-next/src/locales/langs/en-US/demos.json b/apps/web-antdv-next/src/locales/langs/en-US/demos.json
new file mode 100644
index 00000000..551f22b4
--- /dev/null
+++ b/apps/web-antdv-next/src/locales/langs/en-US/demos.json
@@ -0,0 +1,14 @@
+{
+ "title": "Demos",
+ "antd": "Antdv Next",
+ "vben": {
+ "title": "Project",
+ "about": "About",
+ "document": "Document",
+ "antdv": "Ant Design Vue Version",
+ "antdv-next": "Antdv Next Version",
+ "naive-ui": "Naive UI Version",
+ "element-plus": "Element Plus Version",
+ "tdesign": "TDesign Vue Version"
+ }
+}
diff --git a/apps/web-antdv-next/src/locales/langs/en-US/page.json b/apps/web-antdv-next/src/locales/langs/en-US/page.json
new file mode 100644
index 00000000..39f1641c
--- /dev/null
+++ b/apps/web-antdv-next/src/locales/langs/en-US/page.json
@@ -0,0 +1,15 @@
+{
+ "auth": {
+ "login": "Login",
+ "register": "Register",
+ "codeLogin": "Code Login",
+ "qrcodeLogin": "Qr Code Login",
+ "forgetPassword": "Forget Password",
+ "profile": "Profile"
+ },
+ "dashboard": {
+ "title": "Dashboard",
+ "analytics": "Analytics",
+ "workspace": "Workspace"
+ }
+}
diff --git a/apps/web-antdv-next/src/locales/langs/zh-CN/demos.json b/apps/web-antdv-next/src/locales/langs/zh-CN/demos.json
new file mode 100644
index 00000000..ef4e43fd
--- /dev/null
+++ b/apps/web-antdv-next/src/locales/langs/zh-CN/demos.json
@@ -0,0 +1,14 @@
+{
+ "title": "演示",
+ "antd": "Antdv Next",
+ "vben": {
+ "title": "项目",
+ "about": "关于",
+ "document": "文档",
+ "antdv": "Ant Design Vue 版本",
+ "antdv-next": "Antdv Next 版本",
+ "naive-ui": "Naive UI 版本",
+ "element-plus": "Element Plus 版本",
+ "tdesign": "TDesign Vue 版本"
+ }
+}
diff --git a/apps/web-antdv-next/src/locales/langs/zh-CN/page.json b/apps/web-antdv-next/src/locales/langs/zh-CN/page.json
new file mode 100644
index 00000000..2192d1d5
--- /dev/null
+++ b/apps/web-antdv-next/src/locales/langs/zh-CN/page.json
@@ -0,0 +1,15 @@
+{
+ "auth": {
+ "login": "登录",
+ "register": "注册",
+ "codeLogin": "验证码登录",
+ "qrcodeLogin": "二维码登录",
+ "forgetPassword": "忘记密码",
+ "profile": "个人中心"
+ },
+ "dashboard": {
+ "title": "概览",
+ "analytics": "分析页",
+ "workspace": "工作台"
+ }
+}
diff --git a/apps/web-antdv-next/src/main.ts b/apps/web-antdv-next/src/main.ts
new file mode 100644
index 00000000..5d728a02
--- /dev/null
+++ b/apps/web-antdv-next/src/main.ts
@@ -0,0 +1,31 @@
+import { initPreferences } from '@vben/preferences';
+import { unmountGlobalLoading } from '@vben/utils';
+
+import { overridesPreferences } from './preferences';
+
+/**
+ * 应用初始化完成之后再进行页面加载渲染
+ */
+async function initApplication() {
+ // name用于指定项目唯一标识
+ // 用于区分不同项目的偏好设置以及存储数据的key前缀以及其他一些需要隔离的数据
+ const env = import.meta.env.PROD ? 'prod' : 'dev';
+ const appVersion = import.meta.env.VITE_APP_VERSION;
+ const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${appVersion}-${env}`;
+
+ // app偏好设置初始化
+ await initPreferences({
+ namespace,
+ overrides: overridesPreferences,
+ });
+
+ // 启动应用并挂载
+ // vue应用主要逻辑及视图
+ const { bootstrap } = await import('./bootstrap');
+ await bootstrap(namespace);
+
+ // 移除并销毁loading
+ unmountGlobalLoading();
+}
+
+initApplication();
diff --git a/apps/web-antdv-next/src/preferences.ts b/apps/web-antdv-next/src/preferences.ts
new file mode 100644
index 00000000..303c496e
--- /dev/null
+++ b/apps/web-antdv-next/src/preferences.ts
@@ -0,0 +1,17 @@
+import {
+ appCopyrightPreferences,
+ defineOverridesPreferences,
+} from '@vben/preferences';
+
+/**
+ * @description 项目配置文件
+ * 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
+ * !!! 更改配置后请清空缓存,否则可能不生效
+ */
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ app: {
+ name: import.meta.env.VITE_APP_TITLE,
+ },
+ copyright: appCopyrightPreferences,
+});
diff --git a/apps/web-antdv-next/src/router/access.ts b/apps/web-antdv-next/src/router/access.ts
new file mode 100644
index 00000000..e9e98511
--- /dev/null
+++ b/apps/web-antdv-next/src/router/access.ts
@@ -0,0 +1,42 @@
+import type {
+ ComponentRecordType,
+ GenerateMenuAndRoutesOptions,
+} from '@vben/types';
+
+import { generateAccessible } from '@vben/access';
+import { preferences } from '@vben/preferences';
+
+import { message } from 'antdv-next';
+
+import { getAllMenusApi } from '#/api';
+import { BasicLayout, IFrameView } from '#/layouts';
+import { $t } from '#/locales';
+
+const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
+
+async function generateAccess(options: GenerateMenuAndRoutesOptions) {
+ const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
+
+ const layoutMap: ComponentRecordType = {
+ BasicLayout,
+ IFrameView,
+ };
+
+ return await generateAccessible(preferences.app.accessMode, {
+ ...options,
+ fetchMenuListAsync: async () => {
+ message.loading({
+ content: `${$t('common.loadingMenu')}...`,
+ duration: 1.5,
+ });
+ return await getAllMenusApi();
+ },
+ // 可以指定没有权限跳转403页面
+ forbiddenComponent,
+ // 如果 route.meta.menuVisibleWithForbidden = true
+ layoutMap,
+ pageMap,
+ });
+}
+
+export { generateAccess };
diff --git a/apps/web-antdv-next/src/router/guard.ts b/apps/web-antdv-next/src/router/guard.ts
new file mode 100644
index 00000000..a1ad6d88
--- /dev/null
+++ b/apps/web-antdv-next/src/router/guard.ts
@@ -0,0 +1,133 @@
+import type { Router } from 'vue-router';
+
+import { LOGIN_PATH } from '@vben/constants';
+import { preferences } from '@vben/preferences';
+import { useAccessStore, useUserStore } from '@vben/stores';
+import { startProgress, stopProgress } from '@vben/utils';
+
+import { accessRoutes, coreRouteNames } from '#/router/routes';
+import { useAuthStore } from '#/store';
+
+import { generateAccess } from './access';
+
+/**
+ * 通用守卫配置
+ * @param router
+ */
+function setupCommonGuard(router: Router) {
+ // 记录已经加载的页面
+ const loadedPaths = new Set();
+
+ router.beforeEach((to) => {
+ to.meta.loaded = loadedPaths.has(to.path);
+
+ // 页面加载进度条
+ if (!to.meta.loaded && preferences.transition.progress) {
+ startProgress();
+ }
+ return true;
+ });
+
+ router.afterEach((to) => {
+ // 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
+
+ loadedPaths.add(to.path);
+
+ // 关闭页面加载进度条
+ if (preferences.transition.progress) {
+ stopProgress();
+ }
+ });
+}
+
+/**
+ * 权限访问守卫配置
+ * @param router
+ */
+function setupAccessGuard(router: Router) {
+ router.beforeEach(async (to, from) => {
+ const accessStore = useAccessStore();
+ const userStore = useUserStore();
+ const authStore = useAuthStore();
+
+ // 基本路由,这些路由不需要进入权限拦截
+ if (coreRouteNames.includes(to.name as string)) {
+ if (to.path === LOGIN_PATH && accessStore.accessToken) {
+ return decodeURIComponent(
+ (to.query?.redirect as string) ||
+ userStore.userInfo?.homePath ||
+ preferences.app.defaultHomePath,
+ );
+ }
+ return true;
+ }
+
+ // accessToken 检查
+ if (!accessStore.accessToken) {
+ // 明确声明忽略权限访问权限,则可以访问
+ if (to.meta.ignoreAccess) {
+ return true;
+ }
+
+ // 没有访问权限,跳转登录页面
+ if (to.fullPath !== LOGIN_PATH) {
+ return {
+ path: LOGIN_PATH,
+ // 如不需要,直接删除 query
+ query:
+ to.fullPath === preferences.app.defaultHomePath
+ ? {}
+ : { redirect: encodeURIComponent(to.fullPath) },
+ // 携带当前跳转的页面,登录后重新跳转该页面
+ replace: true,
+ };
+ }
+ return to;
+ }
+
+ // 是否已经生成过动态路由
+ if (accessStore.isAccessChecked) {
+ return true;
+ }
+
+ // 生成路由表
+ // 当前登录用户拥有的角色标识列表
+ const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
+ const userRoles = userInfo.roles ?? [];
+
+ // 生成菜单和路由
+ const { accessibleMenus, accessibleRoutes } = await generateAccess({
+ roles: userRoles,
+ router,
+ // 则会在菜单中显示,但是访问会被重定向到403
+ routes: accessRoutes,
+ });
+
+ // 保存菜单信息和路由信息
+ accessStore.setAccessMenus(accessibleMenus);
+ accessStore.setAccessRoutes(accessibleRoutes);
+ accessStore.setIsAccessChecked(true);
+ const redirectPath = (from.query.redirect ??
+ (to.path === preferences.app.defaultHomePath
+ ? userInfo.homePath || preferences.app.defaultHomePath
+ : to.fullPath)) as string;
+
+ return {
+ ...router.resolve(decodeURIComponent(redirectPath)),
+ replace: true,
+ };
+ });
+}
+
+/**
+ * 项目守卫配置
+ * @param router
+ */
+function createRouterGuard(router: Router) {
+ /** 通用 */
+ setupCommonGuard(router);
+ /** 权限访问 */
+ setupAccessGuard(router);
+}
+
+export { createRouterGuard };
diff --git a/apps/web-antdv-next/src/router/index.ts b/apps/web-antdv-next/src/router/index.ts
new file mode 100644
index 00000000..48402303
--- /dev/null
+++ b/apps/web-antdv-next/src/router/index.ts
@@ -0,0 +1,37 @@
+import {
+ createRouter,
+ createWebHashHistory,
+ createWebHistory,
+} from 'vue-router';
+
+import { resetStaticRoutes } from '@vben/utils';
+
+import { createRouterGuard } from './guard';
+import { routes } from './routes';
+
+/**
+ * @zh_CN 创建vue-router实例
+ */
+const router = createRouter({
+ history:
+ import.meta.env.VITE_ROUTER_HISTORY === 'hash'
+ ? createWebHashHistory(import.meta.env.VITE_BASE)
+ : createWebHistory(import.meta.env.VITE_BASE),
+ // 应该添加到路由的初始路由列表。
+ routes,
+ scrollBehavior: (to, _from, savedPosition) => {
+ if (savedPosition) {
+ return savedPosition;
+ }
+ return to.hash ? { behavior: 'smooth', el: to.hash } : { left: 0, top: 0 };
+ },
+ // 是否应该禁止尾部斜杠。
+ // strict: true,
+});
+
+const resetRoutes = () => resetStaticRoutes(router, routes);
+
+// 创建路由守卫
+createRouterGuard(router);
+
+export { resetRoutes, router };
diff --git a/apps/web-antdv-next/src/router/routes/core.ts b/apps/web-antdv-next/src/router/routes/core.ts
new file mode 100644
index 00000000..949b0b65
--- /dev/null
+++ b/apps/web-antdv-next/src/router/routes/core.ts
@@ -0,0 +1,97 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import { LOGIN_PATH } from '@vben/constants';
+import { preferences } from '@vben/preferences';
+
+import { $t } from '#/locales';
+
+const BasicLayout = () => import('#/layouts/basic.vue');
+const AuthPageLayout = () => import('#/layouts/auth.vue');
+/** 全局404页面 */
+const fallbackNotFoundRoute: RouteRecordRaw = {
+ component: () => import('#/views/_core/fallback/not-found.vue'),
+ meta: {
+ hideInBreadcrumb: true,
+ hideInMenu: true,
+ hideInTab: true,
+ title: '404',
+ },
+ name: 'FallbackNotFound',
+ path: '/:path(.*)*',
+};
+
+/** 基本路由,这些路由是必须存在的 */
+const coreRoutes: RouteRecordRaw[] = [
+ /**
+ * 根路由
+ * 使用基础布局,作为所有页面的父级容器,子级就不必配置BasicLayout。
+ * 此路由必须存在,且不应修改
+ */
+ {
+ component: BasicLayout,
+ meta: {
+ hideInBreadcrumb: true,
+ title: 'Root',
+ },
+ name: 'Root',
+ path: '/',
+ redirect: preferences.app.defaultHomePath,
+ children: [],
+ },
+ {
+ component: AuthPageLayout,
+ meta: {
+ hideInTab: true,
+ title: 'Authentication',
+ },
+ name: 'Authentication',
+ path: '/auth',
+ redirect: LOGIN_PATH,
+ children: [
+ {
+ name: 'Login',
+ path: 'login',
+ component: () => import('#/views/_core/authentication/login.vue'),
+ meta: {
+ title: $t('page.auth.login'),
+ },
+ },
+ {
+ name: 'CodeLogin',
+ path: 'code-login',
+ component: () => import('#/views/_core/authentication/code-login.vue'),
+ meta: {
+ title: $t('page.auth.codeLogin'),
+ },
+ },
+ {
+ name: 'QrCodeLogin',
+ path: 'qrcode-login',
+ component: () =>
+ import('#/views/_core/authentication/qrcode-login.vue'),
+ meta: {
+ title: $t('page.auth.qrcodeLogin'),
+ },
+ },
+ {
+ name: 'ForgetPassword',
+ path: 'forget-password',
+ component: () =>
+ import('#/views/_core/authentication/forget-password.vue'),
+ meta: {
+ title: $t('page.auth.forgetPassword'),
+ },
+ },
+ {
+ name: 'Register',
+ path: 'register',
+ component: () => import('#/views/_core/authentication/register.vue'),
+ meta: {
+ title: $t('page.auth.register'),
+ },
+ },
+ ],
+ },
+];
+
+export { coreRoutes, fallbackNotFoundRoute };
diff --git a/apps/web-antdv-next/src/router/routes/index.ts b/apps/web-antdv-next/src/router/routes/index.ts
new file mode 100644
index 00000000..e6fb1440
--- /dev/null
+++ b/apps/web-antdv-next/src/router/routes/index.ts
@@ -0,0 +1,37 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import { mergeRouteModules, traverseTreeValues } from '@vben/utils';
+
+import { coreRoutes, fallbackNotFoundRoute } from './core';
+
+const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
+ eager: true,
+});
+
+// 有需要可以自行打开注释,并创建文件夹
+// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
+// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
+
+/** 动态路由 */
+const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
+
+/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
+// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
+// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
+const staticRoutes: RouteRecordRaw[] = [];
+const externalRoutes: RouteRecordRaw[] = [];
+
+/** 路由列表,由基本路由、外部路由和404兜底路由组成
+ * 无需走权限验证(会一直显示在菜单中) */
+const routes: RouteRecordRaw[] = [
+ ...coreRoutes,
+ ...externalRoutes,
+ fallbackNotFoundRoute,
+];
+
+/** 基本路由列表,这些路由不需要进入权限拦截 */
+const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
+
+/** 有权限校验的路由列表,包含动态路由和静态路由 */
+const accessRoutes = [...dynamicRoutes, ...staticRoutes];
+export { accessRoutes, coreRouteNames, routes };
diff --git a/apps/web-antdv-next/src/router/routes/modules/dashboard.ts b/apps/web-antdv-next/src/router/routes/modules/dashboard.ts
new file mode 100644
index 00000000..44f15ef9
--- /dev/null
+++ b/apps/web-antdv-next/src/router/routes/modules/dashboard.ts
@@ -0,0 +1,38 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import { $t } from '#/locales';
+
+const routes: RouteRecordRaw[] = [
+ {
+ meta: {
+ icon: 'lucide:layout-dashboard',
+ order: -1,
+ title: $t('page.dashboard.title'),
+ },
+ name: 'Dashboard',
+ path: '/dashboard',
+ children: [
+ {
+ name: 'Analytics',
+ path: 'analytics',
+ component: () => import('#/views/dashboard/analytics/index.vue'),
+ meta: {
+ affixTab: true,
+ icon: 'lucide:area-chart',
+ title: $t('page.dashboard.analytics'),
+ },
+ },
+ {
+ name: 'Workspace',
+ path: 'workspace',
+ component: () => import('#/views/dashboard/workspace/index.vue'),
+ meta: {
+ icon: 'carbon:workspace',
+ title: $t('page.dashboard.workspace'),
+ },
+ },
+ ],
+ },
+];
+
+export default routes;
diff --git a/apps/web-antdv-next/src/router/routes/modules/demos.ts b/apps/web-antdv-next/src/router/routes/modules/demos.ts
new file mode 100644
index 00000000..18b756c7
--- /dev/null
+++ b/apps/web-antdv-next/src/router/routes/modules/demos.ts
@@ -0,0 +1,28 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import { $t } from '#/locales';
+
+const routes: RouteRecordRaw[] = [
+ {
+ meta: {
+ icon: 'ic:baseline-view-in-ar',
+ keepAlive: true,
+ order: 1000,
+ title: $t('demos.title'),
+ },
+ name: 'Demos',
+ path: '/demos',
+ children: [
+ {
+ meta: {
+ title: $t('demos.antd'),
+ },
+ name: 'AntDesignDemos',
+ path: 'ant-design-next',
+ component: () => import('#/views/demos/antd/index.vue'),
+ },
+ ],
+ },
+];
+
+export default routes;
diff --git a/apps/web-antdv-next/src/router/routes/modules/vben.ts b/apps/web-antdv-next/src/router/routes/modules/vben.ts
new file mode 100644
index 00000000..1f3e238b
--- /dev/null
+++ b/apps/web-antdv-next/src/router/routes/modules/vben.ts
@@ -0,0 +1,116 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import {
+ VBEN_ANT_PREVIEW_URL,
+ VBEN_DOC_URL,
+ VBEN_ELE_PREVIEW_URL,
+ VBEN_GITHUB_URL,
+ VBEN_LOGO_URL,
+ VBEN_NAIVE_PREVIEW_URL,
+ VBEN_TD_PREVIEW_URL,
+} from '@vben/constants';
+import { SvgAntdvLogoIcon, SvgTDesignIcon } from '@vben/icons';
+
+import { IFrameView } from '#/layouts';
+import { $t } from '#/locales';
+
+const routes: RouteRecordRaw[] = [
+ {
+ meta: {
+ badgeType: 'dot',
+ icon: VBEN_LOGO_URL,
+ order: 9998,
+ title: $t('demos.vben.title'),
+ },
+ name: 'VbenProject',
+ path: '/vben-admin',
+ children: [
+ {
+ name: 'VbenDocument',
+ path: 'document',
+ component: IFrameView,
+ meta: {
+ icon: 'lucide:book-open-text',
+ link: VBEN_DOC_URL,
+ title: $t('demos.vben.document'),
+ },
+ },
+ {
+ name: 'VbenGithub',
+ path: 'github',
+ component: IFrameView,
+ meta: {
+ icon: 'mdi:github',
+ link: VBEN_GITHUB_URL,
+ title: 'Github',
+ },
+ },
+ {
+ name: 'VbenAntd',
+ path: 'antd',
+ component: IFrameView,
+ meta: {
+ badgeType: 'dot',
+ icon: SvgAntdvLogoIcon,
+ link: VBEN_ANT_PREVIEW_URL,
+ title: $t('demos.vben.antdv'),
+ },
+ },
+ {
+ name: 'VbenNaive',
+ path: 'naive',
+ component: IFrameView,
+ meta: {
+ badgeType: 'dot',
+ icon: 'logos:naiveui',
+ link: VBEN_NAIVE_PREVIEW_URL,
+ title: $t('demos.vben.naive-ui'),
+ },
+ },
+ {
+ name: 'VbenTDesign',
+ path: 'tdesign',
+ component: IFrameView,
+ meta: {
+ badgeType: 'dot',
+ icon: SvgTDesignIcon,
+ link: VBEN_TD_PREVIEW_URL,
+ title: $t('demos.vben.tdesign'),
+ },
+ },
+ {
+ name: 'VbenElementPlus',
+ path: 'ele',
+ component: IFrameView,
+ meta: {
+ badgeType: 'dot',
+ icon: 'logos:element',
+ link: VBEN_ELE_PREVIEW_URL,
+ title: $t('demos.vben.element-plus'),
+ },
+ },
+ ],
+ },
+ {
+ name: 'VbenAbout',
+ path: '/vben-admin/about',
+ component: () => import('#/views/_core/about/index.vue'),
+ meta: {
+ icon: 'lucide:copyright',
+ title: $t('demos.vben.about'),
+ order: 9999,
+ },
+ },
+ {
+ name: 'Profile',
+ path: '/profile',
+ component: () => import('#/views/_core/profile/index.vue'),
+ meta: {
+ icon: 'lucide:user',
+ hideInMenu: true,
+ title: $t('page.auth.profile'),
+ },
+ },
+];
+
+export default routes;
diff --git a/apps/web-antdv-next/src/store/auth.ts b/apps/web-antdv-next/src/store/auth.ts
new file mode 100644
index 00000000..90af35c8
--- /dev/null
+++ b/apps/web-antdv-next/src/store/auth.ts
@@ -0,0 +1,117 @@
+import type { Recordable, UserInfo } from '@vben/types';
+
+import { ref } from 'vue';
+import { useRouter } from 'vue-router';
+
+import { LOGIN_PATH } from '@vben/constants';
+import { preferences } from '@vben/preferences';
+import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
+
+import { notification } from 'antdv-next';
+import { defineStore } from 'pinia';
+
+import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api';
+import { $t } from '#/locales';
+
+export const useAuthStore = defineStore('auth', () => {
+ const accessStore = useAccessStore();
+ const userStore = useUserStore();
+ const router = useRouter();
+
+ const loginLoading = ref(false);
+
+ /**
+ * 异步处理登录操作
+ * Asynchronously handle the login process
+ * @param params 登录表单数据
+ */
+ async function authLogin(
+ params: Recordable,
+ onSuccess?: () => Promise | void,
+ ) {
+ // 异步处理用户登录操作并获取 accessToken
+ let userInfo: null | UserInfo = null;
+ try {
+ loginLoading.value = true;
+ const { accessToken } = await loginApi(params);
+
+ // 如果成功获取到 accessToken
+ if (accessToken) {
+ accessStore.setAccessToken(accessToken);
+
+ // 获取用户信息并存储到 accessStore 中
+ const [fetchUserInfoResult, accessCodes] = await Promise.all([
+ fetchUserInfo(),
+ getAccessCodesApi(),
+ ]);
+
+ userInfo = fetchUserInfoResult;
+
+ userStore.setUserInfo(userInfo);
+ accessStore.setAccessCodes(accessCodes);
+
+ if (accessStore.loginExpired) {
+ accessStore.setLoginExpired(false);
+ } else {
+ onSuccess
+ ? await onSuccess?.()
+ : await router.push(
+ userInfo.homePath || preferences.app.defaultHomePath,
+ );
+ }
+
+ if (userInfo?.realName) {
+ notification.success({
+ description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,
+ duration: 3,
+ title: $t('authentication.loginSuccess'),
+ });
+ }
+ }
+ } finally {
+ loginLoading.value = false;
+ }
+
+ return {
+ userInfo,
+ };
+ }
+
+ async function logout(redirect: boolean = true) {
+ try {
+ await logoutApi();
+ } catch {
+ // 不做任何处理
+ }
+ resetAllStores();
+ accessStore.setLoginExpired(false);
+
+ // 回登录页带上当前路由地址
+ await router.replace({
+ path: LOGIN_PATH,
+ query: redirect
+ ? {
+ redirect: encodeURIComponent(router.currentRoute.value.fullPath),
+ }
+ : {},
+ });
+ }
+
+ async function fetchUserInfo() {
+ const userInfo = await getUserInfoApi();
+ userStore.setUserInfo(userInfo);
+ return userInfo;
+ }
+
+ function $reset() {
+ loginLoading.value = false;
+ }
+
+ return {
+ $reset,
+ authLogin,
+ fetchUserInfo,
+ loginLoading,
+ logout,
+ };
+});
diff --git a/apps/web-antdv-next/src/store/index.ts b/apps/web-antdv-next/src/store/index.ts
new file mode 100644
index 00000000..269586ee
--- /dev/null
+++ b/apps/web-antdv-next/src/store/index.ts
@@ -0,0 +1 @@
+export * from './auth';
diff --git a/apps/web-antdv-next/src/views/_core/README.md b/apps/web-antdv-next/src/views/_core/README.md
new file mode 100644
index 00000000..8248afe6
--- /dev/null
+++ b/apps/web-antdv-next/src/views/_core/README.md
@@ -0,0 +1,3 @@
+# \_core
+
+此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。
diff --git a/apps/web-antdv-next/src/views/_core/about/index.vue b/apps/web-antdv-next/src/views/_core/about/index.vue
new file mode 100644
index 00000000..0ee52433
--- /dev/null
+++ b/apps/web-antdv-next/src/views/_core/about/index.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/_core/authentication/code-login.vue b/apps/web-antdv-next/src/views/_core/authentication/code-login.vue
new file mode 100644
index 00000000..02370f7d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/_core/authentication/code-login.vue
@@ -0,0 +1,68 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/_core/authentication/forget-password.vue b/apps/web-antdv-next/src/views/_core/authentication/forget-password.vue
new file mode 100644
index 00000000..10444d0a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/_core/authentication/forget-password.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/_core/authentication/login.vue b/apps/web-antdv-next/src/views/_core/authentication/login.vue
new file mode 100644
index 00000000..099e4c8c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/_core/authentication/login.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/_core/authentication/qrcode-login.vue b/apps/web-antdv-next/src/views/_core/authentication/qrcode-login.vue
new file mode 100644
index 00000000..23f5f2da
--- /dev/null
+++ b/apps/web-antdv-next/src/views/_core/authentication/qrcode-login.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/_core/authentication/register.vue b/apps/web-antdv-next/src/views/_core/authentication/register.vue
new file mode 100644
index 00000000..a1175357
--- /dev/null
+++ b/apps/web-antdv-next/src/views/_core/authentication/register.vue
@@ -0,0 +1,95 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/_core/fallback/coming-soon.vue b/apps/web-antdv-next/src/views/_core/fallback/coming-soon.vue
new file mode 100644
index 00000000..f394930f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/_core/fallback/coming-soon.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/_core/fallback/forbidden.vue b/apps/web-antdv-next/src/views/_core/fallback/forbidden.vue
new file mode 100644
index 00000000..8ea65fed
--- /dev/null
+++ b/apps/web-antdv-next/src/views/_core/fallback/forbidden.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/_core/fallback/internal-error.vue b/apps/web-antdv-next/src/views/_core/fallback/internal-error.vue
new file mode 100644
index 00000000..819a47d5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/_core/fallback/internal-error.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/_core/fallback/not-found.vue b/apps/web-antdv-next/src/views/_core/fallback/not-found.vue
new file mode 100644
index 00000000..4d178e9c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/_core/fallback/not-found.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/_core/fallback/offline.vue b/apps/web-antdv-next/src/views/_core/fallback/offline.vue
new file mode 100644
index 00000000..5de4a88d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/_core/fallback/offline.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/_core/profile/base-setting.vue b/apps/web-antdv-next/src/views/_core/profile/base-setting.vue
new file mode 100644
index 00000000..aa8a4c26
--- /dev/null
+++ b/apps/web-antdv-next/src/views/_core/profile/base-setting.vue
@@ -0,0 +1,65 @@
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/_core/profile/index.vue b/apps/web-antdv-next/src/views/_core/profile/index.vue
new file mode 100644
index 00000000..8740894e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/_core/profile/index.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/_core/profile/notification-setting.vue b/apps/web-antdv-next/src/views/_core/profile/notification-setting.vue
new file mode 100644
index 00000000..324a4b39
--- /dev/null
+++ b/apps/web-antdv-next/src/views/_core/profile/notification-setting.vue
@@ -0,0 +1,31 @@
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/_core/profile/password-setting.vue b/apps/web-antdv-next/src/views/_core/profile/password-setting.vue
new file mode 100644
index 00000000..a79d083a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/_core/profile/password-setting.vue
@@ -0,0 +1,63 @@
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/_core/profile/security-setting.vue b/apps/web-antdv-next/src/views/_core/profile/security-setting.vue
new file mode 100644
index 00000000..be30db58
--- /dev/null
+++ b/apps/web-antdv-next/src/views/_core/profile/security-setting.vue
@@ -0,0 +1,43 @@
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/dashboard/analytics/analytics-trends.vue b/apps/web-antdv-next/src/views/dashboard/analytics/analytics-trends.vue
new file mode 100644
index 00000000..f1f0b232
--- /dev/null
+++ b/apps/web-antdv-next/src/views/dashboard/analytics/analytics-trends.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/dashboard/analytics/analytics-visits-data.vue b/apps/web-antdv-next/src/views/dashboard/analytics/analytics-visits-data.vue
new file mode 100644
index 00000000..190fb41f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/dashboard/analytics/analytics-visits-data.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/dashboard/analytics/analytics-visits-sales.vue b/apps/web-antdv-next/src/views/dashboard/analytics/analytics-visits-sales.vue
new file mode 100644
index 00000000..6ff52086
--- /dev/null
+++ b/apps/web-antdv-next/src/views/dashboard/analytics/analytics-visits-sales.vue
@@ -0,0 +1,46 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/dashboard/analytics/analytics-visits-source.vue b/apps/web-antdv-next/src/views/dashboard/analytics/analytics-visits-source.vue
new file mode 100644
index 00000000..0915c7af
--- /dev/null
+++ b/apps/web-antdv-next/src/views/dashboard/analytics/analytics-visits-source.vue
@@ -0,0 +1,65 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/dashboard/analytics/analytics-visits.vue b/apps/web-antdv-next/src/views/dashboard/analytics/analytics-visits.vue
new file mode 100644
index 00000000..7e0f1013
--- /dev/null
+++ b/apps/web-antdv-next/src/views/dashboard/analytics/analytics-visits.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/dashboard/analytics/index.vue b/apps/web-antdv-next/src/views/dashboard/analytics/index.vue
new file mode 100644
index 00000000..e794c99a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/dashboard/analytics/index.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/dashboard/workspace/index.vue b/apps/web-antdv-next/src/views/dashboard/workspace/index.vue
new file mode 100644
index 00000000..b95d6138
--- /dev/null
+++ b/apps/web-antdv-next/src/views/dashboard/workspace/index.vue
@@ -0,0 +1,266 @@
+
+
+
+
+
+
+ 早安, {{ userStore.userInfo?.realName }}, 开始您一天的工作吧!
+
+ 今日晴,20℃ - 32℃!
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/demos/antd/index.vue b/apps/web-antdv-next/src/views/demos/antd/index.vue
new file mode 100644
index 00000000..6fb1998d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/demos/antd/index.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+ Default
+ Primary
+ Info
+ Error
+
+
+
+
+ 信息
+ 错误
+ 警告
+ 成功
+
+
+
+
+
+ 信息
+ 错误
+ 警告
+ 成功
+
+
+
+
diff --git a/apps/web-antdv-next/tsconfig.json b/apps/web-antdv-next/tsconfig.json
new file mode 100644
index 00000000..858a0ec0
--- /dev/null
+++ b/apps/web-antdv-next/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "@vben/tsconfig/web-app.json",
+ "compilerOptions": {
+ "paths": {
+ "#/*": ["./src/*"]
+ }
+ },
+ "references": [{ "path": "./tsconfig.node.json" }],
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
+}
diff --git a/apps/web-antdv-next/tsconfig.node.json b/apps/web-antdv-next/tsconfig.node.json
new file mode 100644
index 00000000..36e9fb5f
--- /dev/null
+++ b/apps/web-antdv-next/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "@vben/tsconfig/node.json",
+ "compilerOptions": {
+ "composite": true,
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "noEmit": false
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/apps/web-naive/vite.config.mts b/apps/web-antdv-next/vite.config.ts
similarity index 100%
rename from apps/web-naive/vite.config.mts
rename to apps/web-antdv-next/vite.config.ts
diff --git a/apps/web-ele/index.html b/apps/web-ele/index.html
index 2b59b8d7..eed06f6b 100644
--- a/apps/web-ele/index.html
+++ b/apps/web-ele/index.html
@@ -12,7 +12,7 @@
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
/>
- <%= VITE_APP_TITLE %>
+ %VITE_APP_TITLE%
diff --git a/apps/web-ele/src/layouts/basic.vue b/apps/web-ele/src/layouts/basic.vue
index 1481dc5a..e85dd1cf 100644
--- a/apps/web-ele/src/layouts/basic.vue
+++ b/apps/web-ele/src/layouts/basic.vue
@@ -2,18 +2,19 @@
import type { NotificationItem } from '@vben/layouts';
import { computed, ref, watch } from 'vue';
+import { useRouter } from 'vue-router';
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
import { useWatermark } from '@vben/hooks';
-import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
+import { BookOpenText, CircleHelp, SvgGithubIcon } from '@vben/icons';
import {
BasicLayout,
LockScreen,
Notification,
UserDropdown,
} from '@vben/layouts';
-import { preferences } from '@vben/preferences';
+import { preferences, usePreferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores';
import { openWindow } from '@vben/utils';
@@ -23,6 +24,7 @@ import LoginForm from '#/views/_core/authentication/login.vue';
const notifications = ref([
{
+ id: 1,
avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
date: '3小时前',
isRead: true,
@@ -30,6 +32,7 @@ const notifications = ref([
title: '收到了 14 份新周报',
},
{
+ id: 2,
avatar: 'https://avatar.vercel.sh/1',
date: '刚刚',
isRead: false,
@@ -37,6 +40,7 @@ const notifications = ref([
title: '朱偏右 回复了你',
},
{
+ id: 3,
avatar: 'https://avatar.vercel.sh/1',
date: '2024-01-01',
isRead: false,
@@ -44,23 +48,51 @@ const notifications = ref([
title: '曲丽丽 评论了你',
},
{
+ id: 4,
avatar: 'https://avatar.vercel.sh/satori',
date: '1天前',
isRead: false,
message: '描述信息描述信息描述信息',
title: '代办提醒',
},
+ {
+ id: 5,
+ avatar: 'https://avatar.vercel.sh/satori',
+ date: '1天前',
+ isRead: false,
+ message: '描述信息描述信息描述信息',
+ title: '跳转Workspace示例',
+ link: '/workspace',
+ },
+ {
+ id: 6,
+ avatar: 'https://avatar.vercel.sh/satori',
+ date: '1天前',
+ isRead: false,
+ message: '描述信息描述信息描述信息',
+ title: '跳转外部链接示例',
+ link: 'https://doc.vben.pro',
+ },
]);
+const router = useRouter();
const userStore = useUserStore();
const authStore = useAuthStore();
const accessStore = useAccessStore();
const { destroyWatermark, updateWatermark } = useWatermark();
+const { isDark } = usePreferences();
const showDot = computed(() =>
notifications.value.some((item) => !item.isRead),
);
const menus = computed(() => [
+ {
+ handler: () => {
+ router.push({ name: 'Profile' });
+ },
+ icon: 'lucide:user',
+ text: $t('page.auth.profile'),
+ },
{
handler: () => {
openWindow(VBEN_DOC_URL, {
@@ -76,7 +108,7 @@ const menus = computed(() => [
target: '_blank',
});
},
- icon: MdiGithub,
+ icon: SvgGithubIcon,
text: 'GitHub',
},
{
@@ -102,15 +134,77 @@ function handleNoticeClear() {
notifications.value = [];
}
+function markRead(id: number | string) {
+ const item = notifications.value.find((item) => item.id === id);
+ if (item) {
+ item.isRead = true;
+ }
+}
+
+function remove(id: number | string) {
+ notifications.value = notifications.value.filter((item) => item.id !== id);
+}
+
function handleMakeAll() {
notifications.value.forEach((item) => (item.isRead = true));
}
+
+const viewAll = () => {};
+
+const handleClick = (item: NotificationItem) => {
+ // 如果通知项有链接,点击时跳转
+ if (item.link) {
+ navigateTo(item.link, item.query, item.state);
+ }
+};
+
+function navigateTo(
+ link: string,
+ query?: Record,
+ state?: Record,
+) {
+ if (link.startsWith('http://') || link.startsWith('https://')) {
+ // 外部链接,在新标签页打开
+ window.open(link, '_blank');
+ } else {
+ // 内部路由链接,支持 query 参数和 state
+ router.push({
+ path: link,
+ query: query || {},
+ state,
+ });
+ }
+}
+
watch(
- () => preferences.app.watermark,
- async (enable) => {
+ () => ({
+ enable: preferences.app.watermark,
+ content: preferences.app.watermarkContent,
+ isDark: isDark.value,
+ }),
+ async ({ enable, content, isDark: isDarkValue }) => {
if (enable) {
+ const watermarkColor = isDarkValue
+ ? 'rgba(255, 255, 255, 0.12)'
+ : 'rgba(0, 0, 0, 0.12)';
+
await updateWatermark({
- content: `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
+ advancedStyle: {
+ colorStops: [
+ {
+ color: watermarkColor,
+ offset: 0,
+ },
+ {
+ color: watermarkColor,
+ offset: 1,
+ },
+ ],
+ type: 'linear',
+ },
+ content:
+ content ||
+ `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
});
} else {
destroyWatermark();
@@ -123,7 +217,12 @@ watch(
-
+
@@ -139,7 +239,11 @@ watch(
:dot="showDot"
:notifications="notifications"
@clear="handleNoticeClear"
+ @read="(item) => item.id && markRead(item.id)"
+ @remove="(item) => item.id && remove(item.id)"
@make-all="handleMakeAll"
+ @on-click="handleClick"
+ @view-all="viewAll"
/>
diff --git a/apps/web-ele/src/locales/langs/en-US/demos.json b/apps/web-ele/src/locales/langs/en-US/demos.json
index 6eddebb5..e379d22a 100644
--- a/apps/web-ele/src/locales/langs/en-US/demos.json
+++ b/apps/web-ele/src/locales/langs/en-US/demos.json
@@ -7,7 +7,9 @@
"about": "About",
"document": "Document",
"antdv": "Ant Design Vue Version",
+ "antdv-next": "Antdv Next Version",
"naive-ui": "Naive UI Version",
- "element-plus": "Element Plus Version"
+ "element-plus": "Element Plus Version",
+ "tdesign": "TDesign Vue Version"
}
}
diff --git a/apps/web-ele/src/locales/langs/en-US/page.json b/apps/web-ele/src/locales/langs/en-US/page.json
index 618a258c..39f1641c 100644
--- a/apps/web-ele/src/locales/langs/en-US/page.json
+++ b/apps/web-ele/src/locales/langs/en-US/page.json
@@ -4,7 +4,8 @@
"register": "Register",
"codeLogin": "Code Login",
"qrcodeLogin": "Qr Code Login",
- "forgetPassword": "Forget Password"
+ "forgetPassword": "Forget Password",
+ "profile": "Profile"
},
"dashboard": {
"title": "Dashboard",
diff --git a/apps/web-ele/src/locales/langs/zh-CN/demos.json b/apps/web-ele/src/locales/langs/zh-CN/demos.json
index ba6d6ccd..054c447c 100644
--- a/apps/web-ele/src/locales/langs/zh-CN/demos.json
+++ b/apps/web-ele/src/locales/langs/zh-CN/demos.json
@@ -7,7 +7,9 @@
"about": "关于",
"document": "文档",
"antdv": "Ant Design Vue 版本",
+ "antdv-next": "Antdv Next 版本",
"naive-ui": "Naive UI 版本",
- "element-plus": "Element Plus 版本"
+ "element-plus": "Element Plus 版本",
+ "tdesign": "TDesign Vue 版本"
}
}
diff --git a/apps/web-ele/src/locales/langs/zh-CN/page.json b/apps/web-ele/src/locales/langs/zh-CN/page.json
index 4cb67081..2192d1d5 100644
--- a/apps/web-ele/src/locales/langs/zh-CN/page.json
+++ b/apps/web-ele/src/locales/langs/zh-CN/page.json
@@ -4,7 +4,8 @@
"register": "注册",
"codeLogin": "验证码登录",
"qrcodeLogin": "二维码登录",
- "forgetPassword": "忘记密码"
+ "forgetPassword": "忘记密码",
+ "profile": "个人中心"
},
"dashboard": {
"title": "概览",
diff --git a/apps/web-ele/src/preferences.ts b/apps/web-ele/src/preferences.ts
index b2e9ace4..303c496e 100644
--- a/apps/web-ele/src/preferences.ts
+++ b/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,
});
diff --git a/apps/web-ele/src/router/routes/modules/dashboard.ts b/apps/web-ele/src/router/routes/modules/dashboard.ts
index 5254dc65..44f15ef9 100644
--- a/apps/web-ele/src/router/routes/modules/dashboard.ts
+++ b/apps/web-ele/src/router/routes/modules/dashboard.ts
@@ -14,7 +14,7 @@ const routes: RouteRecordRaw[] = [
children: [
{
name: 'Analytics',
- path: '/analytics',
+ path: 'analytics',
component: () => import('#/views/dashboard/analytics/index.vue'),
meta: {
affixTab: true,
@@ -24,7 +24,7 @@ const routes: RouteRecordRaw[] = [
},
{
name: 'Workspace',
- path: '/workspace',
+ path: 'workspace',
component: () => import('#/views/dashboard/workspace/index.vue'),
meta: {
icon: 'carbon:workspace',
diff --git a/apps/web-ele/src/router/routes/modules/demos.ts b/apps/web-ele/src/router/routes/modules/demos.ts
index 907ea3f4..b73d14bd 100644
--- a/apps/web-ele/src/router/routes/modules/demos.ts
+++ b/apps/web-ele/src/router/routes/modules/demos.ts
@@ -18,7 +18,7 @@ const routes: RouteRecordRaw[] = [
title: $t('demos.elementPlus'),
},
name: 'NaiveDemos',
- path: '/demos/element',
+ path: 'element',
component: () => import('#/views/demos/element/index.vue'),
},
{
@@ -26,7 +26,7 @@ const routes: RouteRecordRaw[] = [
title: $t('demos.form'),
},
name: 'BasicForm',
- path: '/demos/form',
+ path: 'form',
component: () => import('#/views/demos/form/basic.vue'),
},
],
diff --git a/apps/web-ele/src/router/routes/modules/vben.ts b/apps/web-ele/src/router/routes/modules/vben.ts
index 20fbc96c..da15417a 100644
--- a/apps/web-ele/src/router/routes/modules/vben.ts
+++ b/apps/web-ele/src/router/routes/modules/vben.ts
@@ -2,12 +2,18 @@ import type { RouteRecordRaw } from 'vue-router';
import {
VBEN_ANT_PREVIEW_URL,
+ VBEN_ANTDV_NEXT_PREVIEW_URL,
VBEN_DOC_URL,
VBEN_GITHUB_URL,
VBEN_LOGO_URL,
VBEN_NAIVE_PREVIEW_URL,
+ VBEN_TD_PREVIEW_URL,
} from '@vben/constants';
-import { SvgAntdvLogoIcon } from '@vben/icons';
+import {
+ SvgAntdvLogoIcon,
+ SvgAntdvNextLogoIcon,
+ SvgTDesignIcon,
+} from '@vben/icons';
import { IFrameView } from '#/layouts';
import { $t } from '#/locales';
@@ -25,7 +31,7 @@ const routes: RouteRecordRaw[] = [
children: [
{
name: 'VbenDocument',
- path: '/vben-admin/document',
+ path: 'document',
component: IFrameView,
meta: {
icon: 'lucide:book-open-text',
@@ -35,7 +41,7 @@ const routes: RouteRecordRaw[] = [
},
{
name: 'VbenGithub',
- path: '/vben-admin/github',
+ path: 'github',
component: IFrameView,
meta: {
icon: 'mdi:github',
@@ -45,7 +51,7 @@ const routes: RouteRecordRaw[] = [
},
{
name: 'VbenNaive',
- path: '/vben-admin/naive',
+ path: 'naive',
component: IFrameView,
meta: {
badgeType: 'dot',
@@ -56,7 +62,7 @@ const routes: RouteRecordRaw[] = [
},
{
name: 'VbenAntd',
- path: '/vben-admin/antd',
+ path: 'antd',
component: IFrameView,
meta: {
badgeType: 'dot',
@@ -65,6 +71,28 @@ const routes: RouteRecordRaw[] = [
title: $t('demos.vben.antdv'),
},
},
+ {
+ name: 'VbenAntdVNext',
+ path: 'antdv-next',
+ component: IFrameView,
+ meta: {
+ badgeType: 'dot',
+ icon: SvgAntdvNextLogoIcon,
+ link: VBEN_ANTDV_NEXT_PREVIEW_URL,
+ title: $t('demos.vben.antdv-next'),
+ },
+ },
+ {
+ name: 'VbenTDesign',
+ path: 'tdesign',
+ component: IFrameView,
+ meta: {
+ badgeType: 'dot',
+ icon: SvgTDesignIcon,
+ link: VBEN_TD_PREVIEW_URL,
+ title: $t('demos.vben.tdesign'),
+ },
+ },
],
},
{
@@ -77,6 +105,16 @@ const routes: RouteRecordRaw[] = [
order: 9999,
},
},
+ {
+ name: 'Profile',
+ path: '/profile',
+ component: () => import('#/views/_core/profile/index.vue'),
+ meta: {
+ icon: 'lucide:user',
+ hideInMenu: true,
+ title: $t('page.auth.profile'),
+ },
+ },
];
export default routes;
diff --git a/apps/web-ele/src/store/auth.ts b/apps/web-ele/src/store/auth.ts
index 74fadfe2..bb37051a 100644
--- a/apps/web-ele/src/store/auth.ts
+++ b/apps/web-ele/src/store/auth.ts
@@ -99,8 +99,7 @@ export const useAuthStore = defineStore('auth', () => {
}
async function fetchUserInfo() {
- let userInfo: null | UserInfo = null;
- userInfo = await getUserInfoApi();
+ const userInfo = await getUserInfoApi();
userStore.setUserInfo(userInfo);
return userInfo;
}
diff --git a/apps/web-ele/src/types/element-plus-style-css.d.ts b/apps/web-ele/src/types/element-plus-style-css.d.ts
new file mode 100644
index 00000000..ba7a7667
--- /dev/null
+++ b/apps/web-ele/src/types/element-plus-style-css.d.ts
@@ -0,0 +1,4 @@
+declare module 'element-plus/es/components/*/style/css' {
+ const sideEffect: undefined;
+ export default sideEffect;
+}
diff --git a/apps/web-ele/src/views/_core/authentication/code-login.vue b/apps/web-ele/src/views/_core/authentication/code-login.vue
index acfd1fd7..126a9506 100644
--- a/apps/web-ele/src/views/_core/authentication/code-login.vue
+++ b/apps/web-ele/src/views/_core/authentication/code-login.vue
@@ -40,6 +40,10 @@ const formSchema = computed((): VbenFormSchema[] => {
return text;
},
placeholder: $t('authentication.code'),
+ handleSendCode: async () => {
+ console.warn('发送验证码前校验等逻辑');
+ throw new Error('手机号校验失败');
+ },
},
fieldName: 'code',
label: $t('authentication.code'),
@@ -55,8 +59,7 @@ const formSchema = computed((): VbenFormSchema[] => {
* @param values 登录表单数据
*/
async function handleLogin(values: Recordable) {
- // eslint-disable-next-line no-console
- console.log(values);
+ void values;
}
diff --git a/apps/web-ele/src/views/_core/authentication/forget-password.vue b/apps/web-ele/src/views/_core/authentication/forget-password.vue
index fef0d427..10444d0a 100644
--- a/apps/web-ele/src/views/_core/authentication/forget-password.vue
+++ b/apps/web-ele/src/views/_core/authentication/forget-password.vue
@@ -29,8 +29,7 @@ const formSchema = computed((): VbenFormSchema[] => {
});
function handleSubmit(value: Recordable) {
- // eslint-disable-next-line no-console
- console.log('reset email:', value);
+ void value;
}
diff --git a/apps/web-ele/src/views/_core/authentication/register.vue b/apps/web-ele/src/views/_core/authentication/register.vue
index b1a5de72..a1175357 100644
--- a/apps/web-ele/src/views/_core/authentication/register.vue
+++ b/apps/web-ele/src/views/_core/authentication/register.vue
@@ -46,7 +46,7 @@ const formSchema = computed((): VbenFormSchema[] => {
rules(values) {
const { password } = values;
return z
- .string({ required_error: $t('authentication.passwordTip') })
+ .string({ error: $t('authentication.passwordTip') })
.min(1, { message: $t('authentication.passwordTip') })
.refine((value) => value === password, {
message: $t('authentication.confirmPasswordTip'),
@@ -82,8 +82,7 @@ const formSchema = computed((): VbenFormSchema[] => {
});
function handleSubmit(value: Recordable) {
- // eslint-disable-next-line no-console
- console.log('register submit:', value);
+ void value;
}
diff --git a/apps/web-ele/src/views/_core/profile/base-setting.vue b/apps/web-ele/src/views/_core/profile/base-setting.vue
new file mode 100644
index 00000000..aa8a4c26
--- /dev/null
+++ b/apps/web-ele/src/views/_core/profile/base-setting.vue
@@ -0,0 +1,65 @@
+
+
+
+
diff --git a/apps/web-ele/src/views/_core/profile/index.vue b/apps/web-ele/src/views/_core/profile/index.vue
new file mode 100644
index 00000000..8740894e
--- /dev/null
+++ b/apps/web-ele/src/views/_core/profile/index.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-ele/src/views/_core/profile/notification-setting.vue b/apps/web-ele/src/views/_core/profile/notification-setting.vue
new file mode 100644
index 00000000..324a4b39
--- /dev/null
+++ b/apps/web-ele/src/views/_core/profile/notification-setting.vue
@@ -0,0 +1,31 @@
+
+
+
+
diff --git a/apps/web-ele/src/views/_core/profile/password-setting.vue b/apps/web-ele/src/views/_core/profile/password-setting.vue
new file mode 100644
index 00000000..61b0ebfc
--- /dev/null
+++ b/apps/web-ele/src/views/_core/profile/password-setting.vue
@@ -0,0 +1,63 @@
+
+
+
+
diff --git a/apps/web-ele/src/views/_core/profile/security-setting.vue b/apps/web-ele/src/views/_core/profile/security-setting.vue
new file mode 100644
index 00000000..be30db58
--- /dev/null
+++ b/apps/web-ele/src/views/_core/profile/security-setting.vue
@@ -0,0 +1,43 @@
+
+
+
+
diff --git a/apps/web-ele/src/views/dashboard/analytics/analytics-visits-sales.vue b/apps/web-ele/src/views/dashboard/analytics/analytics-visits-sales.vue
index 02f50912..6ff52086 100644
--- a/apps/web-ele/src/views/dashboard/analytics/analytics-visits-sales.vue
+++ b/apps/web-ele/src/views/dashboard/analytics/analytics-visits-sales.vue
@@ -24,7 +24,7 @@ onMounted(() => {
{ name: '定制', value: 310 },
{ name: '技术支持', value: 274 },
{ name: '远程', value: 400 },
- ].sort((a, b) => {
+ ].toSorted((a, b) => {
return a.value - b.value;
}),
name: '商业占比',
diff --git a/apps/web-ele/src/views/dashboard/analytics/index.vue b/apps/web-ele/src/views/dashboard/analytics/index.vue
index 5e3d6d28..e794c99a 100644
--- a/apps/web-ele/src/views/dashboard/analytics/index.vue
+++ b/apps/web-ele/src/views/dashboard/analytics/index.vue
@@ -76,10 +76,10 @@ const chartTabs: TabOption[] = [
-
+
-
+
diff --git a/apps/web-ele/src/views/demos/element/index.vue b/apps/web-ele/src/views/demos/element/index.vue
index 0a7012d6..8d391700 100644
--- a/apps/web-ele/src/views/demos/element/index.vue
+++ b/apps/web-ele/src/views/demos/element/index.vue
@@ -102,9 +102,7 @@ const segmentedOptions = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
V-Loading
-
- 一些演示的内容
-
+ 一些演示的内容
diff --git a/apps/web-ele/src/views/demos/form/basic.vue b/apps/web-ele/src/views/demos/form/basic.vue
index 0ecab586..0d936f17 100644
--- a/apps/web-ele/src/views/demos/form/basic.vue
+++ b/apps/web-ele/src/views/demos/form/basic.vue
@@ -175,7 +175,7 @@ function setFormValues() {
description="我们重新包装了CheckboxGroup、RadioGroup、Select,可以通过options属性传入选项属性数组以自动生成选项"
title="表单演示"
>
-
+
diff --git a/apps/web-ele/tailwind.config.mjs b/apps/web-ele/tailwind.config.mjs
deleted file mode 100644
index f17f556f..00000000
--- a/apps/web-ele/tailwind.config.mjs
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from '@vben/tailwind-config';
diff --git a/apps/web-ele/tsconfig.json b/apps/web-ele/tsconfig.json
index 02c287fe..858a0ec0 100644
--- a/apps/web-ele/tsconfig.json
+++ b/apps/web-ele/tsconfig.json
@@ -2,7 +2,6 @@
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/web-app.json",
"compilerOptions": {
- "baseUrl": ".",
"paths": {
"#/*": ["./src/*"]
}
diff --git a/apps/web-ele/tsconfig.node.json b/apps/web-ele/tsconfig.node.json
index c2f0d86c..36e9fb5f 100644
--- a/apps/web-ele/tsconfig.node.json
+++ b/apps/web-ele/tsconfig.node.json
@@ -6,5 +6,5 @@
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"noEmit": false
},
- "include": ["vite.config.mts"]
+ "include": ["vite.config.ts"]
}
diff --git a/apps/web-ele/vite.config.ts b/apps/web-ele/vite.config.ts
new file mode 100644
index 00000000..c50186cc
--- /dev/null
+++ b/apps/web-ele/vite.config.ts
@@ -0,0 +1,27 @@
+import { defineConfig, viteCssLayerPlugin } from '@vben/vite-config';
+
+import ElementPlus from 'unplugin-element-plus/vite';
+
+export default defineConfig(async () => {
+ return {
+ application: {},
+ vite: {
+ plugins: [
+ // element-plus 的 css 包进 @layer el,使 Tailwind 工具类可覆盖组件样式
+ viteCssLayerPlugin({ layerName: 'el', packageName: 'element-plus' }),
+ ElementPlus({ format: 'esm' }),
+ ],
+ server: {
+ proxy: {
+ '/api': {
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api/, ''),
+ // mock代理目标地址
+ target: 'http://localhost:5320/api',
+ ws: true,
+ },
+ },
+ },
+ },
+ };
+});
diff --git a/apps/web-naive/index.html b/apps/web-naive/index.html
index 7ea63841..ec2c919e 100644
--- a/apps/web-naive/index.html
+++ b/apps/web-naive/index.html
@@ -12,7 +12,7 @@
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
/>
- <%= VITE_APP_TITLE %>
+ %VITE_APP_TITLE%
diff --git a/apps/web-naive/src/layouts/basic.vue b/apps/web-naive/src/layouts/basic.vue
index 69189384..e85dd1cf 100644
--- a/apps/web-naive/src/layouts/basic.vue
+++ b/apps/web-naive/src/layouts/basic.vue
@@ -2,18 +2,19 @@
import type { NotificationItem } from '@vben/layouts';
import { computed, ref, watch } from 'vue';
+import { useRouter } from 'vue-router';
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
import { useWatermark } from '@vben/hooks';
-import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
+import { BookOpenText, CircleHelp, SvgGithubIcon } from '@vben/icons';
import {
BasicLayout,
LockScreen,
Notification,
UserDropdown,
} from '@vben/layouts';
-import { preferences } from '@vben/preferences';
+import { preferences, usePreferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores';
import { openWindow } from '@vben/utils';
@@ -23,6 +24,7 @@ import LoginForm from '#/views/_core/authentication/login.vue';
const notifications = ref([
{
+ id: 1,
avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
date: '3小时前',
isRead: true,
@@ -30,6 +32,7 @@ const notifications = ref([
title: '收到了 14 份新周报',
},
{
+ id: 2,
avatar: 'https://avatar.vercel.sh/1',
date: '刚刚',
isRead: false,
@@ -37,6 +40,7 @@ const notifications = ref([
title: '朱偏右 回复了你',
},
{
+ id: 3,
avatar: 'https://avatar.vercel.sh/1',
date: '2024-01-01',
isRead: false,
@@ -44,23 +48,51 @@ const notifications = ref([
title: '曲丽丽 评论了你',
},
{
+ id: 4,
avatar: 'https://avatar.vercel.sh/satori',
date: '1天前',
isRead: false,
message: '描述信息描述信息描述信息',
title: '代办提醒',
},
+ {
+ id: 5,
+ avatar: 'https://avatar.vercel.sh/satori',
+ date: '1天前',
+ isRead: false,
+ message: '描述信息描述信息描述信息',
+ title: '跳转Workspace示例',
+ link: '/workspace',
+ },
+ {
+ id: 6,
+ avatar: 'https://avatar.vercel.sh/satori',
+ date: '1天前',
+ isRead: false,
+ message: '描述信息描述信息描述信息',
+ title: '跳转外部链接示例',
+ link: 'https://doc.vben.pro',
+ },
]);
+const router = useRouter();
const userStore = useUserStore();
const authStore = useAuthStore();
const accessStore = useAccessStore();
const { destroyWatermark, updateWatermark } = useWatermark();
+const { isDark } = usePreferences();
const showDot = computed(() =>
notifications.value.some((item) => !item.isRead),
);
const menus = computed(() => [
+ {
+ handler: () => {
+ router.push({ name: 'Profile' });
+ },
+ icon: 'lucide:user',
+ text: $t('page.auth.profile'),
+ },
{
handler: () => {
openWindow(VBEN_DOC_URL, {
@@ -76,7 +108,7 @@ const menus = computed(() => [
target: '_blank',
});
},
- icon: MdiGithub,
+ icon: SvgGithubIcon,
text: 'GitHub',
},
{
@@ -102,16 +134,77 @@ function handleNoticeClear() {
notifications.value = [];
}
+function markRead(id: number | string) {
+ const item = notifications.value.find((item) => item.id === id);
+ if (item) {
+ item.isRead = true;
+ }
+}
+
+function remove(id: number | string) {
+ notifications.value = notifications.value.filter((item) => item.id !== id);
+}
+
function handleMakeAll() {
notifications.value.forEach((item) => (item.isRead = true));
}
+const viewAll = () => {};
+
+const handleClick = (item: NotificationItem) => {
+ // 如果通知项有链接,点击时跳转
+ if (item.link) {
+ navigateTo(item.link, item.query, item.state);
+ }
+};
+
+function navigateTo(
+ link: string,
+ query?: Record,
+ state?: Record,
+) {
+ if (link.startsWith('http://') || link.startsWith('https://')) {
+ // 外部链接,在新标签页打开
+ window.open(link, '_blank');
+ } else {
+ // 内部路由链接,支持 query 参数和 state
+ router.push({
+ path: link,
+ query: query || {},
+ state,
+ });
+ }
+}
+
watch(
- () => preferences.app.watermark,
- async (enable) => {
+ () => ({
+ enable: preferences.app.watermark,
+ content: preferences.app.watermarkContent,
+ isDark: isDark.value,
+ }),
+ async ({ enable, content, isDark: isDarkValue }) => {
if (enable) {
+ const watermarkColor = isDarkValue
+ ? 'rgba(255, 255, 255, 0.12)'
+ : 'rgba(0, 0, 0, 0.12)';
+
await updateWatermark({
- content: `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
+ advancedStyle: {
+ colorStops: [
+ {
+ color: watermarkColor,
+ offset: 0,
+ },
+ {
+ color: watermarkColor,
+ offset: 1,
+ },
+ ],
+ type: 'linear',
+ },
+ content:
+ content ||
+ `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
});
} else {
destroyWatermark();
@@ -124,7 +217,12 @@ watch(
-
+
@@ -140,7 +239,11 @@ watch(
:dot="showDot"
:notifications="notifications"
@clear="handleNoticeClear"
+ @read="(item) => item.id && markRead(item.id)"
+ @remove="(item) => item.id && remove(item.id)"
@make-all="handleMakeAll"
+ @on-click="handleClick"
+ @view-all="viewAll"
/>
diff --git a/apps/web-naive/src/locales/langs/en-US/demos.json b/apps/web-naive/src/locales/langs/en-US/demos.json
index 839fc2e6..b02a97de 100644
--- a/apps/web-naive/src/locales/langs/en-US/demos.json
+++ b/apps/web-naive/src/locales/langs/en-US/demos.json
@@ -3,12 +3,15 @@
"naive": "Naive UI",
"table": "Table",
"form": "Form",
+ "arrayForm": "Array Editor",
"vben": {
"title": "Project",
"about": "About",
"document": "Document",
"antdv": "Ant Design Vue Version",
+ "antdv-next": "Antdv Next Version",
"naive-ui": "Naive UI Version",
- "element-plus": "Element Plus Version"
+ "element-plus": "Element Plus Version",
+ "tdesign": "TDesign Vue Version"
}
}
diff --git a/apps/web-naive/src/locales/langs/en-US/page.json b/apps/web-naive/src/locales/langs/en-US/page.json
index 618a258c..39f1641c 100644
--- a/apps/web-naive/src/locales/langs/en-US/page.json
+++ b/apps/web-naive/src/locales/langs/en-US/page.json
@@ -4,7 +4,8 @@
"register": "Register",
"codeLogin": "Code Login",
"qrcodeLogin": "Qr Code Login",
- "forgetPassword": "Forget Password"
+ "forgetPassword": "Forget Password",
+ "profile": "Profile"
},
"dashboard": {
"title": "Dashboard",
diff --git a/apps/web-naive/src/locales/langs/zh-CN/demos.json b/apps/web-naive/src/locales/langs/zh-CN/demos.json
index e0d7e616..b35cb934 100644
--- a/apps/web-naive/src/locales/langs/zh-CN/demos.json
+++ b/apps/web-naive/src/locales/langs/zh-CN/demos.json
@@ -3,12 +3,15 @@
"naive": "Naive UI",
"table": "Table",
"form": "表单",
+ "arrayForm": "数组编辑器",
"vben": {
"title": "项目",
"about": "关于",
"document": "文档",
"antdv": "Ant Design Vue 版本",
+ "antdv-next": "Antdv Next 版本",
"naive-ui": "Naive UI 版本",
- "element-plus": "Element Plus 版本"
+ "element-plus": "Element Plus 版本",
+ "tdesign": "TDesign Vue 版本"
}
}
diff --git a/apps/web-naive/src/locales/langs/zh-CN/page.json b/apps/web-naive/src/locales/langs/zh-CN/page.json
index 4cb67081..2192d1d5 100644
--- a/apps/web-naive/src/locales/langs/zh-CN/page.json
+++ b/apps/web-naive/src/locales/langs/zh-CN/page.json
@@ -4,7 +4,8 @@
"register": "注册",
"codeLogin": "验证码登录",
"qrcodeLogin": "二维码登录",
- "forgetPassword": "忘记密码"
+ "forgetPassword": "忘记密码",
+ "profile": "个人中心"
},
"dashboard": {
"title": "概览",
diff --git a/apps/web-naive/src/preferences.ts b/apps/web-naive/src/preferences.ts
index b2e9ace4..303c496e 100644
--- a/apps/web-naive/src/preferences.ts
+++ b/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,
});
diff --git a/apps/web-naive/src/router/routes/modules/dashboard.ts b/apps/web-naive/src/router/routes/modules/dashboard.ts
index 5254dc65..44f15ef9 100644
--- a/apps/web-naive/src/router/routes/modules/dashboard.ts
+++ b/apps/web-naive/src/router/routes/modules/dashboard.ts
@@ -14,7 +14,7 @@ const routes: RouteRecordRaw[] = [
children: [
{
name: 'Analytics',
- path: '/analytics',
+ path: 'analytics',
component: () => import('#/views/dashboard/analytics/index.vue'),
meta: {
affixTab: true,
@@ -24,7 +24,7 @@ const routes: RouteRecordRaw[] = [
},
{
name: 'Workspace',
- path: '/workspace',
+ path: 'workspace',
component: () => import('#/views/dashboard/workspace/index.vue'),
meta: {
icon: 'carbon:workspace',
diff --git a/apps/web-naive/src/router/routes/modules/demos.ts b/apps/web-naive/src/router/routes/modules/demos.ts
index 5e49ffa0..b6d33c40 100644
--- a/apps/web-naive/src/router/routes/modules/demos.ts
+++ b/apps/web-naive/src/router/routes/modules/demos.ts
@@ -18,7 +18,7 @@ const routes: RouteRecordRaw[] = [
title: $t('demos.naive'),
},
name: 'NaiveDemos',
- path: '/demos/naive',
+ path: 'naive',
component: () => import('#/views/demos/naive/index.vue'),
},
{
@@ -26,7 +26,7 @@ const routes: RouteRecordRaw[] = [
title: $t('demos.table'),
},
name: 'Table',
- path: '/demos/table',
+ path: 'table',
component: () => import('#/views/demos/table/index.vue'),
},
{
@@ -34,9 +34,17 @@ const routes: RouteRecordRaw[] = [
title: $t('demos.form'),
},
name: 'Form',
- path: '/demos/form',
+ path: 'form',
component: () => import('#/views/demos/form/basic.vue'),
},
+ {
+ meta: {
+ title: $t('demos.arrayForm'),
+ },
+ name: 'ArrayForm',
+ path: 'array-form',
+ component: () => import('#/views/demos/naive/array-form/index.vue'),
+ },
],
},
];
diff --git a/apps/web-naive/src/router/routes/modules/vben.ts b/apps/web-naive/src/router/routes/modules/vben.ts
index 169de855..3048c25f 100644
--- a/apps/web-naive/src/router/routes/modules/vben.ts
+++ b/apps/web-naive/src/router/routes/modules/vben.ts
@@ -2,12 +2,18 @@ import type { RouteRecordRaw } from 'vue-router';
import {
VBEN_ANT_PREVIEW_URL,
+ VBEN_ANTDV_NEXT_PREVIEW_URL,
VBEN_DOC_URL,
VBEN_ELE_PREVIEW_URL,
VBEN_GITHUB_URL,
VBEN_LOGO_URL,
+ VBEN_TD_PREVIEW_URL,
} from '@vben/constants';
-import { SvgAntdvLogoIcon } from '@vben/icons';
+import {
+ SvgAntdvLogoIcon,
+ SvgAntdvNextLogoIcon,
+ SvgTDesignIcon,
+} from '@vben/icons';
import { IFrameView } from '#/layouts';
import { $t } from '#/locales';
@@ -25,7 +31,7 @@ const routes: RouteRecordRaw[] = [
children: [
{
name: 'VbenDocument',
- path: '/vben-admin/document',
+ path: 'document',
component: IFrameView,
meta: {
icon: 'lucide:book-open-text',
@@ -35,7 +41,7 @@ const routes: RouteRecordRaw[] = [
},
{
name: 'VbenGithub',
- path: '/vben-admin/github',
+ path: 'github',
component: IFrameView,
meta: {
icon: 'mdi:github',
@@ -45,7 +51,7 @@ const routes: RouteRecordRaw[] = [
},
{
name: 'VbenAntd',
- path: '/vben-admin/antd',
+ path: 'antd',
component: IFrameView,
meta: {
badgeType: 'dot',
@@ -54,9 +60,31 @@ const routes: RouteRecordRaw[] = [
title: $t('demos.vben.antdv'),
},
},
+ {
+ name: 'VbenAntdVNext',
+ path: 'antdv-next',
+ component: IFrameView,
+ meta: {
+ badgeType: 'dot',
+ icon: SvgAntdvNextLogoIcon,
+ link: VBEN_ANTDV_NEXT_PREVIEW_URL,
+ title: $t('demos.vben.antdv-next'),
+ },
+ },
+ {
+ name: 'VbenTDesign',
+ path: 'tdesign',
+ component: IFrameView,
+ meta: {
+ badgeType: 'dot',
+ icon: SvgTDesignIcon,
+ link: VBEN_TD_PREVIEW_URL,
+ title: $t('demos.vben.tdesign'),
+ },
+ },
{
name: 'VbenElementPlus',
- path: '/vben-admin/ele',
+ path: 'ele',
component: IFrameView,
meta: {
badgeType: 'dot',
@@ -77,6 +105,16 @@ const routes: RouteRecordRaw[] = [
order: 9999,
},
},
+ {
+ name: 'Profile',
+ path: '/profile',
+ component: () => import('#/views/_core/profile/index.vue'),
+ meta: {
+ icon: 'lucide:user',
+ hideInMenu: true,
+ title: $t('page.auth.profile'),
+ },
+ },
];
export default routes;
diff --git a/apps/web-naive/src/store/auth.ts b/apps/web-naive/src/store/auth.ts
index 0ff050b3..5e5cf655 100644
--- a/apps/web-naive/src/store/auth.ts
+++ b/apps/web-naive/src/store/auth.ts
@@ -99,8 +99,7 @@ export const useAuthStore = defineStore('auth', () => {
}
async function fetchUserInfo() {
- let userInfo: null | UserInfo = null;
- userInfo = await getUserInfoApi();
+ const userInfo = await getUserInfoApi();
userStore.setUserInfo(userInfo);
return userInfo;
}
diff --git a/apps/web-naive/src/views/_core/authentication/code-login.vue b/apps/web-naive/src/views/_core/authentication/code-login.vue
index acfd1fd7..02370f7d 100644
--- a/apps/web-naive/src/views/_core/authentication/code-login.vue
+++ b/apps/web-naive/src/views/_core/authentication/code-login.vue
@@ -55,8 +55,7 @@ const formSchema = computed((): VbenFormSchema[] => {
* @param values 登录表单数据
*/
async function handleLogin(values: Recordable) {
- // eslint-disable-next-line no-console
- console.log(values);
+ void values;
}
diff --git a/apps/web-naive/src/views/_core/authentication/forget-password.vue b/apps/web-naive/src/views/_core/authentication/forget-password.vue
index fef0d427..10444d0a 100644
--- a/apps/web-naive/src/views/_core/authentication/forget-password.vue
+++ b/apps/web-naive/src/views/_core/authentication/forget-password.vue
@@ -29,8 +29,7 @@ const formSchema = computed((): VbenFormSchema[] => {
});
function handleSubmit(value: Recordable) {
- // eslint-disable-next-line no-console
- console.log('reset email:', value);
+ void value;
}
diff --git a/apps/web-naive/src/views/_core/authentication/register.vue b/apps/web-naive/src/views/_core/authentication/register.vue
index daf89c44..ed71f13b 100644
--- a/apps/web-naive/src/views/_core/authentication/register.vue
+++ b/apps/web-naive/src/views/_core/authentication/register.vue
@@ -46,7 +46,7 @@ const formSchema = computed((): VbenFormSchema[] => {
rules(values) {
const { password } = values;
return z
- .string({ required_error: $t('authentication.passwordTip') })
+ .string({ error: $t('authentication.passwordTip') })
.min(1, { message: $t('authentication.passwordTip') })
.refine((value) => value === password, {
message: $t('authentication.confirmPasswordTip'),
@@ -82,8 +82,7 @@ const formSchema = computed((): VbenFormSchema[] => {
});
function handleSubmit(value: Recordable) {
- // eslint-disable-next-line no-console
- console.log('register submit:', value);
+ void value;
}
diff --git a/apps/web-naive/src/views/_core/profile/base-setting.vue b/apps/web-naive/src/views/_core/profile/base-setting.vue
new file mode 100644
index 00000000..aa8a4c26
--- /dev/null
+++ b/apps/web-naive/src/views/_core/profile/base-setting.vue
@@ -0,0 +1,65 @@
+
+
+
+
diff --git a/apps/web-naive/src/views/_core/profile/index.vue b/apps/web-naive/src/views/_core/profile/index.vue
new file mode 100644
index 00000000..8740894e
--- /dev/null
+++ b/apps/web-naive/src/views/_core/profile/index.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-naive/src/views/_core/profile/notification-setting.vue b/apps/web-naive/src/views/_core/profile/notification-setting.vue
new file mode 100644
index 00000000..324a4b39
--- /dev/null
+++ b/apps/web-naive/src/views/_core/profile/notification-setting.vue
@@ -0,0 +1,31 @@
+
+
+
+
diff --git a/apps/web-naive/src/views/_core/profile/password-setting.vue b/apps/web-naive/src/views/_core/profile/password-setting.vue
new file mode 100644
index 00000000..bacc0c8b
--- /dev/null
+++ b/apps/web-naive/src/views/_core/profile/password-setting.vue
@@ -0,0 +1,63 @@
+
+
+
+
diff --git a/apps/web-naive/src/views/_core/profile/security-setting.vue b/apps/web-naive/src/views/_core/profile/security-setting.vue
new file mode 100644
index 00000000..be30db58
--- /dev/null
+++ b/apps/web-naive/src/views/_core/profile/security-setting.vue
@@ -0,0 +1,43 @@
+
+
+
+
diff --git a/apps/web-naive/src/views/dashboard/analytics/analytics-visits-sales.vue b/apps/web-naive/src/views/dashboard/analytics/analytics-visits-sales.vue
index 02f50912..6ff52086 100644
--- a/apps/web-naive/src/views/dashboard/analytics/analytics-visits-sales.vue
+++ b/apps/web-naive/src/views/dashboard/analytics/analytics-visits-sales.vue
@@ -24,7 +24,7 @@ onMounted(() => {
{ name: '定制', value: 310 },
{ name: '技术支持', value: 274 },
{ name: '远程', value: 400 },
- ].sort((a, b) => {
+ ].toSorted((a, b) => {
return a.value - b.value;
}),
name: '商业占比',
diff --git a/apps/web-naive/src/views/dashboard/analytics/index.vue b/apps/web-naive/src/views/dashboard/analytics/index.vue
index 5e3d6d28..e794c99a 100644
--- a/apps/web-naive/src/views/dashboard/analytics/index.vue
+++ b/apps/web-naive/src/views/dashboard/analytics/index.vue
@@ -76,10 +76,10 @@ const chartTabs: TabOption[] = [
-
+
-
+
diff --git a/apps/web-naive/src/views/demos/form/basic.vue b/apps/web-naive/src/views/demos/form/basic.vue
index fe26624c..d23a7b6e 100644
--- a/apps/web-naive/src/views/demos/form/basic.vue
+++ b/apps/web-naive/src/views/demos/form/basic.vue
@@ -1,12 +1,15 @@
-
+
设置表单值
+
+ 打开弹窗
+
+
diff --git a/apps/web-naive/src/views/demos/form/modal.vue b/apps/web-naive/src/views/demos/form/modal.vue
new file mode 100644
index 00000000..14a7021d
--- /dev/null
+++ b/apps/web-naive/src/views/demos/form/modal.vue
@@ -0,0 +1,77 @@
+
+
+
+
+
+
diff --git a/apps/web-naive/src/views/demos/naive/array-form/index.vue b/apps/web-naive/src/views/demos/naive/array-form/index.vue
new file mode 100644
index 00000000..b715f880
--- /dev/null
+++ b/apps/web-naive/src/views/demos/naive/array-form/index.vue
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+ 设置表单值
+ 获取表单值
+ 提交校验
+
+
+
+
+
diff --git a/apps/web-naive/tailwind.config.mjs b/apps/web-naive/tailwind.config.mjs
deleted file mode 100644
index f17f556f..00000000
--- a/apps/web-naive/tailwind.config.mjs
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from '@vben/tailwind-config';
diff --git a/apps/web-naive/tsconfig.json b/apps/web-naive/tsconfig.json
index 02c287fe..858a0ec0 100644
--- a/apps/web-naive/tsconfig.json
+++ b/apps/web-naive/tsconfig.json
@@ -2,7 +2,6 @@
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/web-app.json",
"compilerOptions": {
- "baseUrl": ".",
"paths": {
"#/*": ["./src/*"]
}
diff --git a/apps/web-naive/tsconfig.node.json b/apps/web-naive/tsconfig.node.json
index c2f0d86c..36e9fb5f 100644
--- a/apps/web-naive/tsconfig.node.json
+++ b/apps/web-naive/tsconfig.node.json
@@ -6,5 +6,5 @@
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"noEmit": false
},
- "include": ["vite.config.mts"]
+ "include": ["vite.config.ts"]
}
diff --git a/playground/vite.config.mts b/apps/web-naive/vite.config.ts
similarity index 100%
rename from playground/vite.config.mts
rename to apps/web-naive/vite.config.ts
diff --git a/apps/web-tdesign/.env b/apps/web-tdesign/.env
new file mode 100644
index 00000000..dae0149d
--- /dev/null
+++ b/apps/web-tdesign/.env
@@ -0,0 +1,8 @@
+# 应用标题
+VITE_APP_TITLE=Vben Admin Tdesign
+
+# 应用命名空间,用于缓存、store等功能的前缀,确保隔离
+VITE_APP_NAMESPACE=vben-web-tdesign
+
+# 对store进行加密的密钥,在将store持久化到localStorage时会使用该密钥进行加密
+VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key
diff --git a/apps/web-tdesign/.env.analyze b/apps/web-tdesign/.env.analyze
new file mode 100644
index 00000000..ffafa8dd
--- /dev/null
+++ b/apps/web-tdesign/.env.analyze
@@ -0,0 +1,7 @@
+# public path
+VITE_BASE=/
+
+# Basic interface address SPA
+VITE_GLOB_API_URL=/api
+
+VITE_VISUALIZER=true
diff --git a/apps/web-tdesign/.env.development b/apps/web-tdesign/.env.development
new file mode 100644
index 00000000..f2b44428
--- /dev/null
+++ b/apps/web-tdesign/.env.development
@@ -0,0 +1,16 @@
+# 端口号
+VITE_PORT=5999
+
+VITE_BASE=/
+
+# 接口地址
+VITE_GLOB_API_URL=/api
+
+# 是否开启 Nitro Mock服务,true 为开启,false 为关闭
+VITE_NITRO_MOCK=true
+
+# 是否打开 devtools,true 为打开,false 为关闭
+VITE_DEVTOOLS=false
+
+# 是否注入全局loading
+VITE_INJECT_APP_LOADING=true
diff --git a/apps/web-tdesign/.env.production b/apps/web-tdesign/.env.production
new file mode 100644
index 00000000..5375847a
--- /dev/null
+++ b/apps/web-tdesign/.env.production
@@ -0,0 +1,19 @@
+VITE_BASE=/
+
+# 接口地址
+VITE_GLOB_API_URL=https://mock-napi.vben.pro/api
+
+# 是否开启压缩,可以设置为 none, brotli, gzip
+VITE_COMPRESS=none
+
+# 是否开启 PWA
+VITE_PWA=false
+
+# vue-router 的模式
+VITE_ROUTER_HISTORY=hash
+
+# 是否注入全局loading
+VITE_INJECT_APP_LOADING=true
+
+# 打包后是否生成dist.zip
+VITE_ARCHIVER=true
diff --git a/apps/web-tdesign/index.html b/apps/web-tdesign/index.html
new file mode 100644
index 00000000..e3b9ae5c
--- /dev/null
+++ b/apps/web-tdesign/index.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+ %VITE_APP_TITLE%
+
+
+
+
+
+
+
+
diff --git a/apps/web-tdesign/package.json b/apps/web-tdesign/package.json
new file mode 100644
index 00000000..e2b13263
--- /dev/null
+++ b/apps/web-tdesign/package.json
@@ -0,0 +1,51 @@
+{
+ "name": "@vben/web-tdesign",
+ "version": "5.7.0",
+ "homepage": "https://vben.pro",
+ "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/vbenjs/vue-vben-admin.git",
+ "directory": "apps/web-tdesign"
+ },
+ "license": "MIT",
+ "author": {
+ "name": "vben",
+ "email": "ann.vben@gmail.com",
+ "url": "https://github.com/anncwb"
+ },
+ "type": "module",
+ "scripts": {
+ "build": "pnpm vite build --mode production",
+ "build:analyze": "pnpm vite build --mode analyze",
+ "dev": "pnpm vite --mode development",
+ "preview": "vite preview",
+ "typecheck": "vue-tsc --noEmit --skipLibCheck"
+ },
+ "imports": {
+ "#/*": "./src/*"
+ },
+ "dependencies": {
+ "@vben/access": "workspace:*",
+ "@vben/common-ui": "workspace:*",
+ "@vben/constants": "workspace:*",
+ "@vben/hooks": "workspace:*",
+ "@vben/icons": "workspace:*",
+ "@vben/layouts": "workspace:*",
+ "@vben/locales": "workspace:*",
+ "@vben/plugins": "workspace:*",
+ "@vben/preferences": "workspace:*",
+ "@vben/request": "workspace:*",
+ "@vben/stores": "workspace:*",
+ "@vben/styles": "workspace:*",
+ "@vben/types": "workspace:*",
+ "@vben/utils": "workspace:*",
+ "@vueuse/core": "catalog:",
+ "dayjs": "catalog:",
+ "es-toolkit": "catalog:",
+ "pinia": "catalog:",
+ "tdesign-vue-next": "catalog:",
+ "vue": "catalog:",
+ "vue-router": "catalog:"
+ }
+}
diff --git a/apps/web-tdesign/public/favicon.ico b/apps/web-tdesign/public/favicon.ico
new file mode 100644
index 00000000..fcf9818e
Binary files /dev/null and b/apps/web-tdesign/public/favicon.ico differ
diff --git a/apps/web-tdesign/src/adapter/component/index.ts b/apps/web-tdesign/src/adapter/component/index.ts
new file mode 100644
index 00000000..c2f9634d
--- /dev/null
+++ b/apps/web-tdesign/src/adapter/component/index.ts
@@ -0,0 +1,294 @@
+import type {
+ AutoCompleteProps,
+ ButtonProps,
+ CheckboxGroupProps,
+ CheckboxProps,
+ DatePickerProps,
+ DateRangePickerProps,
+ DividerProps,
+ InputNumberProps,
+ InputProps,
+ RadioGroupProps,
+ RadioProps,
+ SelectProps,
+ SpaceProps,
+ SwitchProps,
+ TextareaProps,
+ TimePickerProps,
+ TreeSelectProps,
+} from 'tdesign-vue-next';
+import type { TdRateProps } from 'tdesign-vue-next/es/rate/type';
+import type { UploadProps } from 'tdesign-vue-next/es/upload/types';
+
+import type { Component } from 'vue';
+
+import type {
+ ApiComponentSharedProps,
+ BaseFormComponentType,
+ IconPickerProps,
+} from '@vben/common-ui';
+import type { Recordable } from '@vben/types';
+
+import { defineAsyncComponent, defineComponent, h, ref } from 'vue';
+
+import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
+import { $t } from '@vben/locales';
+
+import { notification } from '#/adapter/tdesign';
+
+/**
+ * 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
+ * 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
+ */
+
+const AutoComplete = defineAsyncComponent(
+ () => import('tdesign-vue-next/es/auto-complete'),
+);
+const Button = defineAsyncComponent(() => import('tdesign-vue-next/es/button'));
+const Checkbox = defineAsyncComponent(
+ () => import('tdesign-vue-next/es/checkbox'),
+);
+const CheckboxGroup = defineAsyncComponent(() =>
+ import('tdesign-vue-next/es/checkbox').then((res) => res.CheckboxGroup),
+);
+const DatePicker = defineAsyncComponent(
+ () => import('tdesign-vue-next/es/date-picker'),
+);
+const Divider = defineAsyncComponent(
+ () => import('tdesign-vue-next/es/divider'),
+);
+const Input = defineAsyncComponent(() => import('tdesign-vue-next/es/input'));
+const InputNumber = defineAsyncComponent(
+ () => import('tdesign-vue-next/es/input-number'),
+);
+// const InputPassword = defineAsyncComponent(() =>
+// import('tdesign-vue-next/es/input').then((res) => res.InputPassword),
+// );
+// const Mentions = defineAsyncComponent(
+// () => import('tdesign-vue-next/es/mentions'),
+// );
+const Radio = defineAsyncComponent(() => import('tdesign-vue-next/es/radio'));
+const RadioGroup = defineAsyncComponent(() =>
+ import('tdesign-vue-next/es/radio').then((res) => res.RadioGroup),
+);
+const RangePicker = defineAsyncComponent(() =>
+ import('tdesign-vue-next/es/date-picker').then((res) => res.DateRangePicker),
+);
+const Rate = defineAsyncComponent(() => import('tdesign-vue-next/es/rate'));
+const Select = defineAsyncComponent(() => import('tdesign-vue-next/es/select'));
+const Space = defineAsyncComponent(() => import('tdesign-vue-next/es/space'));
+const Switch = defineAsyncComponent(() => import('tdesign-vue-next/es/switch'));
+const Textarea = defineAsyncComponent(
+ () => import('tdesign-vue-next/es/textarea'),
+);
+const TimePicker = defineAsyncComponent(
+ () => import('tdesign-vue-next/es/time-picker'),
+);
+const TreeSelect = defineAsyncComponent(
+ () => import('tdesign-vue-next/es/tree-select'),
+);
+const Upload = defineAsyncComponent(() => import('tdesign-vue-next/es/upload'));
+
+const withDefaultPlaceholder = (
+ component: Component,
+ type: 'input' | 'select',
+ componentProps: Recordable = {},
+) => {
+ return defineComponent({
+ name: component.name,
+ inheritAttrs: false,
+ setup: (props: any, { attrs, expose, slots }) => {
+ const placeholder =
+ props?.placeholder ||
+ attrs?.placeholder ||
+ $t(`ui.placeholder.${type}`);
+ // 透传组件暴露的方法
+ const innerRef = ref();
+ expose(
+ new Proxy(
+ {},
+ {
+ get: (_target, key) => innerRef.value?.[key],
+ has: (_target, key) => key in (innerRef.value || {}),
+ },
+ ),
+ );
+ return () =>
+ h(
+ component,
+ { ...componentProps, placeholder, ...props, ...attrs, ref: innerRef },
+ slots,
+ );
+ },
+ });
+};
+
+// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
+export type ComponentType =
+ | 'ApiSelect'
+ | 'ApiTreeSelect'
+ | 'AutoComplete'
+ | 'Checkbox'
+ | 'CheckboxGroup'
+ | 'DatePicker'
+ | 'DefaultButton'
+ | 'Divider'
+ | 'IconPicker'
+ | 'Input'
+ | 'InputNumber'
+ // | 'InputPassword'
+ // | 'Mentions'
+ | 'PrimaryButton'
+ | 'Radio'
+ | 'RadioGroup'
+ | 'RangePicker'
+ | 'Rate'
+ | 'Select'
+ | 'Space'
+ | 'Switch'
+ | 'Textarea'
+ | 'TimePicker'
+ | 'TreeSelect'
+ | 'Upload'
+ | BaseFormComponentType;
+
+/**
+ * 与 {@link ComponentType} 中注册的组件名一一对应,便于 Schema 上 `component` + `componentProps` 联动提示
+ */
+export interface ComponentPropsMap {
+ ApiSelect: ApiComponentSharedProps & SelectProps;
+ ApiTreeSelect: ApiComponentSharedProps & TreeSelectProps;
+ AutoComplete: AutoCompleteProps;
+ Checkbox: CheckboxProps;
+ CheckboxGroup: CheckboxGroupProps;
+ DatePicker: DatePickerProps;
+ DefaultButton: ButtonProps;
+ Divider: DividerProps;
+ IconPicker: IconPickerProps;
+ Input: InputProps;
+ InputNumber: InputNumberProps;
+ PrimaryButton: ButtonProps;
+ Radio: RadioProps;
+ RadioGroup: RadioGroupProps;
+ RangePicker: DateRangePickerProps;
+ Rate: TdRateProps;
+ Select: SelectProps;
+ Space: SpaceProps;
+ Switch: SwitchProps;
+ Textarea: TextareaProps;
+ TimePicker: TimePickerProps;
+ TreeSelect: TreeSelectProps;
+ Upload: UploadProps;
+}
+
+async function initComponentAdapter() {
+ const components: Partial> = {
+ // 如果你的组件体积比较大,可以使用异步加载
+ // Button: () =>
+ // import('xxx').then((res) => res.Button),
+ ApiSelect: withDefaultPlaceholder(
+ {
+ ...ApiComponent,
+ name: 'ApiSelect',
+ },
+ 'select',
+ {
+ component: Select,
+ loadingSlot: 'suffixIcon',
+ visibleEvent: 'onDropdownVisibleChange',
+ modelPropName: 'value',
+ },
+ ),
+ ApiTreeSelect: withDefaultPlaceholder(
+ {
+ ...ApiComponent,
+ name: 'ApiTreeSelect',
+ },
+ 'select',
+ {
+ component: TreeSelect,
+ fieldNames: { label: 'label', value: 'value', children: 'children' },
+ loadingSlot: 'suffixIcon',
+ modelPropName: 'value',
+ optionsPropName: 'treeData',
+ visibleEvent: 'onVisibleChange',
+ },
+ ),
+ AutoComplete,
+ Checkbox,
+ CheckboxGroup,
+ DatePicker,
+ // 自定义默认按钮
+ DefaultButton: (props, { attrs, slots }) => {
+ let ghost = false;
+ let variant = props.variant;
+ if (props.variant === 'ghost') {
+ ghost = true;
+ variant = 'base';
+ }
+ return h(
+ Button,
+ { ...props, ghost, variant, attrs, theme: 'default' },
+ slots,
+ );
+ },
+ Divider,
+ IconPicker: withDefaultPlaceholder(IconPicker, 'select', {
+ iconSlot: 'addonAfter',
+ inputComponent: Input,
+ modelValueProp: 'value',
+ }),
+ Input: withDefaultPlaceholder(Input, 'input'),
+ InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
+ // InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
+ // Mentions: withDefaultPlaceholder(Mentions, 'input'),
+ // 自定义主要按钮
+ PrimaryButton: (props, { attrs, slots }) => {
+ let ghost = false;
+ let variant = props.variant;
+ if (props.variant === 'ghost') {
+ ghost = true;
+ variant = 'base';
+ }
+ return h(
+ Button,
+ { ...props, ghost, variant, attrs, theme: 'primary' },
+ slots,
+ );
+ },
+ Radio,
+ RadioGroup,
+ RangePicker: (props, { attrs, slots }) => {
+ return h(
+ RangePicker,
+ { ...props, modelValue: props.modelValue ?? [], attrs },
+ slots,
+ );
+ },
+ Rate,
+ Select: withDefaultPlaceholder(Select, 'select'),
+ Space,
+ Switch,
+ Textarea: withDefaultPlaceholder(Textarea, 'input'),
+ TimePicker,
+ TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
+ Upload,
+ };
+
+ // 将组件注册到全局共享状态中
+ globalShareState.setComponents(components);
+
+ // 定义全局共享状态中的消息提示
+ globalShareState.defineMessage({
+ // 复制成功消息提示
+ copyPreferencesSuccess: (title, content) => {
+ notification.success({
+ title,
+ content,
+ placement: 'bottom-right',
+ });
+ },
+ });
+}
+
+export { initComponentAdapter };
diff --git a/apps/web-tdesign/src/adapter/form.ts b/apps/web-tdesign/src/adapter/form.ts
new file mode 100644
index 00000000..34f12166
--- /dev/null
+++ b/apps/web-tdesign/src/adapter/form.ts
@@ -0,0 +1,68 @@
+import type {
+ VbenFormProps as FormProps,
+ VbenFormSchema as FormSchema,
+ FormValues,
+} from '@vben/common-ui';
+
+import type { ComponentPropsMap, ComponentType } from './component';
+
+import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
+import { $t } from '@vben/locales';
+
+async function initSetupVbenForm() {
+ setupVbenForm({
+ config: {
+ // tdesign组件库默认都是 v-model:value
+ baseModelPropName: 'value',
+
+ // 一些组件是 v-model:checked 或者 v-model:fileList
+ modelPropNameMap: {
+ Checkbox: 'checked',
+ Radio: 'checked',
+ Switch: 'checked',
+ Upload: 'fileList',
+ },
+ },
+ rules: {
+ // 输入项目必填国际化适配
+ required: (value, _params, ctx) => {
+ if (value === undefined || value === null || value.length === 0) {
+ return $t('ui.formRules.required', [ctx.label]);
+ }
+ return true;
+ },
+ // 选择项目必填国际化适配
+ selectRequired: (value, _params, ctx) => {
+ if (value === undefined || value === null) {
+ return $t('ui.formRules.selectRequired', [ctx.label]);
+ }
+ return true;
+ },
+ },
+ });
+}
+
+function useVbenForm<
+ TFormValues extends FormValues = FormValues,
+ TSubmitValues extends FormValues = TFormValues,
+>(
+ options: FormProps<
+ ComponentType,
+ ComponentPropsMap,
+ TFormValues,
+ TSubmitValues
+ >,
+) {
+ return useForm(
+ options,
+ );
+}
+
+export { initSetupVbenForm, useVbenForm, z };
+
+export type VbenFormSchema =
+ FormSchema;
+export type VbenFormProps<
+ TFormValues extends FormValues = FormValues,
+ TSubmitValues extends FormValues = TFormValues,
+> = FormProps;
diff --git a/apps/web-tdesign/src/adapter/tdesign.ts b/apps/web-tdesign/src/adapter/tdesign.ts
new file mode 100644
index 00000000..70c67601
--- /dev/null
+++ b/apps/web-tdesign/src/adapter/tdesign.ts
@@ -0,0 +1,5 @@
+export {
+ DialogPlugin as dialog,
+ MessagePlugin as message,
+ NotifyPlugin as notification,
+} from 'tdesign-vue-next';
diff --git a/apps/web-tdesign/src/adapter/vxe-table.ts b/apps/web-tdesign/src/adapter/vxe-table.ts
new file mode 100644
index 00000000..c621320b
--- /dev/null
+++ b/apps/web-tdesign/src/adapter/vxe-table.ts
@@ -0,0 +1,93 @@
+import type { FormValues } from '@vben/common-ui';
+import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
+
+import type { ComponentPropsMap, ComponentType } from './component';
+
+import { h } from 'vue';
+
+import {
+ setupVbenVxeTable,
+ useVbenVxeGrid as useGrid,
+} from '@vben/plugins/vxe-table';
+
+import { Button, Image } from 'tdesign-vue-next';
+
+import { useVbenForm } from './form';
+
+setupVbenVxeTable({
+ configVxeTable: (vxeUI) => {
+ vxeUI.setConfig({
+ grid: {
+ align: 'center',
+ border: false,
+ columnConfig: {
+ resizable: true,
+ },
+ minHeight: 180,
+ formConfig: {
+ // 全局禁用vxe-table的表单配置,使用formOptions
+ enabled: false,
+ },
+ proxyConfig: {
+ autoLoad: true,
+ response: {
+ result: 'items',
+ total: 'total',
+ list: 'items',
+ },
+ showActiveMsg: true,
+ showResponseMsg: false,
+ },
+ round: true,
+ showOverflow: true,
+ size: 'small',
+ } as VxeTableGridOptions,
+ });
+
+ // 表格配置项可以用 cellRender: { name: 'CellImage' },
+ vxeUI.renderer.add('CellImage', {
+ renderTableDefault(renderOpts, params) {
+ const { props } = renderOpts;
+ const { column, row } = params;
+ return h(Image, { src: row[column.field], ...props });
+ },
+ });
+
+ // 表格配置项可以用 cellRender: { name: 'CellLink' },
+ vxeUI.renderer.add('CellLink', {
+ renderTableDefault(renderOpts) {
+ const { props } = renderOpts;
+ return h(
+ Button,
+ { size: 'small', theme: 'primary', variant: 'text' },
+ { default: () => props?.text },
+ );
+ },
+ });
+
+ // 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
+ // vxeUI.formats.add
+ },
+ useVbenForm,
+});
+
+export const useVbenVxeGrid = <
+ T extends Record,
+ TFormValues extends FormValues = FormValues,
+ TSubmitValues extends FormValues = TFormValues,
+>(
+ ...rest: Parameters<
+ typeof useGrid<
+ T,
+ ComponentType,
+ ComponentPropsMap,
+ TFormValues,
+ TSubmitValues
+ >
+ >
+) =>
+ useGrid(
+ ...rest,
+ );
+
+export type * from '@vben/plugins/vxe-table';
diff --git a/apps/web-tdesign/src/api/core/auth.ts b/apps/web-tdesign/src/api/core/auth.ts
new file mode 100644
index 00000000..71d9f994
--- /dev/null
+++ b/apps/web-tdesign/src/api/core/auth.ts
@@ -0,0 +1,51 @@
+import { baseRequestClient, requestClient } from '#/api/request';
+
+export namespace AuthApi {
+ /** 登录接口参数 */
+ export interface LoginParams {
+ password?: string;
+ username?: string;
+ }
+
+ /** 登录接口返回值 */
+ export interface LoginResult {
+ accessToken: string;
+ }
+
+ export interface RefreshTokenResult {
+ data: string;
+ status: number;
+ }
+}
+
+/**
+ * 登录
+ */
+export async function loginApi(data: AuthApi.LoginParams) {
+ return requestClient.post('/auth/login', data);
+}
+
+/**
+ * 刷新accessToken
+ */
+export async function refreshTokenApi() {
+ return baseRequestClient.post('/auth/refresh', {
+ withCredentials: true,
+ });
+}
+
+/**
+ * 退出登录
+ */
+export async function logoutApi() {
+ return baseRequestClient.post('/auth/logout', {
+ withCredentials: true,
+ });
+}
+
+/**
+ * 获取用户权限码
+ */
+export async function getAccessCodesApi() {
+ return requestClient.get('/auth/codes');
+}
diff --git a/apps/web-tdesign/src/api/core/index.ts b/apps/web-tdesign/src/api/core/index.ts
new file mode 100644
index 00000000..28a5aef4
--- /dev/null
+++ b/apps/web-tdesign/src/api/core/index.ts
@@ -0,0 +1,3 @@
+export * from './auth';
+export * from './menu';
+export * from './user';
diff --git a/apps/web-tdesign/src/api/core/menu.ts b/apps/web-tdesign/src/api/core/menu.ts
new file mode 100644
index 00000000..9ef60b11
--- /dev/null
+++ b/apps/web-tdesign/src/api/core/menu.ts
@@ -0,0 +1,10 @@
+import type { RouteRecordStringComponent } from '@vben/types';
+
+import { requestClient } from '#/api/request';
+
+/**
+ * 获取用户所有菜单
+ */
+export async function getAllMenusApi() {
+ return requestClient.get('/menu/all');
+}
diff --git a/apps/web-tdesign/src/api/core/user.ts b/apps/web-tdesign/src/api/core/user.ts
new file mode 100644
index 00000000..7e28ea84
--- /dev/null
+++ b/apps/web-tdesign/src/api/core/user.ts
@@ -0,0 +1,10 @@
+import type { UserInfo } from '@vben/types';
+
+import { requestClient } from '#/api/request';
+
+/**
+ * 获取用户信息
+ */
+export async function getUserInfoApi() {
+ return requestClient.get('/user/info');
+}
diff --git a/apps/web-tdesign/src/api/index.ts b/apps/web-tdesign/src/api/index.ts
new file mode 100644
index 00000000..4b0e0413
--- /dev/null
+++ b/apps/web-tdesign/src/api/index.ts
@@ -0,0 +1 @@
+export * from './core';
diff --git a/apps/web-tdesign/src/api/request.ts b/apps/web-tdesign/src/api/request.ts
new file mode 100644
index 00000000..86b181d3
--- /dev/null
+++ b/apps/web-tdesign/src/api/request.ts
@@ -0,0 +1,112 @@
+/**
+ * 该文件可自行根据业务逻辑进行调整
+ */
+import type { RequestClientOptions } from '@vben/request';
+
+import { useAppConfig } from '@vben/hooks';
+import { preferences } from '@vben/preferences';
+import {
+ authenticateResponseInterceptor,
+ defaultResponseInterceptor,
+ errorMessageResponseInterceptor,
+ RequestClient,
+} from '@vben/request';
+import { useAccessStore } from '@vben/stores';
+
+import { message } from '#/adapter/tdesign';
+import { useAuthStore } from '#/store';
+
+import { refreshTokenApi } from './core';
+
+const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
+
+function createRequestClient(baseURL: string, options?: RequestClientOptions) {
+ const client = new RequestClient({
+ ...options,
+ baseURL,
+ });
+
+ /**
+ * 重新认证逻辑
+ */
+ async function doReAuthenticate() {
+ console.warn('Access token or refresh token is invalid or expired. ');
+ const accessStore = useAccessStore();
+ const authStore = useAuthStore();
+ accessStore.setAccessToken(null);
+ if (
+ preferences.app.loginExpiredMode === 'modal' &&
+ accessStore.isAccessChecked
+ ) {
+ accessStore.setLoginExpired(true);
+ } else {
+ await authStore.logout();
+ }
+ }
+
+ /**
+ * 刷新token逻辑
+ */
+ async function doRefreshToken() {
+ const accessStore = useAccessStore();
+ const resp = await refreshTokenApi();
+ const newToken = resp.data;
+ accessStore.setAccessToken(newToken);
+ return newToken;
+ }
+
+ function formatToken(token: null | string) {
+ return token ? `Bearer ${token}` : null;
+ }
+
+ // 请求头处理
+ client.addRequestInterceptor({
+ fulfilled: async (config) => {
+ const accessStore = useAccessStore();
+
+ config.headers.Authorization = formatToken(accessStore.accessToken);
+ config.headers['Accept-Language'] = preferences.app.locale;
+ return config;
+ },
+ });
+
+ // 处理返回的响应数据格式
+ client.addResponseInterceptor(
+ defaultResponseInterceptor({
+ codeField: 'code',
+ dataField: 'data',
+ successCode: 0,
+ }),
+ );
+
+ // token过期的处理
+ client.addResponseInterceptor(
+ authenticateResponseInterceptor({
+ client,
+ doReAuthenticate,
+ doRefreshToken,
+ enableRefreshToken: preferences.app.enableRefreshToken,
+ formatToken,
+ }),
+ );
+
+ // 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
+ client.addResponseInterceptor(
+ errorMessageResponseInterceptor((msg: string, error) => {
+ // 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
+ // 当前mock接口返回的错误字段是 error 或者 message
+ const responseData = error?.response?.data ?? {};
+ const errorMessage = responseData?.error ?? responseData?.message ?? '';
+ // 如果没有错误信息,则会根据状态码进行提示
+ message.error(errorMessage || msg);
+ }),
+ );
+
+ return client;
+}
+
+export const requestClient = createRequestClient(apiURL, {
+ responseReturn: 'data',
+});
+
+export const baseRequestClient = new RequestClient({ baseURL: apiURL });
diff --git a/apps/web-tdesign/src/app.vue b/apps/web-tdesign/src/app.vue
new file mode 100644
index 00000000..01ce79db
--- /dev/null
+++ b/apps/web-tdesign/src/app.vue
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
diff --git a/apps/web-tdesign/src/bootstrap.ts b/apps/web-tdesign/src/bootstrap.ts
new file mode 100644
index 00000000..ad7fef6a
--- /dev/null
+++ b/apps/web-tdesign/src/bootstrap.ts
@@ -0,0 +1,78 @@
+import { createApp, watchEffect } from 'vue';
+
+import { registerAccessDirective } from '@vben/access';
+import { registerLoadingDirective } from '@vben/common-ui/es/loading';
+import { preferences } from '@vben/preferences';
+import { initStores } from '@vben/stores';
+import '@vben/styles';
+
+import { useTitle } from '@vueuse/core';
+
+import { $t, setupI18n } from '#/locales';
+
+import { initComponentAdapter } from './adapter/component';
+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) {
+ // 初始化组件适配器
+ await initComponentAdapter();
+
+ // 初始化表单组件
+ await initSetupVbenForm();
+
+ // // 设置弹窗的默认配置
+ // setDefaultModalProps({
+ // fullscreenButton: false,
+ // });
+ // // 设置抽屉的默认配置
+ // setDefaultDrawerProps({
+ // zIndex: 1020,
+ // });
+
+ const app = createApp(App);
+
+ // 注册v-loading指令
+ registerLoadingDirective(app, {
+ loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令
+ spinning: 'spinning',
+ });
+
+ // 国际化 i18n 配置
+ await setupI18n(app);
+
+ // 配置 pinia-tore
+ await initStores(app, { namespace });
+
+ // 安装权限指令
+ registerAccessDirective(app);
+
+ // 初始化 tippy
+ const { initTippy } = await import('@vben/common-ui/es/tippy');
+ initTippy(app);
+
+ // 配置路由及路由守卫
+ app.use(router);
+
+ // 配置Motion插件
+ const { MotionPlugin } = await import('@vben/plugins/motion');
+ app.use(MotionPlugin);
+
+ // 动态更新标题
+ watchEffect(() => {
+ if (preferences.app.dynamicTitle) {
+ const routeTitle = router.currentRoute.value.meta?.title;
+ const pageTitle =
+ (routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name;
+ useTitle(pageTitle);
+ }
+ });
+
+ app.mount('#app');
+}
+
+export { bootstrap };
diff --git a/apps/web-tdesign/src/layouts/auth.vue b/apps/web-tdesign/src/layouts/auth.vue
new file mode 100644
index 00000000..18d415bc
--- /dev/null
+++ b/apps/web-tdesign/src/layouts/auth.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
diff --git a/apps/web-tdesign/src/layouts/basic.vue b/apps/web-tdesign/src/layouts/basic.vue
new file mode 100644
index 00000000..e85dd1cf
--- /dev/null
+++ b/apps/web-tdesign/src/layouts/basic.vue
@@ -0,0 +1,261 @@
+
+
+
+
+
+
+
+
+ item.id && markRead(item.id)"
+ @remove="(item) => item.id && remove(item.id)"
+ @make-all="handleMakeAll"
+ @on-click="handleClick"
+ @view-all="viewAll"
+ />
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-tdesign/src/layouts/index.ts b/apps/web-tdesign/src/layouts/index.ts
new file mode 100644
index 00000000..a4320780
--- /dev/null
+++ b/apps/web-tdesign/src/layouts/index.ts
@@ -0,0 +1,6 @@
+const BasicLayout = () => import('./basic.vue');
+const AuthPageLayout = () => import('./auth.vue');
+
+const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);
+
+export { AuthPageLayout, BasicLayout, IFrameView };
diff --git a/apps/web-tdesign/src/locales/README.md b/apps/web-tdesign/src/locales/README.md
new file mode 100644
index 00000000..7b451032
--- /dev/null
+++ b/apps/web-tdesign/src/locales/README.md
@@ -0,0 +1,3 @@
+# locale
+
+每个app使用的国际化可能不同,这里用于扩展国际化的功能,例如扩展 dayjs、antd组件库的多语言切换,以及app本身的国际化文件。
diff --git a/apps/web-tdesign/src/locales/index.ts b/apps/web-tdesign/src/locales/index.ts
new file mode 100644
index 00000000..6d98579a
--- /dev/null
+++ b/apps/web-tdesign/src/locales/index.ts
@@ -0,0 +1,77 @@
+import type { App } from 'vue';
+
+import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
+
+import {
+ $t,
+ setupI18n as coreSetup,
+ loadLocalesMapFromDir,
+} from '@vben/locales';
+import { preferences } from '@vben/preferences';
+
+import dayjs from 'dayjs';
+
+const modules = import.meta.glob('./langs/**/*.json');
+
+const localesMap = loadLocalesMapFromDir(
+ /\.\/langs\/([^/]+)\/(.*)\.json$/,
+ modules,
+);
+/**
+ * 加载应用特有的语言包
+ * 这里也可以改造为从服务端获取翻译数据
+ * @param lang
+ */
+async function loadMessages(lang: SupportedLanguagesType) {
+ const [appLocaleMessages] = await Promise.all([
+ localesMap[lang]?.(),
+ loadThirdPartyMessage(lang),
+ ]);
+ return appLocaleMessages?.default;
+}
+
+/**
+ * 加载第三方组件库的语言包
+ * @param lang
+ */
+async function loadThirdPartyMessage(lang: SupportedLanguagesType) {
+ await loadDayjsLocale(lang);
+}
+
+/**
+ * 加载dayjs的语言包
+ * @param lang
+ */
+async function loadDayjsLocale(lang: SupportedLanguagesType) {
+ let locale;
+ switch (lang) {
+ case 'en-US': {
+ locale = await import('dayjs/locale/en');
+ break;
+ }
+ case 'zh-CN': {
+ locale = await import('dayjs/locale/zh-cn');
+ break;
+ }
+ // 默认使用英语
+ default: {
+ locale = await import('dayjs/locale/en');
+ }
+ }
+ if (locale) {
+ dayjs.locale(locale);
+ } else {
+ console.error(`Failed to load dayjs locale for ${lang}`);
+ }
+}
+
+async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
+ await coreSetup(app, {
+ defaultLocale: preferences.app.locale,
+ loadMessages,
+ missingWarn: !import.meta.env.PROD,
+ ...options,
+ });
+}
+
+export { $t, setupI18n };
diff --git a/apps/web-tdesign/src/locales/langs/en-US/demos.json b/apps/web-tdesign/src/locales/langs/en-US/demos.json
new file mode 100644
index 00000000..0cb50b7f
--- /dev/null
+++ b/apps/web-tdesign/src/locales/langs/en-US/demos.json
@@ -0,0 +1,13 @@
+{
+ "title": "Demos",
+ "tdesign": "TDesign Vue",
+ "vben": {
+ "title": "Project",
+ "about": "About",
+ "document": "Document",
+ "antdv": "Ant Design Vue Version",
+ "antdv-next": "Antdv Next Version",
+ "naive-ui": "Naive UI Version",
+ "element-plus": "Element Plus Version"
+ }
+}
diff --git a/apps/web-tdesign/src/locales/langs/en-US/page.json b/apps/web-tdesign/src/locales/langs/en-US/page.json
new file mode 100644
index 00000000..39f1641c
--- /dev/null
+++ b/apps/web-tdesign/src/locales/langs/en-US/page.json
@@ -0,0 +1,15 @@
+{
+ "auth": {
+ "login": "Login",
+ "register": "Register",
+ "codeLogin": "Code Login",
+ "qrcodeLogin": "Qr Code Login",
+ "forgetPassword": "Forget Password",
+ "profile": "Profile"
+ },
+ "dashboard": {
+ "title": "Dashboard",
+ "analytics": "Analytics",
+ "workspace": "Workspace"
+ }
+}
diff --git a/apps/web-tdesign/src/locales/langs/zh-CN/demos.json b/apps/web-tdesign/src/locales/langs/zh-CN/demos.json
new file mode 100644
index 00000000..25e2a0f9
--- /dev/null
+++ b/apps/web-tdesign/src/locales/langs/zh-CN/demos.json
@@ -0,0 +1,13 @@
+{
+ "title": "演示",
+ "tdesign": "TDesign Vue",
+ "vben": {
+ "title": "项目",
+ "about": "关于",
+ "document": "文档",
+ "antdv": "Ant Design Vue 版本",
+ "antdv-next": "Antdv Next 版本",
+ "naive-ui": "Naive UI 版本",
+ "element-plus": "Element Plus 版本"
+ }
+}
diff --git a/apps/web-tdesign/src/locales/langs/zh-CN/page.json b/apps/web-tdesign/src/locales/langs/zh-CN/page.json
new file mode 100644
index 00000000..2192d1d5
--- /dev/null
+++ b/apps/web-tdesign/src/locales/langs/zh-CN/page.json
@@ -0,0 +1,15 @@
+{
+ "auth": {
+ "login": "登录",
+ "register": "注册",
+ "codeLogin": "验证码登录",
+ "qrcodeLogin": "二维码登录",
+ "forgetPassword": "忘记密码",
+ "profile": "个人中心"
+ },
+ "dashboard": {
+ "title": "概览",
+ "analytics": "分析页",
+ "workspace": "工作台"
+ }
+}
diff --git a/apps/web-tdesign/src/main.ts b/apps/web-tdesign/src/main.ts
new file mode 100644
index 00000000..5d728a02
--- /dev/null
+++ b/apps/web-tdesign/src/main.ts
@@ -0,0 +1,31 @@
+import { initPreferences } from '@vben/preferences';
+import { unmountGlobalLoading } from '@vben/utils';
+
+import { overridesPreferences } from './preferences';
+
+/**
+ * 应用初始化完成之后再进行页面加载渲染
+ */
+async function initApplication() {
+ // name用于指定项目唯一标识
+ // 用于区分不同项目的偏好设置以及存储数据的key前缀以及其他一些需要隔离的数据
+ const env = import.meta.env.PROD ? 'prod' : 'dev';
+ const appVersion = import.meta.env.VITE_APP_VERSION;
+ const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${appVersion}-${env}`;
+
+ // app偏好设置初始化
+ await initPreferences({
+ namespace,
+ overrides: overridesPreferences,
+ });
+
+ // 启动应用并挂载
+ // vue应用主要逻辑及视图
+ const { bootstrap } = await import('./bootstrap');
+ await bootstrap(namespace);
+
+ // 移除并销毁loading
+ unmountGlobalLoading();
+}
+
+initApplication();
diff --git a/apps/web-tdesign/src/preferences.ts b/apps/web-tdesign/src/preferences.ts
new file mode 100644
index 00000000..303c496e
--- /dev/null
+++ b/apps/web-tdesign/src/preferences.ts
@@ -0,0 +1,17 @@
+import {
+ appCopyrightPreferences,
+ defineOverridesPreferences,
+} from '@vben/preferences';
+
+/**
+ * @description 项目配置文件
+ * 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
+ * !!! 更改配置后请清空缓存,否则可能不生效
+ */
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ app: {
+ name: import.meta.env.VITE_APP_TITLE,
+ },
+ copyright: appCopyrightPreferences,
+});
diff --git a/apps/web-tdesign/src/router/access.ts b/apps/web-tdesign/src/router/access.ts
new file mode 100644
index 00000000..08cb4b0b
--- /dev/null
+++ b/apps/web-tdesign/src/router/access.ts
@@ -0,0 +1,41 @@
+import type {
+ ComponentRecordType,
+ GenerateMenuAndRoutesOptions,
+} from '@vben/types';
+
+import { generateAccessible } from '@vben/access';
+import { preferences } from '@vben/preferences';
+
+import { message } from '#/adapter/tdesign';
+import { getAllMenusApi } from '#/api';
+import { BasicLayout, IFrameView } from '#/layouts';
+import { $t } from '#/locales';
+
+const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
+
+async function generateAccess(options: GenerateMenuAndRoutesOptions) {
+ const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
+
+ const layoutMap: ComponentRecordType = {
+ BasicLayout,
+ IFrameView,
+ };
+
+ return await generateAccessible(preferences.app.accessMode, {
+ ...options,
+ fetchMenuListAsync: async () => {
+ message.loading({
+ content: `${$t('common.loadingMenu')}...`,
+ duration: 1500,
+ });
+ return await getAllMenusApi();
+ },
+ // 可以指定没有权限跳转403页面
+ forbiddenComponent,
+ // 如果 route.meta.menuVisibleWithForbidden = true
+ layoutMap,
+ pageMap,
+ });
+}
+
+export { generateAccess };
diff --git a/apps/web-tdesign/src/router/guard.ts b/apps/web-tdesign/src/router/guard.ts
new file mode 100644
index 00000000..a1ad6d88
--- /dev/null
+++ b/apps/web-tdesign/src/router/guard.ts
@@ -0,0 +1,133 @@
+import type { Router } from 'vue-router';
+
+import { LOGIN_PATH } from '@vben/constants';
+import { preferences } from '@vben/preferences';
+import { useAccessStore, useUserStore } from '@vben/stores';
+import { startProgress, stopProgress } from '@vben/utils';
+
+import { accessRoutes, coreRouteNames } from '#/router/routes';
+import { useAuthStore } from '#/store';
+
+import { generateAccess } from './access';
+
+/**
+ * 通用守卫配置
+ * @param router
+ */
+function setupCommonGuard(router: Router) {
+ // 记录已经加载的页面
+ const loadedPaths = new Set();
+
+ router.beforeEach((to) => {
+ to.meta.loaded = loadedPaths.has(to.path);
+
+ // 页面加载进度条
+ if (!to.meta.loaded && preferences.transition.progress) {
+ startProgress();
+ }
+ return true;
+ });
+
+ router.afterEach((to) => {
+ // 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
+
+ loadedPaths.add(to.path);
+
+ // 关闭页面加载进度条
+ if (preferences.transition.progress) {
+ stopProgress();
+ }
+ });
+}
+
+/**
+ * 权限访问守卫配置
+ * @param router
+ */
+function setupAccessGuard(router: Router) {
+ router.beforeEach(async (to, from) => {
+ const accessStore = useAccessStore();
+ const userStore = useUserStore();
+ const authStore = useAuthStore();
+
+ // 基本路由,这些路由不需要进入权限拦截
+ if (coreRouteNames.includes(to.name as string)) {
+ if (to.path === LOGIN_PATH && accessStore.accessToken) {
+ return decodeURIComponent(
+ (to.query?.redirect as string) ||
+ userStore.userInfo?.homePath ||
+ preferences.app.defaultHomePath,
+ );
+ }
+ return true;
+ }
+
+ // accessToken 检查
+ if (!accessStore.accessToken) {
+ // 明确声明忽略权限访问权限,则可以访问
+ if (to.meta.ignoreAccess) {
+ return true;
+ }
+
+ // 没有访问权限,跳转登录页面
+ if (to.fullPath !== LOGIN_PATH) {
+ return {
+ path: LOGIN_PATH,
+ // 如不需要,直接删除 query
+ query:
+ to.fullPath === preferences.app.defaultHomePath
+ ? {}
+ : { redirect: encodeURIComponent(to.fullPath) },
+ // 携带当前跳转的页面,登录后重新跳转该页面
+ replace: true,
+ };
+ }
+ return to;
+ }
+
+ // 是否已经生成过动态路由
+ if (accessStore.isAccessChecked) {
+ return true;
+ }
+
+ // 生成路由表
+ // 当前登录用户拥有的角色标识列表
+ const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
+ const userRoles = userInfo.roles ?? [];
+
+ // 生成菜单和路由
+ const { accessibleMenus, accessibleRoutes } = await generateAccess({
+ roles: userRoles,
+ router,
+ // 则会在菜单中显示,但是访问会被重定向到403
+ routes: accessRoutes,
+ });
+
+ // 保存菜单信息和路由信息
+ accessStore.setAccessMenus(accessibleMenus);
+ accessStore.setAccessRoutes(accessibleRoutes);
+ accessStore.setIsAccessChecked(true);
+ const redirectPath = (from.query.redirect ??
+ (to.path === preferences.app.defaultHomePath
+ ? userInfo.homePath || preferences.app.defaultHomePath
+ : to.fullPath)) as string;
+
+ return {
+ ...router.resolve(decodeURIComponent(redirectPath)),
+ replace: true,
+ };
+ });
+}
+
+/**
+ * 项目守卫配置
+ * @param router
+ */
+function createRouterGuard(router: Router) {
+ /** 通用 */
+ setupCommonGuard(router);
+ /** 权限访问 */
+ setupAccessGuard(router);
+}
+
+export { createRouterGuard };
diff --git a/apps/web-tdesign/src/router/index.ts b/apps/web-tdesign/src/router/index.ts
new file mode 100644
index 00000000..48402303
--- /dev/null
+++ b/apps/web-tdesign/src/router/index.ts
@@ -0,0 +1,37 @@
+import {
+ createRouter,
+ createWebHashHistory,
+ createWebHistory,
+} from 'vue-router';
+
+import { resetStaticRoutes } from '@vben/utils';
+
+import { createRouterGuard } from './guard';
+import { routes } from './routes';
+
+/**
+ * @zh_CN 创建vue-router实例
+ */
+const router = createRouter({
+ history:
+ import.meta.env.VITE_ROUTER_HISTORY === 'hash'
+ ? createWebHashHistory(import.meta.env.VITE_BASE)
+ : createWebHistory(import.meta.env.VITE_BASE),
+ // 应该添加到路由的初始路由列表。
+ routes,
+ scrollBehavior: (to, _from, savedPosition) => {
+ if (savedPosition) {
+ return savedPosition;
+ }
+ return to.hash ? { behavior: 'smooth', el: to.hash } : { left: 0, top: 0 };
+ },
+ // 是否应该禁止尾部斜杠。
+ // strict: true,
+});
+
+const resetRoutes = () => resetStaticRoutes(router, routes);
+
+// 创建路由守卫
+createRouterGuard(router);
+
+export { resetRoutes, router };
diff --git a/apps/web-tdesign/src/router/routes/core.ts b/apps/web-tdesign/src/router/routes/core.ts
new file mode 100644
index 00000000..949b0b65
--- /dev/null
+++ b/apps/web-tdesign/src/router/routes/core.ts
@@ -0,0 +1,97 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import { LOGIN_PATH } from '@vben/constants';
+import { preferences } from '@vben/preferences';
+
+import { $t } from '#/locales';
+
+const BasicLayout = () => import('#/layouts/basic.vue');
+const AuthPageLayout = () => import('#/layouts/auth.vue');
+/** 全局404页面 */
+const fallbackNotFoundRoute: RouteRecordRaw = {
+ component: () => import('#/views/_core/fallback/not-found.vue'),
+ meta: {
+ hideInBreadcrumb: true,
+ hideInMenu: true,
+ hideInTab: true,
+ title: '404',
+ },
+ name: 'FallbackNotFound',
+ path: '/:path(.*)*',
+};
+
+/** 基本路由,这些路由是必须存在的 */
+const coreRoutes: RouteRecordRaw[] = [
+ /**
+ * 根路由
+ * 使用基础布局,作为所有页面的父级容器,子级就不必配置BasicLayout。
+ * 此路由必须存在,且不应修改
+ */
+ {
+ component: BasicLayout,
+ meta: {
+ hideInBreadcrumb: true,
+ title: 'Root',
+ },
+ name: 'Root',
+ path: '/',
+ redirect: preferences.app.defaultHomePath,
+ children: [],
+ },
+ {
+ component: AuthPageLayout,
+ meta: {
+ hideInTab: true,
+ title: 'Authentication',
+ },
+ name: 'Authentication',
+ path: '/auth',
+ redirect: LOGIN_PATH,
+ children: [
+ {
+ name: 'Login',
+ path: 'login',
+ component: () => import('#/views/_core/authentication/login.vue'),
+ meta: {
+ title: $t('page.auth.login'),
+ },
+ },
+ {
+ name: 'CodeLogin',
+ path: 'code-login',
+ component: () => import('#/views/_core/authentication/code-login.vue'),
+ meta: {
+ title: $t('page.auth.codeLogin'),
+ },
+ },
+ {
+ name: 'QrCodeLogin',
+ path: 'qrcode-login',
+ component: () =>
+ import('#/views/_core/authentication/qrcode-login.vue'),
+ meta: {
+ title: $t('page.auth.qrcodeLogin'),
+ },
+ },
+ {
+ name: 'ForgetPassword',
+ path: 'forget-password',
+ component: () =>
+ import('#/views/_core/authentication/forget-password.vue'),
+ meta: {
+ title: $t('page.auth.forgetPassword'),
+ },
+ },
+ {
+ name: 'Register',
+ path: 'register',
+ component: () => import('#/views/_core/authentication/register.vue'),
+ meta: {
+ title: $t('page.auth.register'),
+ },
+ },
+ ],
+ },
+];
+
+export { coreRoutes, fallbackNotFoundRoute };
diff --git a/apps/web-tdesign/src/router/routes/index.ts b/apps/web-tdesign/src/router/routes/index.ts
new file mode 100644
index 00000000..e6fb1440
--- /dev/null
+++ b/apps/web-tdesign/src/router/routes/index.ts
@@ -0,0 +1,37 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import { mergeRouteModules, traverseTreeValues } from '@vben/utils';
+
+import { coreRoutes, fallbackNotFoundRoute } from './core';
+
+const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
+ eager: true,
+});
+
+// 有需要可以自行打开注释,并创建文件夹
+// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
+// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
+
+/** 动态路由 */
+const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
+
+/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
+// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
+// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
+const staticRoutes: RouteRecordRaw[] = [];
+const externalRoutes: RouteRecordRaw[] = [];
+
+/** 路由列表,由基本路由、外部路由和404兜底路由组成
+ * 无需走权限验证(会一直显示在菜单中) */
+const routes: RouteRecordRaw[] = [
+ ...coreRoutes,
+ ...externalRoutes,
+ fallbackNotFoundRoute,
+];
+
+/** 基本路由列表,这些路由不需要进入权限拦截 */
+const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
+
+/** 有权限校验的路由列表,包含动态路由和静态路由 */
+const accessRoutes = [...dynamicRoutes, ...staticRoutes];
+export { accessRoutes, coreRouteNames, routes };
diff --git a/apps/web-tdesign/src/router/routes/modules/dashboard.ts b/apps/web-tdesign/src/router/routes/modules/dashboard.ts
new file mode 100644
index 00000000..44f15ef9
--- /dev/null
+++ b/apps/web-tdesign/src/router/routes/modules/dashboard.ts
@@ -0,0 +1,38 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import { $t } from '#/locales';
+
+const routes: RouteRecordRaw[] = [
+ {
+ meta: {
+ icon: 'lucide:layout-dashboard',
+ order: -1,
+ title: $t('page.dashboard.title'),
+ },
+ name: 'Dashboard',
+ path: '/dashboard',
+ children: [
+ {
+ name: 'Analytics',
+ path: 'analytics',
+ component: () => import('#/views/dashboard/analytics/index.vue'),
+ meta: {
+ affixTab: true,
+ icon: 'lucide:area-chart',
+ title: $t('page.dashboard.analytics'),
+ },
+ },
+ {
+ name: 'Workspace',
+ path: 'workspace',
+ component: () => import('#/views/dashboard/workspace/index.vue'),
+ meta: {
+ icon: 'carbon:workspace',
+ title: $t('page.dashboard.workspace'),
+ },
+ },
+ ],
+ },
+];
+
+export default routes;
diff --git a/apps/web-tdesign/src/router/routes/modules/demos.ts b/apps/web-tdesign/src/router/routes/modules/demos.ts
new file mode 100644
index 00000000..527f9b25
--- /dev/null
+++ b/apps/web-tdesign/src/router/routes/modules/demos.ts
@@ -0,0 +1,28 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import { $t } from '#/locales';
+
+const routes: RouteRecordRaw[] = [
+ {
+ meta: {
+ icon: 'ic:baseline-view-in-ar',
+ keepAlive: true,
+ order: 1000,
+ title: $t('demos.title'),
+ },
+ name: 'Demos',
+ path: '/demos',
+ children: [
+ {
+ meta: {
+ title: $t('demos.tdesign'),
+ },
+ name: 'TDesignDemos',
+ path: 'tdesign',
+ component: () => import('#/views/demos/tdesign/index.vue'),
+ },
+ ],
+ },
+];
+
+export default routes;
diff --git a/apps/web-tdesign/src/router/routes/modules/vben.ts b/apps/web-tdesign/src/router/routes/modules/vben.ts
new file mode 100644
index 00000000..cb125e33
--- /dev/null
+++ b/apps/web-tdesign/src/router/routes/modules/vben.ts
@@ -0,0 +1,116 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import {
+ VBEN_ANT_PREVIEW_URL,
+ VBEN_ANTDV_NEXT_PREVIEW_URL,
+ VBEN_DOC_URL,
+ VBEN_ELE_PREVIEW_URL,
+ VBEN_GITHUB_URL,
+ VBEN_LOGO_URL,
+ VBEN_NAIVE_PREVIEW_URL,
+} from '@vben/constants';
+import { SvgAntdvLogoIcon, SvgAntdvNextLogoIcon } from '@vben/icons';
+
+import { IFrameView } from '#/layouts';
+import { $t } from '#/locales';
+
+const routes: RouteRecordRaw[] = [
+ {
+ meta: {
+ badgeType: 'dot',
+ icon: VBEN_LOGO_URL,
+ order: 9998,
+ title: $t('demos.vben.title'),
+ },
+ name: 'VbenProject',
+ path: '/vben-admin',
+ children: [
+ {
+ name: 'VbenDocument',
+ path: 'document',
+ component: IFrameView,
+ meta: {
+ icon: 'lucide:book-open-text',
+ link: VBEN_DOC_URL,
+ title: $t('demos.vben.document'),
+ },
+ },
+ {
+ name: 'VbenGithub',
+ path: 'github',
+ component: IFrameView,
+ meta: {
+ icon: 'mdi:github',
+ link: VBEN_GITHUB_URL,
+ title: 'Github',
+ },
+ },
+ {
+ name: 'VbenNaive',
+ path: 'naive',
+ component: IFrameView,
+ meta: {
+ badgeType: 'dot',
+ icon: 'logos:naiveui',
+ link: VBEN_NAIVE_PREVIEW_URL,
+ title: $t('demos.vben.naive-ui'),
+ },
+ },
+ {
+ name: 'VbenAntdv',
+ path: 'antdv',
+ component: IFrameView,
+ meta: {
+ badgeType: 'dot',
+ icon: SvgAntdvLogoIcon,
+ link: VBEN_ANT_PREVIEW_URL,
+ title: $t('demos.vben.antdv'),
+ },
+ },
+ {
+ name: 'VbenAntdVNext',
+ path: 'antdv-next',
+ component: IFrameView,
+ meta: {
+ badgeType: 'dot',
+ icon: SvgAntdvNextLogoIcon,
+ link: VBEN_ANTDV_NEXT_PREVIEW_URL,
+ title: $t('demos.vben.antdv-next'),
+ },
+ },
+ {
+ name: 'VbenElementPlus',
+ path: 'ele',
+ component: IFrameView,
+ meta: {
+ badgeType: 'dot',
+ icon: 'logos:element',
+ link: VBEN_ELE_PREVIEW_URL,
+ title: $t('demos.vben.element-plus'),
+ },
+ },
+ ],
+ },
+ {
+ name: 'VbenAbout',
+ path: '/vben-admin/about',
+ component: () => import('#/views/_core/about/index.vue'),
+ meta: {
+ icon: 'lucide:copyright',
+ title: $t('demos.vben.about'),
+ order: 9999,
+ },
+ },
+ {
+ name: 'Profile',
+ path: '/profile',
+ component: () => import('#/views/_core/profile/index.vue'),
+ meta: {
+ icon: 'lucide:user',
+ hideInMenu: true,
+ title: $t('page.auth.profile'),
+ },
+ },
+];
+
+export default routes;
diff --git a/apps/web-tdesign/src/store/auth.ts b/apps/web-tdesign/src/store/auth.ts
new file mode 100644
index 00000000..7f8d8f9b
--- /dev/null
+++ b/apps/web-tdesign/src/store/auth.ts
@@ -0,0 +1,116 @@
+import type { Recordable, UserInfo } from '@vben/types';
+
+import { ref } from 'vue';
+import { useRouter } from 'vue-router';
+
+import { LOGIN_PATH } from '@vben/constants';
+import { preferences } from '@vben/preferences';
+import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
+
+import { defineStore } from 'pinia';
+
+import { notification } from '#/adapter/tdesign';
+import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api';
+import { $t } from '#/locales';
+
+export const useAuthStore = defineStore('auth', () => {
+ const accessStore = useAccessStore();
+ const userStore = useUserStore();
+ const router = useRouter();
+
+ const loginLoading = ref(false);
+
+ /**
+ * 异步处理登录操作
+ * Asynchronously handle the login process
+ * @param params 登录表单数据
+ */
+ async function authLogin(
+ params: Recordable,
+ onSuccess?: () => Promise | void,
+ ) {
+ // 异步处理用户登录操作并获取 accessToken
+ let userInfo: null | UserInfo = null;
+ try {
+ loginLoading.value = true;
+ const { accessToken } = await loginApi(params);
+
+ // 如果成功获取到 accessToken
+ if (accessToken) {
+ accessStore.setAccessToken(accessToken);
+ // 获取用户信息并存储到 accessStore 中
+ const [fetchUserInfoResult, accessCodes] = await Promise.all([
+ fetchUserInfo(),
+ getAccessCodesApi(),
+ ]);
+
+ userInfo = fetchUserInfoResult;
+
+ userStore.setUserInfo(userInfo);
+ accessStore.setAccessCodes(accessCodes);
+
+ if (accessStore.loginExpired) {
+ accessStore.setLoginExpired(false);
+ } else {
+ onSuccess
+ ? await onSuccess?.()
+ : await router.push(
+ userInfo.homePath || preferences.app.defaultHomePath,
+ );
+ }
+
+ if (userInfo?.realName) {
+ notification.success({
+ title: $t('authentication.loginSuccess'),
+ content: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,
+ duration: 3000,
+ });
+ }
+ }
+ } finally {
+ loginLoading.value = false;
+ }
+
+ return {
+ userInfo,
+ };
+ }
+
+ async function logout(redirect: boolean = true) {
+ try {
+ await logoutApi();
+ } catch {
+ // 不做任何处理
+ }
+ resetAllStores();
+ accessStore.setLoginExpired(false);
+
+ // 回登录页带上当前路由地址
+ await router.replace({
+ path: LOGIN_PATH,
+ query: redirect
+ ? {
+ redirect: encodeURIComponent(router.currentRoute.value.fullPath),
+ }
+ : {},
+ });
+ }
+
+ async function fetchUserInfo() {
+ const userInfo = await getUserInfoApi();
+ userStore.setUserInfo(userInfo);
+ return userInfo;
+ }
+
+ function $reset() {
+ loginLoading.value = false;
+ }
+
+ return {
+ $reset,
+ authLogin,
+ fetchUserInfo,
+ loginLoading,
+ logout,
+ };
+});
diff --git a/apps/web-tdesign/src/store/index.ts b/apps/web-tdesign/src/store/index.ts
new file mode 100644
index 00000000..269586ee
--- /dev/null
+++ b/apps/web-tdesign/src/store/index.ts
@@ -0,0 +1 @@
+export * from './auth';
diff --git a/apps/web-tdesign/src/views/_core/README.md b/apps/web-tdesign/src/views/_core/README.md
new file mode 100644
index 00000000..8248afe6
--- /dev/null
+++ b/apps/web-tdesign/src/views/_core/README.md
@@ -0,0 +1,3 @@
+# \_core
+
+此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。
diff --git a/apps/web-tdesign/src/views/_core/about/index.vue b/apps/web-tdesign/src/views/_core/about/index.vue
new file mode 100644
index 00000000..0ee52433
--- /dev/null
+++ b/apps/web-tdesign/src/views/_core/about/index.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/apps/web-tdesign/src/views/_core/authentication/code-login.vue b/apps/web-tdesign/src/views/_core/authentication/code-login.vue
new file mode 100644
index 00000000..02370f7d
--- /dev/null
+++ b/apps/web-tdesign/src/views/_core/authentication/code-login.vue
@@ -0,0 +1,68 @@
+
+
+
+
+
diff --git a/apps/web-tdesign/src/views/_core/authentication/forget-password.vue b/apps/web-tdesign/src/views/_core/authentication/forget-password.vue
new file mode 100644
index 00000000..10444d0a
--- /dev/null
+++ b/apps/web-tdesign/src/views/_core/authentication/forget-password.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
diff --git a/apps/web-tdesign/src/views/_core/authentication/login.vue b/apps/web-tdesign/src/views/_core/authentication/login.vue
new file mode 100644
index 00000000..89af0c27
--- /dev/null
+++ b/apps/web-tdesign/src/views/_core/authentication/login.vue
@@ -0,0 +1,99 @@
+
+
+
+
+
diff --git a/apps/web-tdesign/src/views/_core/authentication/qrcode-login.vue b/apps/web-tdesign/src/views/_core/authentication/qrcode-login.vue
new file mode 100644
index 00000000..23f5f2da
--- /dev/null
+++ b/apps/web-tdesign/src/views/_core/authentication/qrcode-login.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/apps/web-tdesign/src/views/_core/authentication/register.vue b/apps/web-tdesign/src/views/_core/authentication/register.vue
new file mode 100644
index 00000000..6805d558
--- /dev/null
+++ b/apps/web-tdesign/src/views/_core/authentication/register.vue
@@ -0,0 +1,95 @@
+
+
+
+
+
diff --git a/apps/web-tdesign/src/views/_core/fallback/coming-soon.vue b/apps/web-tdesign/src/views/_core/fallback/coming-soon.vue
new file mode 100644
index 00000000..f394930f
--- /dev/null
+++ b/apps/web-tdesign/src/views/_core/fallback/coming-soon.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/apps/web-tdesign/src/views/_core/fallback/forbidden.vue b/apps/web-tdesign/src/views/_core/fallback/forbidden.vue
new file mode 100644
index 00000000..8ea65fed
--- /dev/null
+++ b/apps/web-tdesign/src/views/_core/fallback/forbidden.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/apps/web-tdesign/src/views/_core/fallback/internal-error.vue b/apps/web-tdesign/src/views/_core/fallback/internal-error.vue
new file mode 100644
index 00000000..819a47d5
--- /dev/null
+++ b/apps/web-tdesign/src/views/_core/fallback/internal-error.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/apps/web-tdesign/src/views/_core/fallback/not-found.vue b/apps/web-tdesign/src/views/_core/fallback/not-found.vue
new file mode 100644
index 00000000..4d178e9c
--- /dev/null
+++ b/apps/web-tdesign/src/views/_core/fallback/not-found.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/apps/web-tdesign/src/views/_core/fallback/offline.vue b/apps/web-tdesign/src/views/_core/fallback/offline.vue
new file mode 100644
index 00000000..5de4a88d
--- /dev/null
+++ b/apps/web-tdesign/src/views/_core/fallback/offline.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/apps/web-tdesign/src/views/_core/profile/base-setting.vue b/apps/web-tdesign/src/views/_core/profile/base-setting.vue
new file mode 100644
index 00000000..aa8a4c26
--- /dev/null
+++ b/apps/web-tdesign/src/views/_core/profile/base-setting.vue
@@ -0,0 +1,65 @@
+
+
+
+
diff --git a/apps/web-tdesign/src/views/_core/profile/index.vue b/apps/web-tdesign/src/views/_core/profile/index.vue
new file mode 100644
index 00000000..8740894e
--- /dev/null
+++ b/apps/web-tdesign/src/views/_core/profile/index.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-tdesign/src/views/_core/profile/notification-setting.vue b/apps/web-tdesign/src/views/_core/profile/notification-setting.vue
new file mode 100644
index 00000000..324a4b39
--- /dev/null
+++ b/apps/web-tdesign/src/views/_core/profile/notification-setting.vue
@@ -0,0 +1,31 @@
+
+
+
+
diff --git a/apps/web-tdesign/src/views/_core/profile/password-setting.vue b/apps/web-tdesign/src/views/_core/profile/password-setting.vue
new file mode 100644
index 00000000..eda3f3d4
--- /dev/null
+++ b/apps/web-tdesign/src/views/_core/profile/password-setting.vue
@@ -0,0 +1,63 @@
+
+
+
+
diff --git a/apps/web-tdesign/src/views/_core/profile/security-setting.vue b/apps/web-tdesign/src/views/_core/profile/security-setting.vue
new file mode 100644
index 00000000..be30db58
--- /dev/null
+++ b/apps/web-tdesign/src/views/_core/profile/security-setting.vue
@@ -0,0 +1,43 @@
+
+
+
+
diff --git a/apps/web-tdesign/src/views/dashboard/analytics/analytics-trends.vue b/apps/web-tdesign/src/views/dashboard/analytics/analytics-trends.vue
new file mode 100644
index 00000000..f1f0b232
--- /dev/null
+++ b/apps/web-tdesign/src/views/dashboard/analytics/analytics-trends.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
diff --git a/apps/web-tdesign/src/views/dashboard/analytics/analytics-visits-data.vue b/apps/web-tdesign/src/views/dashboard/analytics/analytics-visits-data.vue
new file mode 100644
index 00000000..190fb41f
--- /dev/null
+++ b/apps/web-tdesign/src/views/dashboard/analytics/analytics-visits-data.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
diff --git a/apps/web-tdesign/src/views/dashboard/analytics/analytics-visits-sales.vue b/apps/web-tdesign/src/views/dashboard/analytics/analytics-visits-sales.vue
new file mode 100644
index 00000000..6ff52086
--- /dev/null
+++ b/apps/web-tdesign/src/views/dashboard/analytics/analytics-visits-sales.vue
@@ -0,0 +1,46 @@
+
+
+
+
+
diff --git a/apps/web-tdesign/src/views/dashboard/analytics/analytics-visits-source.vue b/apps/web-tdesign/src/views/dashboard/analytics/analytics-visits-source.vue
new file mode 100644
index 00000000..0915c7af
--- /dev/null
+++ b/apps/web-tdesign/src/views/dashboard/analytics/analytics-visits-source.vue
@@ -0,0 +1,65 @@
+
+
+
+
+
diff --git a/apps/web-tdesign/src/views/dashboard/analytics/analytics-visits.vue b/apps/web-tdesign/src/views/dashboard/analytics/analytics-visits.vue
new file mode 100644
index 00000000..7e0f1013
--- /dev/null
+++ b/apps/web-tdesign/src/views/dashboard/analytics/analytics-visits.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
diff --git a/apps/web-tdesign/src/views/dashboard/analytics/index.vue b/apps/web-tdesign/src/views/dashboard/analytics/index.vue
new file mode 100644
index 00000000..e794c99a
--- /dev/null
+++ b/apps/web-tdesign/src/views/dashboard/analytics/index.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
diff --git a/apps/web-tdesign/src/views/dashboard/workspace/index.vue b/apps/web-tdesign/src/views/dashboard/workspace/index.vue
new file mode 100644
index 00000000..b95d6138
--- /dev/null
+++ b/apps/web-tdesign/src/views/dashboard/workspace/index.vue
@@ -0,0 +1,266 @@
+
+
+
+
+
+
+ 早安, {{ userStore.userInfo?.realName }}, 开始您一天的工作吧!
+
+ 今日晴,20℃ - 32℃!
+
+
+
+
+
diff --git a/apps/web-tdesign/src/views/demos/tdesign/index.vue b/apps/web-tdesign/src/views/demos/tdesign/index.vue
new file mode 100644
index 00000000..f6342ae3
--- /dev/null
+++ b/apps/web-tdesign/src/views/demos/tdesign/index.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+ Default
+ Primary
+ Info
+ Error
+
+
+
+
+ 信息
+ 错误
+ 警告
+ 成功
+
+
+
+
+
+ 信息
+ 错误
+ 警告
+ 成功
+
+
+
+
diff --git a/apps/web-tdesign/tsconfig.json b/apps/web-tdesign/tsconfig.json
new file mode 100644
index 00000000..858a0ec0
--- /dev/null
+++ b/apps/web-tdesign/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "@vben/tsconfig/web-app.json",
+ "compilerOptions": {
+ "paths": {
+ "#/*": ["./src/*"]
+ }
+ },
+ "references": [{ "path": "./tsconfig.node.json" }],
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
+}
diff --git a/apps/web-tdesign/tsconfig.node.json b/apps/web-tdesign/tsconfig.node.json
new file mode 100644
index 00000000..36e9fb5f
--- /dev/null
+++ b/apps/web-tdesign/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "@vben/tsconfig/node.json",
+ "compilerOptions": {
+ "composite": true,
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "noEmit": false
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/apps/web-tdesign/vite.config.ts b/apps/web-tdesign/vite.config.ts
new file mode 100644
index 00000000..c79ec9a4
--- /dev/null
+++ b/apps/web-tdesign/vite.config.ts
@@ -0,0 +1,27 @@
+import { defineConfig, viteCssLayerPlugin } from '@vben/vite-config';
+
+export default defineConfig(async () => {
+ return {
+ application: {},
+ vite: {
+ plugins: [
+ // tdesign 的 css 包进 @layer td,使 Tailwind 工具类可覆盖组件样式
+ viteCssLayerPlugin({
+ layerName: 'td',
+ packageName: 'tdesign-vue-next',
+ }),
+ ],
+ server: {
+ proxy: {
+ '/api': {
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api/, ''),
+ // mock代理目标地址
+ target: 'http://localhost:5320/api',
+ ws: true,
+ },
+ },
+ },
+ },
+ };
+});
diff --git a/cspell.json b/cspell.json
index 89545b43..982392e1 100644
--- a/cspell.json
+++ b/cspell.json
@@ -7,40 +7,64 @@
"acmr",
"antd",
"antdv",
+ "archiver",
"astro",
+ "axios",
"brotli",
+ "cascader",
+ "chatcmpl",
"clsx",
+ "dedup",
"defu",
"demi",
+ "dotenv",
+ "echart",
"echarts",
"ependencies",
+ "esbuild",
"esno",
"etag",
"execa",
"iconify",
"iconoir",
+ "indexeddb",
"intlify",
+ "isequal",
+ "jspm",
"lockb",
"lucide",
"minh",
"minw",
"mkdist",
"mockjs",
+ "myapp",
"naiveui",
+ "napi",
"nocheck",
+ "nolebase",
"noopener",
"noreferrer",
"nprogress",
"nuxt",
+ "organisation",
+ "oxfmt",
+ "oxlint",
"pinia",
"prefixs",
"publint",
"qrcode",
+ "reka",
+ "rollup",
"shadcn",
"sonner",
"sortablejs",
"styl",
+ "tabler",
"taze",
+ "tdesign",
+ "tsdown",
+ "tsgolint",
+ "turborepo",
"ui-kit",
"uicons",
"unplugin",
@@ -50,19 +74,20 @@
"vite",
"vitejs",
"vitepress",
+ "vitest",
"vnode",
"vueuse",
"yxxx"
],
"ignorePaths": [
- "**/node_modules/**",
- "**/dist/**",
"**/*-dist/**",
- "**/icons/**",
- "pnpm-lock.yaml",
"**/*.log",
- "**/*.test.ts",
"**/*.spec.ts",
- "**/__tests__/**"
+ "**/*.test.ts",
+ "**/__tests__/**",
+ "**/dist/**",
+ "**/icons/**",
+ "**/node_modules/**",
+ "pnpm-lock.yaml"
]
}
diff --git a/docs/.vitepress/build/dayjs-loader.mjs b/docs/.vitepress/build/dayjs-loader.mjs
new file mode 100644
index 00000000..623c394f
--- /dev/null
+++ b/docs/.vitepress/build/dayjs-loader.mjs
@@ -0,0 +1,2 @@
+import { register } from 'node:module';
+register('./dayjs-resolve-hook.mjs', import.meta.url);
diff --git a/docs/.vitepress/build/dayjs-resolve-hook.mjs b/docs/.vitepress/build/dayjs-resolve-hook.mjs
new file mode 100644
index 00000000..f8989044
--- /dev/null
+++ b/docs/.vitepress/build/dayjs-resolve-hook.mjs
@@ -0,0 +1,10 @@
+const DAYJS_SUBPATH_RE = /^dayjs\/(plugin|locale)\/([^./]+)$/;
+
+/** @type {import('node:module').ResolveHook} */
+export async function resolve(specifier, context, nextResolve) {
+ const match = specifier.match(DAYJS_SUBPATH_RE);
+ if (match) {
+ return nextResolve(`${specifier}.js`, context);
+ }
+ return nextResolve(specifier, context);
+}
diff --git a/docs/.vitepress/components/demo-preview.vue b/docs/.vitepress/components/demo-preview.vue
index 4c8829f8..cca9c749 100644
--- a/docs/.vitepress/components/demo-preview.vue
+++ b/docs/.vitepress/components/demo-preview.vue
@@ -19,15 +19,15 @@ const parsedFiles = computed(() => {
-
+
-
-
+
+
ERROR:
The preview directory does not exist. Please check the 'dir'
diff --git a/docs/.vitepress/components/preview-group.vue b/docs/.vitepress/components/preview-group.vue
index e712157c..f816dd34 100644
--- a/docs/.vitepress/components/preview-group.vue
+++ b/docs/.vitepress/components/preview-group.vue
@@ -5,14 +5,14 @@ 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,
TabsList,
TabsRoot,
TabsTrigger,
-} from 'radix-vue';
+} from 'reka-ui';
defineOptions({
inheritAttrs: false,
@@ -48,23 +48,23 @@ const toggleOpen = () => {
-
+
-
+
{{ tab.label }}
@@ -81,7 +81,7 @@ const toggleOpen = () => {
@@ -92,7 +92,7 @@ const toggleOpen = () => {