Merge remote-tracking branch 'vben/main'

# Conflicts:
#	README.md
#	apps/web-antd/src/adapter/component/index.ts
#	apps/web-antd/src/layouts/basic.vue
#	apps/web-antd/src/router/routes/modules/demos.ts
#	apps/web-antd/src/views/demos/antd/index.vue
#	package.json
#	pnpm-lock.yaml
This commit is contained in:
李琦
2026-08-10 13:28:08 +08:00
1326 changed files with 51436 additions and 8916 deletions

View File

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

View File

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

16
.gitattributes vendored
View File

@@ -4,8 +4,18 @@
* text=auto eol=lf * text=auto eol=lf
# Declare files that will always have CRLF line endings on checkout. # Declare files that will always have CRLF line endings on checkout.
*.{cmd,[cC][mM][dD]} text eol=crlf *.cmd text eol=crlf
*.{bat,[bB][aA][tT]} 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. # Denote all files that are truly binary and should not be modified.
*.{ico,png,jpg,jpeg,gif,webp,svg,woff,woff2} binary *.ico binary
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.webp binary
*.svg binary
*.woff binary
*.woff2 binary

View File

@@ -6,10 +6,10 @@ runs:
using: 'composite' using: 'composite'
steps: steps:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v6
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v6
with: with:
node-version-file: .node-version node-version-file: .node-version
cache: 'pnpm' cache: 'pnpm'

3
.github/config.yml vendored
View File

@@ -21,12 +21,10 @@ newPRWelcomeComment: |
firstPRMergeComment: > firstPRMergeComment: >
Thanks for your contribution! 🎉🎉🎉 Thanks for your contribution! 🎉🎉🎉
# Comment to be posted to on first time issues # Comment to be posted to on first time issues
newIssueWelcomeComment: > newIssueWelcomeComment: >
Thanks for opening your first issue! Be sure to follow the issue template and provide every bit of information to help the developers! Thanks for opening your first issue! Be sure to follow the issue template and provide every bit of information to help the developers!
# *OPTIONAL* default titles to check against for lack of descriptiveness # *OPTIONAL* default titles to check against for lack of descriptiveness
# MUST BE ALL LOWERCASE # MUST BE ALL LOWERCASE
requestInfoDefaultTitles: requestInfoDefaultTitles:
@@ -36,4 +34,3 @@ requestInfoDefaultTitles:
# *Required* Comment to reply with # *Required* Comment to reply with
requestInfoReplyComment: > requestInfoReplyComment: >
Thanks for filing this issue/PR! It would be much appreciated if you could provide us with more information so we can effectively analyze the situation in context. Thanks for filing this issue/PR! It would be much appreciated if you could provide us with more information so we can effectively analyze the situation in context.

View File

@@ -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. - Checkout a topic branch from the relevant branch, e.g. main, and merge back against that branch.
- If adding a new feature: - 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. - 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: - If fixing bug:
- Provide a detailed description of the bug in the PR. Live demo preferred. - 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. - It's OK to have multiple small commits as you work on the PR - GitHub can automatically squash them before merging.

View File

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

View File

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

View File

@@ -28,12 +28,12 @@ jobs:
timeout-minutes: 20 timeout-minutes: 20
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v6
with: with:
run_install: false run_install: false
@@ -67,7 +67,7 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -90,7 +90,7 @@ jobs:
- windows-latest - windows-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0

View File

@@ -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 # 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: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v7
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3 uses: github/codeql-action/init@v4
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }} build-mode: ${{ matrix.build-mode }}
@@ -89,6 +89,6 @@ jobs:
exit 1 exit 1
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3 uses: github/codeql-action/analyze@v4
with: with:
category: '/language:${{matrix.language}}' category: '/language:${{matrix.language}}'

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -30,7 +30,7 @@ jobs:
run: pnpm build:play run: pnpm build:play
- name: Sync Playground files - name: Sync Playground files
uses: SamKirkland/FTP-Deploy-Action@v4.3.5 uses: SamKirkland/FTP-Deploy-Action@v4.4.0
with: with:
server: ${{ secrets.PRO_FTP_HOST }} server: ${{ secrets.PRO_FTP_HOST }}
username: ${{ secrets.WEB_PLAYGROUND_FTP_ACCOUNT }} username: ${{ secrets.WEB_PLAYGROUND_FTP_ACCOUNT }}
@@ -43,7 +43,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -54,7 +54,7 @@ jobs:
run: pnpm build:docs run: pnpm build:docs
- name: Sync Docs files - name: Sync Docs files
uses: SamKirkland/FTP-Deploy-Action@v4.3.5 uses: SamKirkland/FTP-Deploy-Action@v4.4.0
with: with:
server: ${{ secrets.PRO_FTP_HOST }} server: ${{ secrets.PRO_FTP_HOST }}
username: ${{ secrets.WEBSITE_FTP_ACCOUNT }} username: ${{ secrets.WEBSITE_FTP_ACCOUNT }}
@@ -67,7 +67,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -85,7 +85,7 @@ jobs:
run: pnpm run build:antd run: pnpm run build:antd
- name: Sync files - name: Sync files
uses: SamKirkland/FTP-Deploy-Action@v4.3.5 uses: SamKirkland/FTP-Deploy-Action@v4.4.0
with: with:
server: ${{ secrets.PRO_FTP_HOST }} server: ${{ secrets.PRO_FTP_HOST }}
username: ${{ secrets.WEB_ANTD_FTP_ACCOUNT }} username: ${{ secrets.WEB_ANTD_FTP_ACCOUNT }}
@@ -98,7 +98,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -116,7 +116,7 @@ jobs:
run: pnpm run build:ele run: pnpm run build:ele
- name: Sync files - name: Sync files
uses: SamKirkland/FTP-Deploy-Action@v4.3.5 uses: SamKirkland/FTP-Deploy-Action@v4.4.0
with: with:
server: ${{ secrets.PRO_FTP_HOST }} server: ${{ secrets.PRO_FTP_HOST }}
username: ${{ secrets.WEB_ELE_FTP_ACCOUNT }} username: ${{ secrets.WEB_ELE_FTP_ACCOUNT }}
@@ -129,7 +129,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -147,7 +147,7 @@ jobs:
run: pnpm run build:naive run: pnpm run build:naive
- name: Sync files - name: Sync files
uses: SamKirkland/FTP-Deploy-Action@v4.3.5 uses: SamKirkland/FTP-Deploy-Action@v4.4.0
with: with:
server: ${{ secrets.PRO_FTP_HOST }} server: ${{ secrets.PRO_FTP_HOST }}
username: ${{ secrets.WEB_NAIVE_FTP_ACCOUNT }} username: ${{ secrets.WEB_NAIVE_FTP_ACCOUNT }}

View File

@@ -20,6 +20,6 @@ jobs:
if: github.repository == 'vbenjs/vue-vben-admin' if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: release-drafter/release-drafter@v6 - uses: release-drafter/release-drafter@v7
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

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

View File

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

View File

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

View File

@@ -3,78 +3,48 @@ name: Create Release Tag
on: on:
push: push:
tags: tags:
- 'v*.*.*' # Push events to matching v*, i.e. v1.0, v20.15.10 - 'v*.*.*'
workflow_dispatch:
env: inputs:
HUSKY: '0' tag:
description: 'Tag to create (e.g. v1.2.3)'
required: true
type: string
permissions: permissions:
pull-requests: write
contents: write contents: write
jobs: jobs:
build: release:
name: Create Release name: Create Release
if: github.repository == 'vbenjs/vue-vben-admin' if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20]
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
# - name: Checkout code - name: Extract version
# 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
id: version id: version
run: | run: |
tag=${GITHUB_REF/refs\/tags\//} if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
version=${tag#v} raw_tag="${{ inputs.tag }}"
major=${version%%.*} else
echo "tag=${tag}" >> $GITHUB_OUTPUT raw_tag="${GITHUB_REF_NAME}"
echo "version=${version}" >> $GITHUB_OUTPUT fi
echo "major=${major}" >> $GITHUB_OUTPUT # 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: with:
version: ${{ steps.version.outputs.version }} version: ${{ steps.version.outputs.version }}
publish: true publish: true
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 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.

View File

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

View File

@@ -9,7 +9,7 @@ jobs:
if: github.repository == 'vbenjs/vue-vben-admin' if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v9 - uses: actions/stale@v11
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} 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' 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'

14
.gitignore vendored
View File

@@ -22,7 +22,8 @@ yarn.lock
package-lock.json package-lock.json
.VSCodeCounter .VSCodeCounter
**/backend-mock/data **/backend-mock/data
.omx
.pnpm-store
# local env files # local env files
.env.local .env.local
.env.*.local .env.*.local
@@ -49,3 +50,14 @@ vite.config.ts.*
*.sln *.sln
*.sw? *.sw?
.history .history
.cursor
# AI
.agent
.agents
.claude
.codex
skills-lock.json
.atomcode
datalog
.playwright-mcp

View File

@@ -1 +1 @@
22.1.0 24.16.0

14
.npmrc
View File

@@ -1,13 +1 @@
registry = "https://registry.npmmirror.com" 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

View File

@@ -1,18 +0,0 @@
dist
dev-dist
.local
.output.js
node_modules
.nvmrc
coverage
CODEOWNERS
.nitro
.output
**/*.svg
**/*.sh
public
.npmrc
*-lock.yaml

View File

@@ -1 +0,0 @@
export { default } from '@vben/prettier-config';

View File

@@ -2,3 +2,7 @@ dist
public public
__tests__ __tests__
coverage coverage
.codex
.claude
.agent
.agents

View File

@@ -2,14 +2,18 @@
"recommendations": [ "recommendations": [
// Vue 3 的语言支持 // Vue 3 的语言支持
"Vue.volar", "Vue.volar",
// 将 ESLint JavaScript 集成到 VS Code 中。 // 将 eslint 集成到 VS Code 中。
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
// 将 oxlint 集成到 VS Code 中。
"oxc.oxc-vscode",
// Visual Studio Code 的官方 Stylelint 扩展 // Visual Studio Code 的官方 Stylelint 扩展
"stylelint.vscode-stylelint", "stylelint.vscode-stylelint",
// 使用 Prettier 的代码格式化程序 // 使用 oxfmt 的代码格式化程序
"esbenp.prettier-vscode", "oxc.oxc-vscode",
// 支持 dotenv 文件语法 // 支持 dotenv 文件语法
"mikestead.dotenv", "mikestead.dotenv",
// YAML 语言支持,供 ESLint 校验 pnpm-workspace.yaml 等文件
"redhat.vscode-yaml",
// 源代码的拼写检查器 // 源代码的拼写检查器
"streetsidesoftware.code-spell-checker", "streetsidesoftware.code-spell-checker",
// Tailwind CSS 的官方 VS Code 插件 // Tailwind CSS 的官方 VS Code 插件

70
.vscode/settings.json vendored
View File

@@ -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
"workbench.list.smoothScrolling": true, "workbench.list.smoothScrolling": true,
"workbench.startupEditor": "newUntitledFile", "workbench.startupEditor": "newUntitledFile",
@@ -31,39 +32,51 @@
"editor.autoClosingOvertype": "always", "editor.autoClosingOvertype": "always",
"editor.autoClosingQuotes": "beforeWhitespace", "editor.autoClosingQuotes": "beforeWhitespace",
"editor.wordSeparators": "`~!@#%^&*()=+[{]}\\|;:'\",.<>/?", "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": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit", "source.fixAll.eslint": "explicit",
"source.fixAll.oxc": "explicit",
"source.fixAll.stylelint": "explicit", "source.fixAll.stylelint": "explicit",
"source.organizeImports": "never" "source.organizeImports": "never"
}, },
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "oxc.oxc-vscode",
"[html]": { "[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "oxc.oxc-vscode"
}, },
"[css]": { "[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "oxc.oxc-vscode"
}, },
"[scss]": { "[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "oxc.oxc-vscode"
}, },
"[javascript]": { "[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "oxc.oxc-vscode"
}, },
"[typescript]": { "[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "oxc.oxc-vscode"
}, },
"[json]": { "[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "oxc.oxc-vscode"
}, },
"[markdown]": { "[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "oxc.oxc-vscode"
}, },
"[jsonc]": { "[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "oxc.oxc-vscode"
}, },
"[vue]": { "[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "oxc.oxc-vscode"
}, },
// extensions // extensions
"extensions.ignoreRecommendations": true, "extensions.ignoreRecommendations": true,
@@ -79,6 +92,7 @@
"files.insertFinalNewline": true, "files.insertFinalNewline": true,
"files.simpleDialog.enable": true, "files.simpleDialog.enable": true,
"files.associations": { "files.associations": {
"*.css": "tailwindcss",
"*.ejs": "html", "*.ejs": "html",
"*.art": "html", "*.art": "html",
"**/tsconfig.json": "jsonc", "**/tsconfig.json": "jsonc",
@@ -118,7 +132,7 @@
// search // search
"search.searchEditor.singleClickBehaviour": "peekDefinition", "search.searchEditor.singleClickBehaviour": "peekDefinition",
"search.followSymlinks": false, "search.followSymlinks": false,
// 在使用搜索功能时,将这些文件夹/文件排除在外 // 使用搜索功能时,将这些文件和文件夹排除在外
"search.exclude": { "search.exclude": {
"**/node_modules": true, "**/node_modules": true,
"**/*.log": true, "**/*.log": true,
@@ -158,7 +172,7 @@
"emmet.triggerExpansionOnTab": false, "emmet.triggerExpansionOnTab": false,
"errorLens.enabledDiagnosticLevels": ["warning", "error"], "errorLens.enabledDiagnosticLevels": ["warning", "error"],
"errorLens.excludeBySource": ["cSpell", "Grammarly", "eslint"], "errorLens.excludeBySource": ["cSpell", "Grammarly"],
"stylelint.enable": true, "stylelint.enable": true,
"stylelint.packageManager": "pnpm", "stylelint.packageManager": "pnpm",
@@ -166,9 +180,10 @@
"stylelint.customSyntax": "postcss-html", "stylelint.customSyntax": "postcss-html",
"stylelint.snippet": ["css", "less", "postcss", "scss", "vue"], "stylelint.snippet": ["css", "less", "postcss", "scss", "vue"],
"typescript.inlayHints.enumMemberValues.enabled": true, "js/ts.tsdk.path": "node_modules/typescript/lib",
"typescript.preferences.preferTypeOnlyAutoImports": true, "js/ts.inlayHints.enumMemberValues.enabled": true,
"typescript.preferences.includePackageJsonAutoImports": "on", "js/ts.preferences.preferTypeOnlyAutoImports": true,
"js/ts.preferences.includePackageJsonAutoImports": "on",
"eslint.validate": [ "eslint.validate": [
"javascript", "javascript",
@@ -180,7 +195,8 @@
"markdown", "markdown",
"json", "json",
"jsonc", "jsonc",
"json5" "json5",
"yaml"
], ],
"tailwindCSS.experimental.classRegex": [ "tailwindCSS.experimental.classRegex": [
@@ -194,7 +210,7 @@
"yaml": false "yaml": false
}, },
"cssVariables.lookupFiles": ["packages/core/base/design/src/**/*.css"], "cssVariables.lookupFiles": ["packages/@core/base/design/src/**/*.css"],
"i18n-ally.localesPaths": [ "i18n-ally.localesPaths": [
"packages/locales/src/langs", "packages/locales/src/langs",
@@ -219,23 +235,9 @@
"*.env": "$(capture).env.*", "*.env": "$(capture).env.*",
"README.md": "README*,CHANGELOG*,LICENSE,CNAME", "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", "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", "oxlint.config.ts": ".eslintignore,.stylelintignore,.commitlintrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json,lefthook.yml,oxfmt.config.*,eslint.config.*"
"tailwind.config.mjs": "postcss.*"
}, },
"commentTranslate.hover.enabled": false, "commentTranslate.hover.enabled": false,
"commentTranslate.multiLineMerge": true, "commentTranslate.multiLineMerge": true,
"vue.server.hybridMode": 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"
]
} }

View File

@@ -114,7 +114,7 @@ pnpm build
## ブラウザサポート ## ブラウザサポート
ローカル開発には `Chrome 80+` ブラウザを推奨します Tailwind CSS v4.0 is designed for Safari 16.4+, Chrome 111+, and Firefox 128+
モダンブラウザをサポートし、IEはサポートしません モダンブラウザをサポートし、IEはサポートしません
@@ -140,8 +140,12 @@ pnpm build
## 貢献者 ## 貢献者
<a href="https://openomy.app/github/vbenjs/vue-vben-admin" target="_blank" style="display: block; width: 100%;" align="center">
<img src="https://openomy.app/svg?repo=vbenjs/vue-vben-admin&chart=bubble&latestMonth=3" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
</a>
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors"> <a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" /> <img alt="Contributors" src="https://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
</a> </a>
## Discord ## Discord

158
README.md
View File

@@ -1 +1,157 @@
# 奶酪云后台管理脚手架 <div align="center">
<a href="https://github.com/anncwb/vue-vben-admin">
<img alt="VbenAdmin Logo" width="215" src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp">
</a>
<br>
<br>
[![license](https://img.shields.io/github/license/anncwb/vue-vben-admin.svg)](LICENSE)
<h1>Vue Vben Admin</h1>
</div>
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=vbenjs_vue-vben-admin&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin) [![codeql](https://github.com/vbenjs/vue-vben-admin/actions/workflows/codeql.yml/badge.svg)](https://github.com/vbenjs/vue-vben-admin/actions/workflows/codeql.yml) [![build](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml/badge.svg)](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml) [![ci](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml/badge.svg)](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml) [![deploy](https://github.com/vbenjs/vue-vben-admin/actions/workflows/deploy.yml/badge.svg)](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
<div align="center">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview1.png">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview2.png">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview3.png">
</div>
### Use Gitpod
Open the project in Gitpod (free online dev environment for GitHub) and start coding immediately.
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](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
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
| :-: | :-: | :-: | :-: |
| last 2 versions | last 2 versions | last 2 versions | last 2 versions |
## Maintainer
[@Vben](https://github.com/anncwb)
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=vbenjs/vue-vben-admin&type=Date)](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!
![donate](https://unpkg.com/@vbenjs/static-source@0.1.7/source/sponsor.png)
<a style="display: block;width: 100px;height: 50px;line-height: 50px; color: #fff;text-align: center; background: #408aee;border-radius: 4px;" href="https://www.paypal.com/paypalme/cvvben">Paypal Me</a>
## Contributors
<a href="https://openomy.app/github/vbenjs/vue-vben-admin" target="_blank" style="display: block; width: 100%;" align="center">
<img src="https://openomy.app/svg?repo=vbenjs/vue-vben-admin&chart=bubble&latestMonth=3" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
</a>
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
<img alt="Contributors" src="https://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
</a>
## Discord
- [Github Discussions](https://github.com/anncwb/vue-vben-admin/discussions)
## License
[MIT © Vben-2020](./LICENSE)

View File

@@ -114,7 +114,7 @@ pnpm build
## 浏览器支持 ## 浏览器支持
本地开发推荐使用 `Chrome 80+` 浏览器 Tailwind CSS v4.0 is designed for Safari 16.4+, Chrome 111+, and Firefox 128+
支持现代浏览器,不支持 IE 支持现代浏览器,不支持 IE
@@ -140,8 +140,12 @@ pnpm build
## 贡献者 ## 贡献者
<a href="https://openomy.app/github/vbenjs/vue-vben-admin" target="_blank" style="display: block; width: 100%;" align="center">
<img src="https://openomy.app/svg?repo=vbenjs/vue-vben-admin&chart=bubble&latestMonth=3" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
</a>
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors"> <a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" /> <img alt="Contributors" src="https://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
</a> </a>
## Discord ## Discord

View File

@@ -1,5 +1,7 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils'; 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) => { export default eventHandler((event) => {
const userinfo = verifyAccessToken(event); const userinfo = verifyAccessToken(event);

View File

@@ -1,9 +1,15 @@
import { defineEventHandler, readBody, setResponseStatus } from 'h3';
import { import {
clearRefreshTokenCookie, clearRefreshTokenCookie,
setRefreshTokenCookie, setRefreshTokenCookie,
} from '~/utils/cookie-utils'; } from '~/utils/cookie-utils';
import { generateAccessToken, generateRefreshToken } from '~/utils/jwt-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) => { export default defineEventHandler(async (event) => {
const { password, username } = await readBody(event); const { password, username } = await readBody(event);

View File

@@ -1,7 +1,9 @@
import { defineEventHandler } from 'h3';
import { import {
clearRefreshTokenCookie, clearRefreshTokenCookie,
getRefreshTokenFromCookie, getRefreshTokenFromCookie,
} from '~/utils/cookie-utils'; } from '~/utils/cookie-utils';
import { useResponseSuccess } from '~/utils/response';
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const refreshToken = getRefreshTokenFromCookie(event); const refreshToken = getRefreshTokenFromCookie(event);

View File

@@ -1,9 +1,11 @@
import { defineEventHandler } from 'h3';
import { import {
clearRefreshTokenCookie, clearRefreshTokenCookie,
getRefreshTokenFromCookie, getRefreshTokenFromCookie,
setRefreshTokenCookie, setRefreshTokenCookie,
} from '~/utils/cookie-utils'; } 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'; import { forbiddenResponse } from '~/utils/response';
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {

View File

@@ -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;
});

View File

@@ -1,5 +1,7 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils'; 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) => { export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event); const userinfo = verifyAccessToken(event);

View File

@@ -1,3 +1,6 @@
import { eventHandler, getQuery, setResponseStatus } from 'h3';
import { useResponseError } from '~/utils/response';
export default eventHandler((event) => { export default eventHandler((event) => {
const { status } = getQuery(event); const { status } = getQuery(event);
setResponseStatus(event, Number(status)); setResponseStatus(event, Number(status));

View File

@@ -1,3 +1,4 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils'; import { verifyAccessToken } from '~/utils/jwt-utils';
import { import {
sleep, sleep,

View File

@@ -1,3 +1,4 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils'; import { verifyAccessToken } from '~/utils/jwt-utils';
import { import {
sleep, sleep,

View File

@@ -1,3 +1,4 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils'; import { verifyAccessToken } from '~/utils/jwt-utils';
import { import {
sleep, sleep,

View File

@@ -1,4 +1,5 @@
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils'; import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response'; import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';

View File

@@ -1,3 +1,4 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils'; import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data'; import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response'; import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';

View File

@@ -1,6 +1,7 @@
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils'; import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data'; import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse } from '~/utils/response'; import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
const namesMap: Record<string, any> = {}; const namesMap: Record<string, any> = {};

View File

@@ -1,6 +1,7 @@
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils'; import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data'; import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse } from '~/utils/response'; import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
const pathMap: Record<string, any> = { '/': 0 }; const pathMap: Record<string, any> = { '/': 0 };

View File

@@ -1,4 +1,5 @@
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils'; import { verifyAccessToken } from '~/utils/jwt-utils';
import { getMenuIds, MOCK_MENU_LIST } from '~/utils/mock-data'; import { getMenuIds, MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response'; import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';

View File

@@ -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<string, any> = {
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);
});

View File

@@ -1,6 +1,11 @@
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils'; import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response'; import {
sleep,
unAuthorizedResponse,
usePageResponseSuccess,
} from '~/utils/response';
function generateMockDataList(count: number) { function generateMockDataList(count: number) {
const dataList = []; const dataList = [];
@@ -44,30 +49,69 @@ export default eventHandler(async (event) => {
await sleep(600); await sleep(600);
const { page, pageSize, sortBy, sortOrder } = getQuery(event); 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); 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) => { listData.sort((a, b) => {
if (sortOrder === 'asc') { const aValue = a[sortKey] as unknown;
if (sortBy === 'price') { const bValue = b[sortKey] as unknown;
return (
Number.parseFloat(a[sortBy as string]) - let result: number;
Number.parseFloat(b[sortBy as string])
); 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 { } else {
return a[sortBy as string] > b[sortBy as string] ? 1 : -1; result = aValue ? 1 : -1;
} }
} else { } else {
if (sortBy === 'price') { const aStr = String(aValue);
return ( const bStr = String(bValue);
Number.parseFloat(b[sortBy as string]) - const aNum = Number(aStr);
Number.parseFloat(a[sortBy as string]) const bNum = Number(bStr);
); result =
} else { Number.isFinite(aNum) && Number.isFinite(bNum)
return a[sortBy as string] < b[sortBy as string] ? 1 : -1; ? 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,
);
}); });

View File

@@ -1 +1,3 @@
import { defineEventHandler } from 'h3';
export default defineEventHandler(() => 'Test get handler'); export default defineEventHandler(() => 'Test get handler');

View File

@@ -1 +1,3 @@
import { defineEventHandler } from 'h3';
export default defineEventHandler(() => 'Test post handler'); export default defineEventHandler(() => 'Test post handler');

View File

@@ -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());
});

View File

@@ -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);
});

View File

@@ -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({});
});

View File

@@ -1,5 +1,6 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils'; import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response'; import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler((event) => { export default eventHandler((event) => {
const userinfo = verifyAccessToken(event); const userinfo = verifyAccessToken(event);

View File

@@ -1,5 +1,6 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils'; import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response'; import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler((event) => { export default eventHandler((event) => {
const userinfo = verifyAccessToken(event); const userinfo = verifyAccessToken(event);

View File

@@ -1,3 +1,4 @@
import { defineEventHandler } from 'h3';
import { forbiddenResponse, sleep } from '~/utils/response'; import { forbiddenResponse, sleep } from '~/utils/response';
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {

View File

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

View File

@@ -1,6 +1,6 @@
{ {
"name": "@vben/backend-mock", "name": "@vben/backend-mock",
"version": "0.0.1", "version": "5.7.0",
"description": "", "description": "",
"private": true, "private": true,
"license": "MIT", "license": "MIT",

View File

@@ -1,13 +1,15 @@
import { defineEventHandler } from 'h3';
export default defineEventHandler(() => { export default defineEventHandler(() => {
return ` return `
<h1>Hello Vben Admin</h1> <h1>Hello Vben Admin</h1>
<h2>Mock service is starting</h2> <h2>Mock service is running</h2>
<ul> <ul>
<li><a href="/api/user">/api/user/info</a></li>
<li><a href="/api/menu">/api/menu/all</a></li>
<li><a href="/api/auth/codes">/api/auth/codes</a></li>
<li><a href="/api/auth/login">/api/auth/login</a></li> <li><a href="/api/auth/login">/api/auth/login</a></li>
<li><a href="/api/upload">/api/upload</a></li> <li><a href="/api/auth/codes">/api/auth/codes</a></li>
<li><a href="/api/user/info">/api/user/info</a></li>
<li><a href="/api/menu/all">/api/menu/all</a></li>
<li><a href="/api/auth/logout">/api/auth/logout</a></li>
</ul> </ul>
`; `;
}); });

View File

@@ -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"]
} }

View File

@@ -1,5 +1,7 @@
import type { EventHandlerRequest, H3Event } from 'h3'; import type { EventHandlerRequest, H3Event } from 'h3';
import { deleteCookie, getCookie, setCookie } from 'h3';
export function clearRefreshTokenCookie(event: H3Event<EventHandlerRequest>) { export function clearRefreshTokenCookie(event: H3Event<EventHandlerRequest>) {
deleteCookie(event, 'jwt', { deleteCookie(event, 'jwt', {
httpOnly: true, httpOnly: true,

View File

@@ -1,8 +1,11 @@
import type { EventHandlerRequest, H3Event } from 'h3'; import type { EventHandlerRequest, H3Event } from 'h3';
import type { UserInfo } from './mock-data';
import { getHeader } from 'h3';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { UserInfo } from './mock-data'; import { MOCK_USERS } from './mock-data';
// TODO: Replace with your own secret key // TODO: Replace with your own secret key
const ACCESS_TOKEN_SECRET = 'access_token_secret'; const ACCESS_TOKEN_SECRET = 'access_token_secret';
@@ -31,12 +34,22 @@ export function verifyAccessToken(
return null; return null;
} }
const token = authHeader.split(' ')[1]; const tokenParts = authHeader.split(' ');
if (tokenParts.length !== 2) {
return null;
}
const token = tokenParts[1] as string;
try { 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 username = decoded.username;
const user = MOCK_USERS.find((item) => item.username === username); const user = MOCK_USERS.find((item) => item.username === username);
if (!user) {
return null;
}
const { password: _pwd, ...userinfo } = user; const { password: _pwd, ...userinfo } = user;
return userinfo; return userinfo;
} catch { } catch {
@@ -50,7 +63,12 @@ export function verifyRefreshToken(
try { try {
const decoded = jwt.verify(token, REFRESH_TOKEN_SECRET) as UserPayload; const decoded = jwt.verify(token, REFRESH_TOKEN_SECRET) as UserPayload;
const username = decoded.username; 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; const { password: _pwd, ...userinfo } = user;
return userinfo; return userinfo;
} catch { } catch {

View File

@@ -7,6 +7,11 @@ export interface UserInfo {
homePath?: string; homePath?: string;
} }
export interface TimezoneOption {
offset: number;
timezone: string;
}
export const MOCK_USERS: UserInfo[] = [ export const MOCK_USERS: UserInfo[] = [
{ {
id: 0, id: 0,
@@ -14,6 +19,7 @@ export const MOCK_USERS: UserInfo[] = [
realName: 'Vben', realName: 'Vben',
roles: ['super'], roles: ['super'],
username: 'vben', username: 'vben',
homePath: '/dashboard/workspace',
}, },
{ {
id: 1, id: 1,
@@ -21,7 +27,7 @@ export const MOCK_USERS: UserInfo[] = [
realName: 'Admin', realName: 'Admin',
roles: ['admin'], roles: ['admin'],
username: 'admin', username: 'admin',
homePath: '/workspace', homePath: '/dashboard/workspace',
}, },
{ {
id: 2, id: 2,
@@ -29,7 +35,7 @@ export const MOCK_USERS: UserInfo[] = [
realName: 'Jack', realName: 'Jack',
roles: ['user'], roles: ['user'],
username: 'jack', username: 'jack',
homePath: '/analytics', homePath: '/dashboard/analytics',
}, },
]; ];
@@ -59,11 +65,11 @@ const dashboardMenus = [
}, },
name: 'Dashboard', name: 'Dashboard',
path: '/dashboard', path: '/dashboard',
redirect: '/analytics', redirect: '/dashboard/analytics',
children: [ children: [
{ {
name: 'Analytics', name: 'Analytics',
path: '/analytics', path: 'analytics',
component: '/dashboard/analytics/index', component: '/dashboard/analytics/index',
meta: { meta: {
affixTab: true, affixTab: true,
@@ -72,7 +78,7 @@ const dashboardMenus = [
}, },
{ {
name: 'Workspace', name: 'Workspace',
path: '/workspace', path: 'workspace',
component: '/dashboard/workspace/index', component: '/dashboard/workspace/index',
meta: { meta: {
title: 'page.dashboard.workspace', title: 'page.dashboard.workspace',
@@ -91,7 +97,7 @@ const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
title: 'demos.access.adminVisible', title: 'demos.access.adminVisible',
}, },
name: 'AccessAdminVisibleDemo', name: 'AccessAdminVisibleDemo',
path: '/demos/access/admin-visible', path: 'admin-visible',
}, },
super: { super: {
component: '/demos/access/super-visible', component: '/demos/access/super-visible',
@@ -100,7 +106,7 @@ const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
title: 'demos.access.superVisible', title: 'demos.access.superVisible',
}, },
name: 'AccessSuperVisibleDemo', name: 'AccessSuperVisibleDemo',
path: '/demos/access/super-visible', path: 'super-visible',
}, },
user: { user: {
component: '/demos/access/user-visible', component: '/demos/access/user-visible',
@@ -109,7 +115,7 @@ const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
title: 'demos.access.userVisible', title: 'demos.access.userVisible',
}, },
name: 'AccessUserVisibleDemo', name: 'AccessUserVisibleDemo',
path: '/demos/access/user-visible', path: 'user-visible',
}, },
}; };
@@ -127,7 +133,7 @@ const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
children: [ children: [
{ {
name: 'AccessDemos', name: 'AccessDemos',
path: '/demosaccess', path: 'access',
meta: { meta: {
icon: 'mdi:cloud-key-outline', icon: 'mdi:cloud-key-outline',
title: 'demos.access.backendPermissions', title: 'demos.access.backendPermissions',
@@ -136,7 +142,7 @@ const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
children: [ children: [
{ {
name: 'AccessPageControlDemo', name: 'AccessPageControlDemo',
path: '/demos/access/page-control', path: 'page-control',
component: '/demos/access/index', component: '/demos/access/index',
meta: { meta: {
icon: 'mdi:page-previous-outline', icon: 'mdi:page-previous-outline',
@@ -145,7 +151,7 @@ const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
}, },
{ {
name: 'AccessButtonControlDemo', name: 'AccessButtonControlDemo',
path: '/demos/access/button-control', path: 'button-control',
component: '/demos/access/button-control', component: '/demos/access/button-control',
meta: { meta: {
icon: 'mdi:button-cursor', icon: 'mdi:button-cursor',
@@ -154,7 +160,7 @@ const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
}, },
{ {
name: 'AccessMenuVisible403Demo', name: 'AccessMenuVisible403Demo',
path: '/demos/access/menu-visible-403', path: 'menu-visible-403',
component: '/demos/access/menu-visible-403', component: '/demos/access/menu-visible-403',
meta: { meta: {
authority: ['no-body'], authority: ['no-body'],
@@ -189,18 +195,46 @@ export const MOCK_MENUS = [
export const MOCK_MENU_LIST = [ export const MOCK_MENU_LIST = [
{ {
id: 1, id: 1,
name: 'Workspace', name: 'Dashboard',
status: 1, status: 1,
type: 'menu', type: 'catalog',
icon: 'mdi:dashboard', icon: 'lucide:layout-dashboard',
path: '/workspace', path: '/dashboard',
component: '/dashboard/workspace/index',
meta: { meta: {
icon: 'carbon:workspace', icon: 'lucide:layout-dashboard',
title: 'page.dashboard.workspace', order: -1,
affixTab: true, title: 'page.dashboard.title',
order: 0,
}, },
children: [
{
id: 101,
pid: 1,
status: 1,
type: 'menu',
name: 'Analytics',
path: 'analytics',
component: '/dashboard/analytics/index',
meta: {
affixTab: true,
icon: 'lucide:area-chart',
title: 'page.dashboard.analytics',
keepAlive: true,
},
},
{
id: 102,
pid: 1,
status: 1,
type: 'menu',
name: 'Workspace',
path: 'workspace',
component: '/views/dashboard/workspace/index',
meta: {
icon: 'carbon:workspace',
title: 'page.dashboard.workspace',
},
},
],
}, },
{ {
id: 2, id: 2,
@@ -276,7 +310,7 @@ export const MOCK_MENU_LIST = [
children: [ children: [
{ {
id: 20_401, id: 20_401,
pid: 201, pid: 202,
name: 'SystemDeptCreate', name: 'SystemDeptCreate',
status: 1, status: 1,
type: 'button', type: 'button',
@@ -285,7 +319,7 @@ export const MOCK_MENU_LIST = [
}, },
{ {
id: 20_402, id: 20_402,
pid: 201, pid: 202,
name: 'SystemDeptEdit', name: 'SystemDeptEdit',
status: 1, status: 1,
type: 'button', type: 'button',
@@ -294,7 +328,7 @@ export const MOCK_MENU_LIST = [
}, },
{ {
id: 20_403, id: 20_403,
pid: 201, pid: 202,
name: 'SystemDeptDelete', name: 'SystemDeptDelete',
status: 1, status: 1,
type: 'button', type: 'button',
@@ -388,3 +422,29 @@ export function getMenuIds(menus: any[]) {
}); });
return ids; 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',
},
];

View File

@@ -1,5 +1,7 @@
import type { EventHandlerRequest, H3Event } from 'h3'; import type { EventHandlerRequest, H3Event } from 'h3';
import { setResponseStatus } from 'h3';
export function useResponseSuccess<T = any>(data: T) { export function useResponseSuccess<T = any>(data: T) {
return { return {
code: 0, code: 0,

View File

@@ -0,0 +1,9 @@
let mockTimeZone: null | string = null;
export const setTimezone = (timeZone: string) => {
mockTimeZone = timeZone;
};
export const getTimezone = () => {
return mockTimeZone;
};

View File

@@ -12,7 +12,7 @@
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
/> />
<!-- 由 vite 注入 VITE_APP_TITLE 变量,在 .env 文件内配置 --> <!-- 由 vite 注入 VITE_APP_TITLE 变量,在 .env 文件内配置 -->
<title><%= VITE_APP_TITLE %></title> <title>%VITE_APP_TITLE%</title>
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<script> <script>
// 生产环境下注入百度统计 // 生产环境下注入百度统计

View File

@@ -1,6 +1,6 @@
{ {
"name": "@vben/web-antd", "name": "@vben/web-antd",
"version": "5.5.6", "version": "5.7.0",
"homepage": "https://vben.pro", "homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues", "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": { "repository": {

View File

@@ -1 +0,0 @@
export { default } from '@vben/tailwind-config/postcss';

View File

@@ -3,23 +3,80 @@
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用, * 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
*/ */
import type { Component } from 'vue'; /* eslint-disable vue/one-component-per-file */
import type { BaseFormComponentType } from '@vben/common-ui'; import type {
AutoCompleteProps,
ButtonProps,
CascaderProps,
CheckboxGroupProps,
CheckboxProps,
DatePickerProps,
DividerProps,
InputNumberProps,
InputProps,
MentionsProps,
RadioGroupProps,
RadioProps,
RateProps,
SelectProps,
SpaceProps,
SwitchProps,
TextAreaProps,
TimePickerProps,
TreeSelectProps,
UploadChangeParam,
UploadFile,
UploadProps,
} from 'ant-design-vue';
import type { RangePickerProps } from 'ant-design-vue/es/date-picker';
import type { Component, Ref } from 'vue';
import type {
ApiComponentSharedProps,
BaseFormComponentType,
IconPickerProps,
} from '@vben/common-ui';
import type { Sortable } from '@vben/hooks';
import type { Recordable } from '@vben/types'; import type { Recordable } from '@vben/types';
import { import {
computed,
defineAsyncComponent, defineAsyncComponent,
defineComponent, defineComponent,
getCurrentInstance,
h, h,
nextTick,
onMounted,
onUnmounted,
ref, ref,
render,
unref,
watch,
} from 'vue'; } from 'vue';
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui'; import {
ApiComponent,
globalShareState,
IconPicker,
VCropper,
} from '@vben/common-ui';
import { useSortable } from '@vben/hooks';
import { IconifyIcon } from '@vben/icons';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { isEmpty } from '@vben/utils';
import { notification } from 'ant-design-vue'; import { message, Modal, notification } from 'ant-design-vue';
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;
};
import { registerComponent } from '#/components/form/component-map'; import { registerComponent } from '#/components/form/component-map';
@@ -67,10 +124,17 @@ const TimePicker = defineAsyncComponent(
const TreeSelect = defineAsyncComponent( const TreeSelect = defineAsyncComponent(
() => import('ant-design-vue/es/tree-select'), () => import('ant-design-vue/es/tree-select'),
); );
const Cascader = defineAsyncComponent(
() => import('ant-design-vue/es/cascader'),
);
const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload')); const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));
const Image = defineAsyncComponent(() => import('ant-design-vue/es/image'));
const PreviewGroup = defineAsyncComponent(() =>
import('ant-design-vue/es/image').then((res) => res.ImagePreviewGroup),
);
const withDefaultPlaceholder = <T extends Component>( const withDefaultPlaceholder = (
component: T, component: Component,
type: 'input' | 'select', type: 'input' | 'select',
componentProps: Recordable<any> = {}, componentProps: Recordable<any> = {},
) => { ) => {
@@ -84,16 +148,15 @@ const withDefaultPlaceholder = <T extends Component>(
$t(`ui.placeholder.${type}`); $t(`ui.placeholder.${type}`);
// 透传组件暴露的方法 // 透传组件暴露的方法
const innerRef = ref(); const innerRef = ref();
const publicApi: Recordable<any> = {}; expose(
expose(publicApi); new Proxy(
const instance = getCurrentInstance(); {},
instance?.proxy?.$nextTick(() => { {
for (const key in innerRef.value) { get: (_target, key) => innerRef.value?.[key],
if (typeof innerRef.value[key] === 'function') { has: (_target, key) => key in (innerRef.value || {}),
publicApi[key] = innerRef.value[key]; },
} ),
} );
});
return () => return () =>
h( h(
component, component,
@@ -104,12 +167,445 @@ const withDefaultPlaceholder = <T extends Component>(
}); });
}; };
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<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.addEventListener('load', () => resolve(reader.result as string));
reader.addEventListener('error', reject);
});
}
/**
* 预览图片
*/
async function previewImage(
file: UploadFile,
visible: Ref<boolean>,
fileList: Ref<UploadProps['fileList']>,
) {
// 非图片文件直接打开链接
if (!isImageFile(file)) {
const url = file.url || file.preview;
if (url) {
window.open(url, '_blank');
} else {
message.error($t('ui.formRules.previewWarning'));
}
return;
}
const [ImageComponent, PreviewGroupComponent] = await Promise.all([
Image,
PreviewGroup,
]);
// 过滤图片文件并生成预览
const imageFiles = (unref(fileList) || []).filter((f) => isImageFile(f));
for (const imgFile of imageFiles) {
if (!imgFile.url && !imgFile.preview && imgFile.originFileObj) {
imgFile.preview = await getBase64(imgFile.originFileObj);
}
}
const container = document.createElement('div');
document.body.append(container);
let isUnmounted = false;
const currentIndex = imageFiles.findIndex((f) => f.uid === file.uid);
const PreviewWrapper = {
setup() {
return () => {
if (isUnmounted) return null;
return h(
PreviewGroupComponent,
{
class: 'hidden',
preview: {
visible: visible.value,
current: currentIndex,
onVisibleChange: (value: boolean) => {
visible.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<Blob | string | undefined>((resolve, reject) => {
const container = document.createElement('div');
document.body.append(container);
let isUnmounted = false;
let objectUrl: null | string = null;
const open = ref<boolean>(true);
const cropperRef = ref<InstanceType<typeof VCropper> | null>(null);
const 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'),
destroyOnClose: 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);
});
}
/**
* 带预览功能的上传组件
*/
const withPreviewUpload = () => {
return defineComponent({
name: Upload.name,
emits: ['update:modelValue'],
setup(
props: any,
{ attrs, slots, emit }: { attrs: any; emit: any; slots: any },
) {
const previewVisible = ref<boolean>(false);
const placeholder = attrs?.placeholder || $t('ui.placeholder.upload');
const listType = attrs?.listType || attrs?.['list-type'] || 'text';
const fileList = ref<UploadProps['fileList']>(
attrs?.fileList || attrs?.['file-list'] || [],
);
const maxSize = computed(() => attrs?.maxSize ?? attrs?.['max-size']);
const aspectRatio = computed(
() => attrs?.aspectRatio ?? attrs?.['aspect-ratio'],
);
const handleBeforeUpload = async (
file: UploadFile,
originFileList: Array<File>,
) => {
// 文件大小限制
if (maxSize.value && (file.size || 0) / 1024 / 1024 > maxSize.value) {
message.error($t('ui.formRules.sizeLimit', [maxSize.value]));
file.status = 'removed';
return false;
}
// 图片裁剪处理
if (
attrs.crop &&
!attrs.multiple &&
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;
};
const 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,
);
};
const handlePreview = async (file: UploadFile) => {
previewVisible.value = true;
await previewImage(file, previewVisible, fileList);
};
const 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 | Sortable>(null);
const styleId = `upload-drag-style-${uploadId}`;
function injectDragStyle() {
if (!document.querySelector(`[id="${styleId}"]`)) {
const style = document.createElement('style');
style.id = styleId;
style.textContent = `
[data-upload-id="${uploadId}"] .ant-upload-list-item { cursor: move; }
[data-upload-id="${uploadId}"] .ant-upload-list-item:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.15); }
`;
document.head.append(style);
}
}
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 = export type ComponentType =
| 'ApiCascader'
| 'ApiSelect' | 'ApiSelect'
| 'ApiTreeSelect' | 'ApiTreeSelect'
| 'AutoComplete' | 'AutoComplete'
| 'Avatar' | 'Avatar'
| 'Cascader'
| 'Checkbox' | 'Checkbox'
| 'CheckboxGroup' | 'CheckboxGroup'
| 'DatePicker' | 'DatePicker'
@@ -134,40 +630,68 @@ export type ComponentType =
| 'Upload' | 'Upload'
| BaseFormComponentType; | 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;
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;
Select: SelectProps;
Space: SpaceProps;
Switch: SwitchProps;
Textarea: TextAreaProps;
TimePicker: TimePickerProps;
TreeSelect: TreeSelectProps;
Upload: AdapterUploadProps;
}
async function initComponentAdapter() { async function initComponentAdapter() {
const components: Partial<Record<ComponentType, Component>> = { const components: Partial<Record<ComponentType, Component>> = {
// 如果你的组件体积比较大,可以使用异步加载 // 如果你的组件体积比较大,可以使用异步加载
// Button: () => // Button: () =>
// import('xxx').then((res) => res.Button), // import('xxx').then((res) => res.Button),
ApiSelect: withDefaultPlaceholder(
{ ApiCascader: withDefaultPlaceholder(ApiComponent, 'select', {
...ApiComponent, component: Cascader,
name: 'ApiSelect', fieldNames: { label: 'label', value: 'value', children: 'children' },
}, loadingSlot: 'suffixIcon',
'select', modelPropName: 'value',
{ visibleEvent: 'onVisibleChange',
component: Select, }),
loadingSlot: 'suffixIcon', ApiSelect: withDefaultPlaceholder(ApiComponent, 'select', {
visibleEvent: 'onDropdownVisibleChange', component: Select,
modelPropName: 'value', loadingSlot: 'suffixIcon',
}, modelPropName: 'value',
), visibleEvent: 'onVisibleChange',
ApiTreeSelect: withDefaultPlaceholder( }),
{ ApiTreeSelect: withDefaultPlaceholder(ApiComponent, 'select', {
...ApiComponent, component: TreeSelect,
name: 'ApiTreeSelect', fieldNames: { label: 'label', value: 'value', children: 'children' },
}, loadingSlot: 'suffixIcon',
'select', modelPropName: 'value',
{ optionsPropName: 'treeData',
component: TreeSelect, visibleEvent: 'onVisibleChange',
fieldNames: { label: 'label', value: 'value', children: 'children' }, }),
loadingSlot: 'suffixIcon',
modelPropName: 'value',
optionsPropName: 'treeData',
visibleEvent: 'onVisibleChange',
},
),
AutoComplete, AutoComplete,
Cascader,
Checkbox, Checkbox,
CheckboxGroup, CheckboxGroup,
DatePicker, DatePicker,
@@ -182,7 +706,9 @@ async function initComponentAdapter() {
modelValueProp: 'value', modelValueProp: 'value',
}), }),
Input: withDefaultPlaceholder(Input, 'input'), Input: withDefaultPlaceholder(Input, 'input'),
InputNumber: withDefaultPlaceholder(InputNumber, 'input'), InputNumber: withDefaultPlaceholder(InputNumber, 'input', {
style: { width: '100%' },
}),
InputPassword: withDefaultPlaceholder(InputPassword, 'input'), InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
Mentions: withDefaultPlaceholder(Mentions, 'input'), Mentions: withDefaultPlaceholder(Mentions, 'input'),
// 自定义主要按钮 // 自定义主要按钮
@@ -199,7 +725,7 @@ async function initComponentAdapter() {
Textarea: withDefaultPlaceholder(Textarea, 'input'), Textarea: withDefaultPlaceholder(Textarea, 'input'),
TimePicker, TimePicker,
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'), TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
Upload, Upload: withPreviewUpload(),
}; };
// 自动注册自定义组件 // 自动注册自定义组件

View File

@@ -1,47 +1,68 @@
import type { import type {
VbenFormProps as FormProps,
VbenFormSchema as FormSchema, VbenFormSchema as FormSchema,
VbenFormProps, FormValues,
} from '@vben/common-ui'; } from '@vben/common-ui';
import type { ComponentType } from './component'; import type { ComponentPropsMap, ComponentType } from './component';
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui'; import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
setupVbenForm<ComponentType>({ async function initSetupVbenForm() {
config: { setupVbenForm<ComponentType>({
// ant design vue组件库默认都是 v-model:value config: {
baseModelPropName: 'value', // ant design vue组件库默认都是 v-model:value
baseModelPropName: 'value',
// 一些组件是 v-model:checked 或者 v-model:fileList // 一些组件是 v-model:checked 或者 v-model:fileList
modelPropNameMap: { modelPropNameMap: {
Checkbox: 'checked', Checkbox: 'checked',
Radio: 'checked', Radio: 'checked',
Switch: 'checked', Switch: 'checked',
Upload: 'fileList', Upload: 'fileList',
},
}, },
}, rules: {
defineRules: { // 输入项目必填国际化适配
// 输入项目必填国际化适配 required: (value, _params, ctx) => {
required: (value, _params, ctx) => { if (value === undefined || value === null || value.length === 0) {
if (value === undefined || value === null || value.length === 0) { return $t('ui.formRules.required', [ctx.label]);
return $t('ui.formRules.required', [ctx.label]); }
} return true;
return true; },
// 选择项目必填国际化适配
selectRequired: (value, _params, ctx) => {
if (value === undefined || value === null) {
return $t('ui.formRules.selectRequired', [ctx.label]);
}
return true;
},
}, },
// 选择项目必填国际化适配 });
selectRequired: (value, _params, ctx) => { }
if (value === undefined || value === null) {
return $t('ui.formRules.selectRequired', [ctx.label]);
}
return true;
},
},
});
const useVbenForm = useForm<ComponentType>; function useVbenForm<
TFormValues extends FormValues = FormValues,
TSubmitValues extends FormValues = TFormValues,
>(
options: FormProps<
ComponentType,
ComponentPropsMap,
TFormValues,
TSubmitValues
>,
) {
return useForm<TFormValues, ComponentType, ComponentPropsMap, TSubmitValues>(
options,
);
}
export { useVbenForm, z }; export { initSetupVbenForm, useVbenForm, z };
export type VbenFormSchema = FormSchema<ComponentType>; export type VbenFormSchema<TValues extends FormValues = FormValues> =
export type { VbenFormProps }; FormSchema<ComponentType, ComponentPropsMap, TValues>;
export type VbenFormProps<
TFormValues extends FormValues = FormValues,
TSubmitValues extends FormValues = TFormValues,
> = FormProps<ComponentType, ComponentPropsMap, TFormValues, TSubmitValues>;

View File

@@ -1,6 +1,14 @@
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 { h } from 'vue';
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table'; import {
setupVbenVxeTable,
useVbenVxeGrid as useGrid,
} from '@vben/plugins/vxe-table';
import { Button, Image } from 'ant-design-vue'; import { Button, Image } from 'ant-design-vue';
@@ -33,14 +41,15 @@ setupVbenVxeTable({
round: true, round: true,
showOverflow: true, showOverflow: true,
size: 'small', size: 'small',
}, } as VxeTableGridOptions,
}); });
// 表格配置项可以用 cellRender: { name: 'CellImage' }, // 表格配置项可以用 cellRender: { name: 'CellImage' },
vxeUI.renderer.add('CellImage', { vxeUI.renderer.add('CellImage', {
renderTableDefault(_renderOpts, params) { renderTableDefault(renderOpts, params) {
const { props } = renderOpts;
const { column, row } = params; const { column, row } = params;
return h(Image, { src: row[column.field] }); return h(Image, { src: row[column.field], ...props });
}, },
}); });
@@ -62,6 +71,23 @@ setupVbenVxeTable({
useVbenForm, useVbenForm,
}); });
export { useVbenVxeGrid }; export const useVbenVxeGrid = <
T extends Record<string, any>,
TFormValues extends FormValues = FormValues,
TSubmitValues extends FormValues = TFormValues,
>(
...rest: Parameters<
typeof useGrid<
T,
ComponentType,
ComponentPropsMap,
TFormValues,
TSubmitValues
>
>
) =>
useGrid<T, ComponentType, ComponentPropsMap, TFormValues, TSubmitValues>(
...rest,
);
export type * from '@vben/plugins/vxe-table'; export type * from '@vben/plugins/vxe-table';

View File

@@ -12,6 +12,7 @@ import { useTitle } from '@vueuse/core';
import { $t, setupI18n } from '#/locales'; import { $t, setupI18n } from '#/locales';
import { initComponentAdapter } from './adapter/component'; import { initComponentAdapter } from './adapter/component';
import { initSetupVbenForm } from './adapter/form';
import App from './app.vue'; import App from './app.vue';
import { router } from './router'; import { router } from './router';
@@ -19,6 +20,9 @@ async function bootstrap(namespace: string) {
// 初始化组件适配器 // 初始化组件适配器
await initComponentAdapter(); await initComponentAdapter();
// 初始化表单组件
await initSetupVbenForm();
// // 设置弹窗的默认配置 // // 设置弹窗的默认配置
// setDefaultModalProps({ // setDefaultModalProps({
// fullscreenButton: false, // fullscreenButton: false,

View File

@@ -8,12 +8,14 @@ import { $t } from '#/locales';
const appName = computed(() => preferences.app.name); const appName = computed(() => preferences.app.name);
const logo = computed(() => preferences.logo.source); const logo = computed(() => preferences.logo.source);
const logoDark = computed(() => preferences.logo.sourceDark);
</script> </script>
<template> <template>
<AuthPageLayout <AuthPageLayout
:app-name="appName" :app-name="appName"
:logo="logo" :logo="logo"
:logo-dark="logoDark"
:page-description="$t('authentication.pageDesc')" :page-description="$t('authentication.pageDesc')"
:page-title="$t('authentication.pageTitle')" :page-title="$t('authentication.pageTitle')"
> >

View File

@@ -2,18 +2,19 @@
import type { NotificationItem } from '@vben/layouts'; import type { NotificationItem } from '@vben/layouts';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { AuthenticationLoginExpiredModal } from '@vben/common-ui'; import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants'; import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
import { useWatermark } from '@vben/hooks'; import { useWatermark } from '@vben/hooks';
import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons'; import { BookOpenText, CircleHelp, SvgGithubIcon } from '@vben/icons';
import { import {
BasicLayout, BasicLayout,
LockScreen, LockScreen,
Notification, Notification,
UserDropdown, UserDropdown,
} from '@vben/layouts'; } from '@vben/layouts';
import { preferences } from '@vben/preferences'; import { preferences, usePreferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores'; import { useAccessStore, useUserStore } from '@vben/stores';
import { openWindow } from '@vben/utils'; import { openWindow } from '@vben/utils';
@@ -23,6 +24,7 @@ import LoginForm from '#/views/_core/authentication/login.vue';
const notifications = ref<NotificationItem[]>([ const notifications = ref<NotificationItem[]>([
{ {
id: 1,
avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB', avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
date: '3小时前', date: '3小时前',
isRead: true, isRead: true,
@@ -30,6 +32,7 @@ const notifications = ref<NotificationItem[]>([
title: '收到了 14 份新周报', title: '收到了 14 份新周报',
}, },
{ {
id: 2,
avatar: 'https://avatar.vercel.sh/1', avatar: 'https://avatar.vercel.sh/1',
date: '刚刚', date: '刚刚',
isRead: false, isRead: false,
@@ -37,6 +40,7 @@ const notifications = ref<NotificationItem[]>([
title: '朱偏右 回复了你', title: '朱偏右 回复了你',
}, },
{ {
id: 3,
avatar: 'https://avatar.vercel.sh/1', avatar: 'https://avatar.vercel.sh/1',
date: '2024-01-01', date: '2024-01-01',
isRead: false, isRead: false,
@@ -44,23 +48,51 @@ const notifications = ref<NotificationItem[]>([
title: '曲丽丽 评论了你', title: '曲丽丽 评论了你',
}, },
{ {
id: 4,
avatar: 'https://avatar.vercel.sh/satori', avatar: 'https://avatar.vercel.sh/satori',
date: '1天前', date: '1天前',
isRead: false, isRead: false,
message: '描述信息描述信息描述信息', message: '描述信息描述信息描述信息',
title: '代办提醒', 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 userStore = useUserStore();
const authStore = useAuthStore(); const authStore = useAuthStore();
const accessStore = useAccessStore(); const accessStore = useAccessStore();
const { destroyWatermark, updateWatermark } = useWatermark(); const { destroyWatermark, updateWatermark } = useWatermark();
const { isDark } = usePreferences();
const showDot = computed(() => const showDot = computed(() =>
notifications.value.some((item) => !item.isRead), notifications.value.some((item) => !item.isRead),
); );
const menus = computed(() => [ const menus = computed(() => [
{
handler: () => {
router.push({ name: 'Profile' });
},
icon: 'lucide:user',
text: $t('page.auth.profile'),
},
{ {
handler: () => { handler: () => {
openWindow(VBEN_DOC_URL, { openWindow(VBEN_DOC_URL, {
@@ -76,7 +108,7 @@ const menus = computed(() => [
target: '_blank', target: '_blank',
}); });
}, },
icon: MdiGithub, icon: SvgGithubIcon,
text: 'GitHub', text: 'GitHub',
}, },
{ {
@@ -102,15 +134,77 @@ function handleNoticeClear() {
notifications.value = []; 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() { function handleMakeAll() {
notifications.value.forEach((item) => (item.isRead = true)); 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<string, any>,
state?: Record<string, any>,
) {
if (link.startsWith('http://') || link.startsWith('https://')) {
// 外部链接,在新标签页打开
window.open(link, '_blank');
} else {
// 内部路由链接,支持 query 参数和 state
router.push({
path: link,
query: query || {},
state,
});
}
}
watch( watch(
() => preferences.app.watermark, () => ({
async (enable) => { enable: preferences.app.watermark,
content: preferences.app.watermarkContent,
isDark: isDark.value,
}),
async ({ enable, content, isDark: isDarkValue }) => {
if (enable) { if (enable) {
const watermarkColor = isDarkValue
? 'rgba(255, 255, 255, 0.12)'
: 'rgba(0, 0, 0, 0.12)';
await updateWatermark({ 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 { } else {
destroyWatermark(); destroyWatermark();
@@ -123,7 +217,12 @@ watch(
</script> </script>
<template> <template>
<BasicLayout @clear-preferences-and-logout="handleLogout"> <BasicLayout
:avatar
:text="userStore.userInfo?.realName"
@clear-preferences-and-logout="handleLogout"
@logout="handleLogout"
>
<template #user-dropdown> <template #user-dropdown>
<UserDropdown <UserDropdown
:avatar :avatar
@@ -139,7 +238,11 @@ watch(
:dot="showDot" :dot="showDot"
:notifications="notifications" :notifications="notifications"
@clear="handleNoticeClear" @clear="handleNoticeClear"
@read="(item) => item.id && markRead(item.id)"
@remove="(item) => item.id && remove(item.id)"
@make-all="handleMakeAll" @make-all="handleMakeAll"
@on-click="handleClick"
@view-all="viewAll"
/> />
</template> </template>
<template #extra> <template #extra>

View File

@@ -6,7 +6,9 @@
"about": "About", "about": "About",
"document": "Document", "document": "Document",
"antdv": "Ant Design Vue Version", "antdv": "Ant Design Vue Version",
"antdv-next": "Antdv Next Version",
"naive-ui": "Naive UI Version", "naive-ui": "Naive UI Version",
"element-plus": "Element Plus Version" "element-plus": "Element Plus Version",
"tdesign": "TDesign Vue Version"
} }
} }

View File

@@ -4,7 +4,8 @@
"register": "Register", "register": "Register",
"codeLogin": "Code Login", "codeLogin": "Code Login",
"qrcodeLogin": "Qr Code Login", "qrcodeLogin": "Qr Code Login",
"forgetPassword": "Forget Password" "forgetPassword": "Forget Password",
"profile": "Profile"
}, },
"dashboard": { "dashboard": {
"title": "Dashboard", "title": "Dashboard",

View File

@@ -6,7 +6,9 @@
"about": "关于", "about": "关于",
"document": "文档", "document": "文档",
"antdv": "Ant Design Vue 版本", "antdv": "Ant Design Vue 版本",
"antdv-next": "Antdv Next 版本",
"naive-ui": "Naive UI 版本", "naive-ui": "Naive UI 版本",
"element-plus": "Element Plus 版本" "element-plus": "Element Plus 版本",
"tdesign": "TDesign Vue 版本"
} }
} }

View File

@@ -4,7 +4,8 @@
"register": "注册", "register": "注册",
"codeLogin": "验证码登录", "codeLogin": "验证码登录",
"qrcodeLogin": "二维码登录", "qrcodeLogin": "二维码登录",
"forgetPassword": "忘记密码" "forgetPassword": "忘记密码",
"profile": "个人中心"
}, },
"dashboard": { "dashboard": {
"title": "概览", "title": "概览",

View File

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

View File

@@ -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 项目配置文件 * @description 项目配置文件
@@ -32,4 +43,54 @@ export const overridesPreferences = defineOverridesPreferences({
colorPrimary: 'hsl(245 82% 67%)', colorPrimary: 'hsl(245 82% 67%)',
mode: 'auto', mode: 'auto',
}, },
copyright: appCopyrightPreferences,
}); });
export const preferencesExtension =
definePreferencesExtension<WebAntdPreferencesExtension>({
tabLabel: 'preferences.antd.tabLabel',
title: 'preferences.antd.title',
fields: [
{
component: 'switch',
defaultValue: true,
key: 'enableFormFullscreen',
label: 'preferences.antd.fields.enableFormFullscreen.label',
tip: 'preferences.antd.fields.enableFormFullscreen.tip',
},
{
component: 'select',
defaultValue: 'single',
key: 'tenantMode',
label: 'preferences.antd.fields.tenantMode.label',
options: [
{
label: 'preferences.antd.fields.tenantMode.options.single.label',
value: 'single',
},
{
label: 'preferences.antd.fields.tenantMode.options.multi.label',
value: 'multi',
},
],
},
{
component: 'number',
componentProps: {
max: 200,
min: 10,
step: 10,
},
defaultValue: 20,
key: 'defaultTableSize',
label: 'preferences.antd.fields.defaultTableSize.label',
},
{
component: 'input',
defaultValue: '',
key: 'reportTitle',
label: 'preferences.antd.fields.reportTitle.label',
placeholder: 'preferences.antd.fields.reportTitle.placeholder',
},
],
});

View File

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

View File

@@ -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;

View File

@@ -1,12 +1,15 @@
import type { RouteRecordRaw } from 'vue-router'; import type { RouteRecordRaw } from 'vue-router';
import { import {
VBEN_ANTDV_NEXT_PREVIEW_URL,
VBEN_DOC_URL, VBEN_DOC_URL,
VBEN_ELE_PREVIEW_URL, VBEN_ELE_PREVIEW_URL,
VBEN_GITHUB_URL, VBEN_GITHUB_URL,
VBEN_LOGO_URL, VBEN_LOGO_URL,
VBEN_NAIVE_PREVIEW_URL, VBEN_NAIVE_PREVIEW_URL,
VBEN_TD_PREVIEW_URL,
} from '@vben/constants'; } from '@vben/constants';
import { SvgAntdvNextLogoIcon, SvgTDesignIcon } from '@vben/icons';
import { IFrameView } from '#/layouts'; import { IFrameView } from '#/layouts';
import { $t } from '#/locales'; import { $t } from '#/locales';
@@ -24,7 +27,7 @@ const routes: RouteRecordRaw[] = [
children: [ children: [
{ {
name: 'VbenDocument', name: 'VbenDocument',
path: '/vben-admin/document', path: 'document',
component: IFrameView, component: IFrameView,
meta: { meta: {
icon: 'lucide:book-open-text', icon: 'lucide:book-open-text',
@@ -34,7 +37,7 @@ const routes: RouteRecordRaw[] = [
}, },
{ {
name: 'VbenGithub', name: 'VbenGithub',
path: '/vben-admin/github', path: 'github',
component: IFrameView, component: IFrameView,
meta: { meta: {
icon: 'mdi:github', icon: 'mdi:github',
@@ -42,9 +45,20 @@ const routes: RouteRecordRaw[] = [
title: 'Github', 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', name: 'VbenNaive',
path: '/vben-admin/naive', path: 'naive',
component: IFrameView, component: IFrameView,
meta: { meta: {
badgeType: 'dot', badgeType: 'dot',
@@ -53,9 +67,20 @@ const routes: RouteRecordRaw[] = [
title: $t('demos.vben.naive-ui'), 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', name: 'VbenElementPlus',
path: '/vben-admin/ele', path: 'ele',
component: IFrameView, component: IFrameView,
meta: { meta: {
badgeType: 'dot', badgeType: 'dot',
@@ -76,6 +101,16 @@ const routes: RouteRecordRaw[] = [
order: 9999, 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; export default routes;

View File

@@ -98,8 +98,7 @@ export const useAuthStore = defineStore('auth', () => {
} }
async function fetchUserInfo() { async function fetchUserInfo() {
let userInfo: null | UserInfo = null; const userInfo = await getUserInfoApi();
userInfo = await getUserInfoApi();
userStore.setUserInfo(userInfo); userStore.setUserInfo(userInfo);
return userInfo; return userInfo;
} }

View File

@@ -55,8 +55,7 @@ const formSchema = computed((): VbenFormSchema[] => {
* @param values 登录表单数据 * @param values 登录表单数据
*/ */
async function handleLogin(values: Recordable<any>) { async function handleLogin(values: Recordable<any>) {
// eslint-disable-next-line no-console void values;
console.log(values);
} }
</script> </script>

View File

@@ -29,8 +29,7 @@ const formSchema = computed((): VbenFormSchema[] => {
}); });
function handleSubmit(value: Recordable<any>) { function handleSubmit(value: Recordable<any>) {
// eslint-disable-next-line no-console void value;
console.log('reset email:', value);
} }
</script> </script>

View File

@@ -46,7 +46,7 @@ const formSchema = computed((): VbenFormSchema[] => {
rules(values) { rules(values) {
const { password } = values; const { password } = values;
return z return z
.string({ required_error: $t('authentication.passwordTip') }) .string({ error: $t('authentication.passwordTip') })
.min(1, { message: $t('authentication.passwordTip') }) .min(1, { message: $t('authentication.passwordTip') })
.refine((value) => value === password, { .refine((value) => value === password, {
message: $t('authentication.confirmPasswordTip'), message: $t('authentication.confirmPasswordTip'),
@@ -82,8 +82,7 @@ const formSchema = computed((): VbenFormSchema[] => {
}); });
function handleSubmit(value: Recordable<any>) { function handleSubmit(value: Recordable<any>) {
// eslint-disable-next-line no-console void value;
console.log('register submit:', value);
} }
</script> </script>

View File

@@ -0,0 +1,65 @@
<script setup lang="ts">
import type { BasicOption } from '@vben/types';
import type { VbenFormSchema } from '#/adapter/form';
import { computed, onMounted, ref } from 'vue';
import { ProfileBaseSetting } from '@vben/common-ui';
import { getUserInfoApi } from '#/api';
const profileBaseSettingRef = ref();
const MOCK_ROLES_OPTIONS: BasicOption[] = [
{
label: '管理员',
value: 'super',
},
{
label: '用户',
value: 'user',
},
{
label: '测试',
value: 'test',
},
];
const formSchema = computed((): VbenFormSchema[] => {
return [
{
fieldName: 'realName',
component: 'Input',
label: '姓名',
},
{
fieldName: 'username',
component: 'Input',
label: '用户名',
},
{
fieldName: 'roles',
component: 'Select',
componentProps: {
mode: 'tags',
options: MOCK_ROLES_OPTIONS,
},
label: '角色',
},
{
fieldName: 'introduction',
component: 'Textarea',
label: '个人简介',
},
];
});
onMounted(async () => {
const data = await getUserInfoApi();
profileBaseSettingRef.value.getFormApi().setValues(data);
});
</script>
<template>
<ProfileBaseSetting ref="profileBaseSettingRef" :form-schema="formSchema" />
</template>

View File

@@ -0,0 +1,49 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Profile } from '@vben/common-ui';
import { useUserStore } from '@vben/stores';
import ProfileBase from './base-setting.vue';
import ProfileNotificationSetting from './notification-setting.vue';
import ProfilePasswordSetting from './password-setting.vue';
import ProfileSecuritySetting from './security-setting.vue';
const userStore = useUserStore();
const tabsValue = ref<string>('basic');
const tabs = ref([
{
label: '基本设置',
value: 'basic',
},
{
label: '安全设置',
value: 'security',
},
{
label: '修改密码',
value: 'password',
},
{
label: '新消息提醒',
value: 'notice',
},
]);
</script>
<template>
<Profile
v-model:model-value="tabsValue"
title="个人中心"
:user-info="userStore.userInfo"
:tabs="tabs"
>
<template #content>
<ProfileBase v-if="tabsValue === 'basic'" />
<ProfileSecuritySetting v-if="tabsValue === 'security'" />
<ProfilePasswordSetting v-if="tabsValue === 'password'" />
<ProfileNotificationSetting v-if="tabsValue === 'notice'" />
</template>
</Profile>
</template>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
import { computed } from 'vue';
import { ProfileNotificationSetting } from '@vben/common-ui';
const formSchema = computed(() => {
return [
{
value: true,
fieldName: 'accountPassword',
label: '账户密码',
description: '其他用户的消息将以站内信的形式通知',
},
{
value: true,
fieldName: 'systemMessage',
label: '系统消息',
description: '系统消息将以站内信的形式通知',
},
{
value: true,
fieldName: 'todoTask',
label: '待办任务',
description: '待办任务将以站内信的形式通知',
},
];
});
</script>
<template>
<ProfileNotificationSetting :form-schema="formSchema" />
</template>

View File

@@ -0,0 +1,63 @@
<script setup lang="ts">
import type { VbenFormSchema } from '#/adapter/form';
import { computed } from 'vue';
import { ProfilePasswordSetting, z } from '@vben/common-ui';
import { message } from 'ant-design-vue';
const formSchema = computed((): VbenFormSchema[] => {
return [
{
fieldName: 'oldPassword',
label: '旧密码',
component: 'VbenInputPassword',
componentProps: {
placeholder: '请输入旧密码',
},
},
{
fieldName: 'newPassword',
label: '新密码',
component: 'VbenInputPassword',
componentProps: {
passwordStrength: true,
placeholder: '请输入新密码',
},
},
{
fieldName: 'confirmPassword',
label: '确认密码',
component: 'VbenInputPassword',
componentProps: {
passwordStrength: true,
placeholder: '请再次输入新密码',
},
dependencies: {
rules(values) {
const { newPassword } = values;
return z
.string({ error: '请再次输入新密码' })
.min(1, { message: '请再次输入新密码' })
.refine((value) => value === newPassword, {
message: '两次输入的密码不一致',
});
},
triggerFields: ['newPassword'],
},
},
];
});
function handleSubmit() {
message.success('密码修改成功');
}
</script>
<template>
<ProfilePasswordSetting
class="w-1/3"
:form-schema="formSchema"
@submit="handleSubmit"
/>
</template>

View File

@@ -0,0 +1,43 @@
<script setup lang="ts">
import { computed } from 'vue';
import { ProfileSecuritySetting } from '@vben/common-ui';
const formSchema = computed(() => {
return [
{
value: true,
fieldName: 'accountPassword',
label: '账户密码',
description: '当前密码强度:强',
},
{
value: true,
fieldName: 'securityPhone',
label: '密保手机',
description: '已绑定手机:138****8293',
},
{
value: true,
fieldName: 'securityQuestion',
label: '密保问题',
description: '未设置密保问题,密保问题可有效保护账户安全',
},
{
value: true,
fieldName: 'securityEmail',
label: '备用邮箱',
description: '已绑定邮箱:ant***sign.com',
},
{
value: false,
fieldName: 'securityMfa',
label: 'MFA 设备',
description: '未绑定 MFA 设备,绑定后,可以进行二次确认',
},
];
});
</script>
<template>
<ProfileSecuritySetting :form-schema="formSchema" />
</template>

View File

@@ -24,7 +24,7 @@ onMounted(() => {
{ name: '定制', value: 310 }, { name: '定制', value: 310 },
{ name: '技术支持', value: 274 }, { name: '技术支持', value: 274 },
{ name: '远程', value: 400 }, { name: '远程', value: 400 },
].sort((a, b) => { ].toSorted((a, b) => {
return a.value - b.value; return a.value - b.value;
}), }),
name: '商业占比', name: '商业占比',

View File

@@ -76,10 +76,10 @@ const chartTabs: TabOption[] = [
</AnalysisChartsTabs> </AnalysisChartsTabs>
<div class="mt-5 w-full md:flex"> <div class="mt-5 w-full md:flex">
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问数量"> <AnalysisChartCard class="mt-5 md:mt-0 md:mr-4 md:w-1/3" title="访问数量">
<AnalyticsVisitsData /> <AnalyticsVisitsData />
</AnalysisChartCard> </AnalysisChartCard>
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问来源"> <AnalysisChartCard class="mt-5 md:mt-0 md:mr-4 md:w-1/3" title="访问来源">
<AnalyticsVisitsSource /> <AnalyticsVisitsSource />
</AnalysisChartCard> </AnalysisChartCard>
<AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="访问来源"> <AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="访问来源">

View File

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

View File

@@ -0,0 +1,68 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import { Button, Card, message, notification, Space } from 'ant-design-vue';
type NotificationType = 'error' | 'info' | 'success' | 'warning';
function info() {
message.info('How many roads must a man walk down');
}
function error() {
message.error({
content: 'Once upon a time you dressed so fine',
// ant-design-vue 的 message.duration 单位是「秒」, 不是毫秒
duration: 2.5,
});
}
function warning() {
message.warning('How many roads must a man walk down');
}
function success() {
message.success('Cause you walked hand in hand With another man in my place');
}
function notify(type: NotificationType) {
notification[type]({
// ant-design-vue 的 notification.duration 单位是「秒」, 不是毫秒
duration: 2.5,
message: '说点啥呢',
type,
});
}
</script>
<template>
<Page
description="支持多语言,主题功能集成切换等"
title="Ant Design Vue组件使用演示"
>
<Card class="mb-5" title="按钮">
<Space>
<Button>Default</Button>
<Button type="primary"> Primary </Button>
<Button> Info </Button>
<Button danger> Error </Button>
</Space>
</Card>
<Card class="mb-5" title="Message">
<Space>
<Button @click="info"> 信息 </Button>
<Button danger @click="error"> 错误 </Button>
<Button @click="warning"> 警告 </Button>
<Button @click="success"> 成功 </Button>
</Space>
</Card>
<Card class="mb-5" title="Notification">
<Space>
<Button @click="notify('info')"> 信息 </Button>
<Button danger @click="notify('error')"> 错误 </Button>
<Button @click="notify('warning')"> 警告 </Button>
<Button @click="notify('success')"> 成功 </Button>
</Space>
</Card>
</Page>
</template>

View File

@@ -1 +0,0 @@
export { default } from '@vben/tailwind-config';

View File

@@ -2,7 +2,6 @@
"$schema": "https://json.schemastore.org/tsconfig", "$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/web-app.json", "extends": "@vben/tsconfig/web-app.json",
"compilerOptions": { "compilerOptions": {
"baseUrl": ".",
"paths": { "paths": {
"#/*": ["./src/*"] "#/*": ["./src/*"]
} }

View File

@@ -6,5 +6,5 @@
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"noEmit": false "noEmit": false
}, },
"include": ["vite.config.mts"] "include": ["vite.config.ts"]
} }

8
apps/web-antdv-next/.env Normal file
View File

@@ -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

View File

@@ -0,0 +1,7 @@
# public path
VITE_BASE=/
# Basic interface address SPA
VITE_GLOB_API_URL=/api
VITE_VISUALIZER=true

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