Compare commits

..

No commits in common. "main" and "v5.1.0" have entirely different histories.
main ... v5.1.0

807 changed files with 8754 additions and 33732 deletions

View File

@ -3,5 +3,3 @@ node_modules
.gitignore .gitignore
*.md *.md
dist dist
.turbo
dist.zip

View File

@ -1,7 +1,7 @@
name: 🐞 Bug Report name: 🐞 Bug Report
description: Report an issue with Vben Admin to help us make it better. description: Report an issue with Vben Admin to help us make it better.
title: 'Bug: ' title: "Bug: "
labels: ['bug: pending triage'] labels: ["bug: pending triage"]
body: body:
- type: markdown - type: markdown

View File

@ -1,6 +1,6 @@
name: 📚 Documentation name: 📚 Documentation
description: Report an issue with Vben Admin Website to help us make it better. description: Report an issue with Vben Admin Website to help us make it better.
title: 'Docs: ' title: "Docs: "
labels: [documentation] labels: [documentation]
body: body:
- type: markdown - type: markdown

View File

@ -1,7 +1,7 @@
name: ✨ New Feature Proposal name: ✨ New Feature Proposal
description: Propose a new feature to be added to Vben Admin description: Propose a new feature to be added to Vben Admin
title: 'FEATURE: ' title: "FEATURE: "
labels: ['enhancement: pending triage'] labels: ["enhancement: pending triage"]
body: body:
- type: markdown - type: markdown
attributes: attributes:

View File

@ -1,9 +1,9 @@
name: 'Setup Node' name: "Setup Node"
description: 'Setup node and pnpm' description: "Setup node and pnpm"
runs: runs:
using: 'composite' using: "composite"
steps: steps:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
@ -12,7 +12,7 @@ runs:
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version-file: .node-version node-version-file: .node-version
cache: 'pnpm' cache: "pnpm"
- name: Get pnpm store directory - name: Get pnpm store directory
shell: bash shell: bash

View File

@ -1,7 +1,7 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: npm - package-ecosystem: npm
directory: '/' directory: "/"
schedule: schedule:
interval: daily interval: daily
groups: groups:
@ -9,7 +9,7 @@ updates:
update-types: [minor, patch] update-types: [minor, patch]
- package-ecosystem: github-actions - package-ecosystem: github-actions
directory: '/' directory: "/"
schedule: schedule:
interval: weekly interval: weekly
groups: groups:

View File

@ -1,7 +1,7 @@
name-template: 'v$RESOLVED_VERSION' name-template: "v$RESOLVED_VERSION"
tag-template: 'v$RESOLVED_VERSION' tag-template: "v$RESOLVED_VERSION"
version-template: $MAJOR.$MINOR.$PATCH version-template: $MAJOR.$MINOR.$PATCH
change-template: '* $TITLE (#$NUMBER) @$AUTHOR' change-template: "* $TITLE (#$NUMBER) @$AUTHOR"
template: | template: |
# What's Changed # What's Changed
@ -10,52 +10,52 @@ template: |
**Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION
categories: categories:
- title: '🚀 Features' - title: "🚀 Features"
labels: labels:
- 'feature' - "feature"
- title: '🐞 Bug Fixes' - "enhancement"
- title: "🐞 Bug Fixes"
labels: labels:
- 'bug' - "bug"
- title: '📈 Performance' - title: "📈 Performance"
labels: labels:
- 'perf' - "perf"
- 'enhancement'
- title: 📝 Documentation - title: 📝 Documentation
labels: labels:
- 'documentation' - "documentation"
- title: 👻 Maintenance - title: 👻 Maintenance
labels: labels:
- 'chore' - "chore"
- 'dependencies' - "dependencies"
# collapse-after: 12 # collapse-after: 12
- title: 🚦 Tests - title: 🚦 Tests
labels: labels:
- 'tests' - "tests"
- title: 'Breaking' - title: "Breaking"
label: 'breaking' label: "breaking"
version-resolver: version-resolver:
major: major:
labels: labels:
- 'major' - "major"
- 'breaking' - "breaking"
minor: minor:
labels: labels:
- 'minor' - "minor"
- "feature"
patch: patch:
labels: labels:
- 'feature' - "patch"
- 'patch' - "bug"
- 'bug' - "maintenance"
- 'maintenance' - "docs"
- 'docs' - "dependencies"
- 'dependencies' - "security"
- 'security'
exclude-labels: exclude-labels:
- 'skip-changelog' - "skip-changelog"
- 'no-changelog' - "no-changelog"
- 'changelog' - "changelog"
- 'bump versions' - "bump versions"
- 'reverted' - "reverted"
- 'invalid' - "invalid"

View File

@ -7,7 +7,7 @@ on:
- main - main
env: env:
HUSKY: '0' HUSKY: "0"
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }} group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
@ -19,15 +19,8 @@ permissions:
jobs: jobs:
post-update: post-update:
if: github.repository == 'vbenjs/vue-vben-admin'
# if: ${{ github.actor == 'dependabot[bot]' }} # if: ${{ github.actor == 'dependabot[bot]' }}
runs-on: ${{ matrix.os }} runs-on: ubuntu-latest
strategy:
matrix:
os:
- ubuntu-latest
# - macos-latest
- windows-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4

View File

@ -18,7 +18,7 @@ env:
jobs: jobs:
version: version:
if: (github.event.pull_request.merged || github.event_name == 'workflow_dispatch') && github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]') && github.repository == 'vbenjs/vue-vben-admin' if: (github.event.pull_request.merged || github.event_name == 'workflow_dispatch') && github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]')
# if: github.repository == 'vbenjs/vue-vben-admin' # if: github.repository == 'vbenjs/vue-vben-admin'
timeout-minutes: 15 timeout-minutes: 15
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -36,7 +36,7 @@ jobs:
uses: changesets/action@v1 uses: changesets/action@v1
with: with:
version: pnpm run version version: pnpm run version
commit: 'chore: bump versions' commit: "chore: bump versions"
title: 'chore: bump versions' title: "chore: bump versions"
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -5,7 +5,7 @@ on:
push: push:
branches: branches:
- main - main
- 'releases/*' - "releases/*"
permissions: permissions:
contents: read contents: read
@ -17,13 +17,13 @@ env:
jobs: jobs:
test: test:
name: Test name: Test
if: github.repository == 'vbenjs/vue-vben-admin' if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]')
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: os:
- ubuntu-latest - ubuntu-latest
# - macos-latest - macos-latest
- windows-latest - windows-latest
timeout-minutes: 20 timeout-minutes: 20
steps: steps:
@ -56,13 +56,13 @@ jobs:
lint: lint:
name: Lint name: Lint
if: github.repository == 'vbenjs/vue-vben-admin' if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]')
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: os:
- ubuntu-latest - ubuntu-latest
# - macos-latest - macos-latest
- windows-latest - windows-latest
steps: steps:
@ -79,14 +79,13 @@ jobs:
check: check:
name: Check name: Check
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
timeout-minutes: 20 timeout-minutes: 20
strategy: strategy:
matrix: matrix:
os: os:
- ubuntu-latest - ubuntu-latest
# - macos-latest - macos-latest
- windows-latest - windows-latest
steps: steps:
- name: Checkout code - name: Checkout code
@ -109,8 +108,8 @@ jobs:
ci-ok: ci-ok:
name: CI OK name: CI OK
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]') && always()
needs: [test, check, lint] needs: [test, check, lint]
env: env:
FAILURE: ${{ contains(join(needs.*.result, ','), 'failure') }} FAILURE: ${{ contains(join(needs.*.result, ','), 'failure') }}

View File

@ -9,20 +9,19 @@
# the `language` matrix defined below to confirm you have the correct set of # the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages. # supported CodeQL languages.
# #
name: 'CodeQL' name: "CodeQL"
on: on:
push: push:
branches: ['main'] branches: ["main"]
pull_request: pull_request:
branches: ['main'] branches: ["main"]
schedule: schedule:
- cron: '35 0 * * 0' - cron: "35 0 * * 0"
jobs: jobs:
analyze: analyze:
name: Analyze (${{ matrix.language }}) name: Analyze (${{ matrix.language }})
if: github.repository == 'vbenjs/vue-vben-admin'
# Runner size impacts CodeQL analysis time. To learn more, please see: # Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql # - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources # - https://gh.io/supported-runners-and-hardware-resources
@ -91,4 +90,4 @@ jobs:
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3 uses: github/codeql-action/analyze@v3
with: with:
category: '/language:${{matrix.language}}' category: "/language:${{matrix.language}}"

View File

@ -6,9 +6,9 @@ on:
- main - main
jobs: jobs:
deploy-playground-ftp: deploy-push-playground-ftp:
name: Deploy Push Playground Ftp name: Deploy Push Playground Ftp
if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]') && github.repository == 'vbenjs/vue-vben-admin' if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
@ -27,7 +27,7 @@ jobs:
uses: ./.github/actions/setup-node uses: ./.github/actions/setup-node
- name: Build - name: Build
run: pnpm build:play run: pnpm run build
- name: Sync Playground files - name: Sync Playground files
uses: SamKirkland/FTP-Deploy-Action@v4.3.5 uses: SamKirkland/FTP-Deploy-Action@v4.3.5
@ -37,22 +37,6 @@ jobs:
password: ${{ secrets.WEB_PLAYGROUND_FTP_PWSSWORD }} password: ${{ secrets.WEB_PLAYGROUND_FTP_PWSSWORD }}
local-dir: ./playground/dist/ local-dir: ./playground/dist/
deploy-docs-ftp:
name: Deploy Push Docs Ftp
if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]') && github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node
uses: ./.github/actions/setup-node
- name: Build
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.3.5
with: with:
@ -61,9 +45,9 @@ jobs:
password: ${{ secrets.WEBSITE_FTP_PASSWORD }} password: ${{ secrets.WEBSITE_FTP_PASSWORD }}
local-dir: ./docs/.vitepress/dist/ local-dir: ./docs/.vitepress/dist/
deploy-antd-ftp: deploy-push-antd-ftp:
name: Deploy Push Antd Ftp name: Deploy Push Antd Ftp
if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]') && github.repository == 'vbenjs/vue-vben-admin' if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
@ -82,7 +66,7 @@ jobs:
uses: ./.github/actions/setup-node uses: ./.github/actions/setup-node
- name: Build - name: Build
run: pnpm run build:antd run: pnpm run build
- name: Sync files - name: Sync files
uses: SamKirkland/FTP-Deploy-Action@v4.3.5 uses: SamKirkland/FTP-Deploy-Action@v4.3.5
@ -92,9 +76,9 @@ jobs:
password: ${{ secrets.WEB_ANTD_FTP_PASSWORD }} password: ${{ secrets.WEB_ANTD_FTP_PASSWORD }}
local-dir: ./apps/web-antd/dist/ local-dir: ./apps/web-antd/dist/
deploy-ele-ftp: deploy-push-ele-ftp:
name: Deploy Push Element Ftp name: Deploy Push Element Ftp
if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]') && github.repository == 'vbenjs/vue-vben-admin' if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
@ -113,7 +97,7 @@ jobs:
uses: ./.github/actions/setup-node uses: ./.github/actions/setup-node
- name: Build - name: Build
run: pnpm run build:ele run: pnpm run build
- name: Sync files - name: Sync files
uses: SamKirkland/FTP-Deploy-Action@v4.3.5 uses: SamKirkland/FTP-Deploy-Action@v4.3.5
@ -123,9 +107,9 @@ jobs:
password: ${{ secrets.WEB_ELE_FTP_PASSWORD }} password: ${{ secrets.WEB_ELE_FTP_PASSWORD }}
local-dir: ./apps/web-ele/dist/ local-dir: ./apps/web-ele/dist/
deploy-naive-ftp: deploy-push-naive-ftp:
name: Deploy Push Naive Ftp name: Deploy Push Naive Ftp
if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]') && github.repository == 'vbenjs/vue-vben-admin' if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
@ -144,7 +128,7 @@ jobs:
uses: ./.github/actions/setup-node uses: ./.github/actions/setup-node
- name: Build - name: Build
run: pnpm run build:naive run: pnpm run build
- name: Sync files - name: Sync files
uses: SamKirkland/FTP-Deploy-Action@v4.3.5 uses: SamKirkland/FTP-Deploy-Action@v4.3.5

View File

@ -17,7 +17,6 @@ jobs:
# write permission is required for autolabeler # write permission is required for autolabeler
# otherwise, read permission is required at least # otherwise, read permission is required at least
pull-requests: write pull-requests: write
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@v6

View File

@ -3,29 +3,23 @@ name: Issue Close Require
# 触发条件:每天零点 # 触发条件:每天零点
on: on:
workflow_dispatch:
schedule: schedule:
- cron: '0 0 * * *' - cron: "0 0 * * *"
permissions: permissions:
pull-requests: write pull-requests: write
contents: write contents: write
issues: write
jobs: jobs:
close-issues: close-issues:
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# 关闭未活动的 Issues # 步骤1关闭未活动的 Issues
- name: Close Inactive Issues - name: Close Inactive Issues
uses: actions/stale@v9 uses: actions-cool/issues-helper@v3
with: with:
days-before-stale: -1 # Issues and PR will never be flagged stale automatically. actions: "close-issues" # 执行动作:关闭 Issues
stale-issue-label: needs-reproduction # Label that flags an issue as stale. token: ${{ secrets.GITHUB_TOKEN }} # GitHub Token用于认证
only-labels: needs-reproduction # Only process these issues labels: "need reproduction" # 目标标签
days-before-issue-close: 3 inactive-day: 3 # 未活动天数阈值
ignore-updates: true
remove-stale-when-updated: false
close-issue-message: This issue was closed because it was open for 3 days without a valid reproduction.
close-issue-label: closed-by-action

View File

@ -13,34 +13,33 @@ permissions:
jobs: jobs:
reply-labeled: reply-labeled:
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest runs-on: ubuntu-latest
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@v3
with: with:
actions: 'remove-labels' actions: "remove-labels"
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }} issue-number: ${{ github.event.issue.number }}
labels: 'enhancement: pending triage' labels: "enhancement: pending triage"
- 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@v3
with: with:
actions: 'remove-labels' actions: "remove-labels"
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }} issue-number: ${{ github.event.issue.number }}
labels: 'bug: pending triage' labels: "bug: pending triage"
- 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@v3
with: with:
actions: 'create-comment, remove-labels' actions: "create-comment, remove-labels"
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }} issue-number: ${{ github.event.issue.number }}
body: | body: |
Hello @${{ github.event.issue.user.login }}. Please provide the complete reproduction steps and code. Issues labeled by `needs reproduction` will be closed if no activities in 3 days. Hello @${{ github.event.issue.user.login }}. Please provide the complete reproduction steps and code. Issues labeled by `needs reproduction` will be closed if no activities in 3 days.
labels: 'bug: pending triage' labels: "bug: pending triage"

View File

@ -2,7 +2,7 @@ name: Lock Threads
on: on:
schedule: schedule:
- cron: '0 0 * * *' - cron: "0 0 * * *"
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@ -11,14 +11,13 @@ permissions:
jobs: jobs:
action: action:
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@v5
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
issue-inactive-days: '14' issue-inactive-days: "30"
issue-lock-reason: '' issue-lock-reason: ""
pr-inactive-days: '30' pr-inactive-days: "30"
pr-lock-reason: '' pr-lock-reason: ""
process-only: 'issues, prs' process-only: "issues, prs"

View File

@ -3,10 +3,10 @@ name: Create Release Tag
on: on:
push: push:
tags: tags:
- 'v*.*.*' # Push events to matching v*, i.e. v1.0, v20.15.10 - "v*.*.*" # Push events to matching v*, i.e. v1.0, v20.15.10
env: env:
HUSKY: '0' HUSKY: "0"
permissions: permissions:
pull-requests: write pull-requests: write
@ -15,7 +15,6 @@ permissions:
jobs: jobs:
build: build:
name: Create Release name: Create Release
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:

View File

@ -9,9 +9,8 @@ on:
jobs: jobs:
main: main:
name: Semantic Pull Request
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Semantic Pull Request
steps: steps:
- name: Validate PR title - name: Validate PR title
uses: amannn/action-semantic-pull-request@v5 uses: amannn/action-semantic-pull-request@v5

View File

@ -1,19 +1,18 @@
name: 'Close stale issues' name: "Close stale issues"
on: on:
schedule: schedule:
- cron: '0 1 * * *' - cron: "0 1 * * *"
jobs: jobs:
stale: stale:
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v9 - uses: actions/stale@v9
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"
stale-pr-message: 'This PR 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-pr-message: "This PR 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"
exempt-issue-labels: 'bug,enhancement' exempt-issue-labels: "bug,enhancement"
days-before-stale: 60 days-before-stale: 60
days-before-close: 7 days-before-close: 7

1
.gitignore vendored
View File

@ -48,4 +48,3 @@ vite.config.ts.*
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
.history

View File

@ -3,4 +3,4 @@ ports:
onOpen: open-preview onOpen: open-preview
tasks: tasks:
- init: corepack enable && pnpm install - init: corepack enable && pnpm install
command: pnpm run dev:play command: pnpm run dev

28
.ls-lint.yml Normal file
View File

@ -0,0 +1,28 @@
ls:
.js: kebab-case | pointcase
.vue: kebab-case | pointcase
.ts: kebab-case | pointcase
.tsx: kebab-case | pointcase
.jsx: kebab-case | pointcase
.css: kebab-case | pointcase
.d.ts: kebab-case | pointcase
# shadcn 自动生成文件为 PascalCase 格式
packages/@core/ui-kit/shadcn-ui/src/components/ui:
.vue: PascalCase
ignore:
- "**/*.png"
- "**/*.jpg"
- "**/*.jpeg"
- "**/*.jpeg"
- "**/*.gif"
- "**/_util.ts"
- "**/deps/**"
- "**/dist/**"
- "**/node_modules/**"
- "**/.turbo/**"
- .git
- .vscode
- .idea
- node_modules
- .cache

2
.npmrc
View File

@ -1,4 +1,4 @@
registry = "https://registry.npmmirror.com" # registry = "https://registry.npmmirror.com"
public-hoist-pattern[]=husky public-hoist-pattern[]=husky
public-hoist-pattern[]=eslint public-hoist-pattern[]=eslint
public-hoist-pattern[]=prettier public-hoist-pattern[]=prettier

8
.vscode/launch.json vendored
View File

@ -9,7 +9,7 @@
"url": "http://localhost:5555", "url": "http://localhost:5555",
"env": { "NODE_ENV": "development" }, "env": { "NODE_ENV": "development" },
"sourceMaps": true, "sourceMaps": true,
"webRoot": "${workspaceFolder}" "webRoot": "${workspaceFolder}/playground/src"
}, },
{ {
"type": "chrome", "type": "chrome",
@ -18,7 +18,7 @@
"url": "http://localhost:5666", "url": "http://localhost:5666",
"env": { "NODE_ENV": "development" }, "env": { "NODE_ENV": "development" },
"sourceMaps": true, "sourceMaps": true,
"webRoot": "${workspaceFolder}" "webRoot": "${workspaceFolder}/apps/web-antd/src"
}, },
{ {
"type": "chrome", "type": "chrome",
@ -27,7 +27,7 @@
"url": "http://localhost:5777", "url": "http://localhost:5777",
"env": { "NODE_ENV": "development" }, "env": { "NODE_ENV": "development" },
"sourceMaps": true, "sourceMaps": true,
"webRoot": "${workspaceFolder}" "webRoot": "${workspaceFolder}/apps/web-ele/src"
}, },
{ {
"type": "chrome", "type": "chrome",
@ -36,7 +36,7 @@
"url": "http://localhost:5888", "url": "http://localhost:5888",
"env": { "NODE_ENV": "development" }, "env": { "NODE_ENV": "development" },
"sourceMaps": true, "sourceMaps": true,
"webRoot": "${workspaceFolder}" "webRoot": "${workspaceFolder}/apps/web-naive/src"
} }
] ]
} }

42
.vscode/settings.json vendored
View File

@ -14,6 +14,7 @@
"editor.tabSize": 2, "editor.tabSize": 2,
"editor.detectIndentation": false, "editor.detectIndentation": false,
"editor.cursorBlinking": "expand", "editor.cursorBlinking": "expand",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.largeFileOptimizations": false, "editor.largeFileOptimizations": false,
"editor.accessibilitySupport": "off", "editor.accessibilitySupport": "off",
"editor.cursorSmoothCaretAnimation": "on", "editor.cursorSmoothCaretAnimation": "on",
@ -36,34 +37,7 @@
"source.fixAll.stylelint": "explicit", "source.fixAll.stylelint": "explicit",
"source.organizeImports": "never" "source.organizeImports": "never"
}, },
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
// extensions // extensions
"extensions.ignoreRecommendations": true, "extensions.ignoreRecommendations": true,
@ -82,8 +56,7 @@
"*.ejs": "html", "*.ejs": "html",
"*.art": "html", "*.art": "html",
"**/tsconfig.json": "jsonc", "**/tsconfig.json": "jsonc",
"*.json": "jsonc", "*.json": "jsonc"
"package.json": "json"
}, },
"files.exclude": { "files.exclude": {
@ -194,10 +167,9 @@
"i18n-ally.localesPaths": [ "i18n-ally.localesPaths": [
"packages/locales/src/langs", "packages/locales/src/langs",
"playground/src/locales/langs", "playground/src/langs",
"apps/*/src/locales/langs" "apps/*/src/locales/langs"
], ],
"i18n-ally.pathMatcher": "{locale}.json",
"i18n-ally.enabledParsers": ["json", "ts", "js", "yaml"], "i18n-ally.enabledParsers": ["json", "ts", "js", "yaml"],
"i18n-ally.sourceLanguage": "en", "i18n-ally.sourceLanguage": "en",
"i18n-ally.displayLanguage": "zh-CN", "i18n-ally.displayLanguage": "zh-CN",
@ -212,12 +184,14 @@
"*.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", "Dockerfile": "Dockerfile,.docker*,docker-entrypoint.sh,build-local-docker*,nginx.conf",
"eslint.config.mjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,.ls-lint*,cspell.json",
"tailwind.config.mjs": "postcss.*" "tailwind.config.mjs": "postcss.*"
}, },
"commentTranslate.hover.enabled": false, "commentTranslate.hover.enabled": false,
"i18n-ally.keystyle": "nested", "i18n-ally.keystyle": "nested",
"commentTranslate.multiLineMerge": true, "commentTranslate.multiLineMerge": true,
"vue.server.hybridMode": true, "vue.server.hybridMode": true,
"typescript.tsdk": "node_modules/typescript/lib" "typescript.tsdk": "node_modules/typescript/lib",
"vitest.disableWorkspaceWarning": true
} }

View File

@ -1,12 +1,10 @@
<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> <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.6/source/logo-v1.webp"> </a> <br> <br>
[![license](https://img.shields.io/github/license/anncwb/vue-vben-admin.svg)](LICENSE) [![license](https://img.shields.io/github/license/anncwb/vue-vben-admin.svg)](LICENSE)
<h1>Vue Vben Admin</h1> <h1>Vue Vben Admin</h1>
</div> </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) ![build](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml/badge.svg) ![ci](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml/badge.svg) ![deploy](https://github.com/vbenjs/vue-vben-admin/actions/workflows/deploy.yml/badge.svg)
**日本語** | [English](./README.md) | [中文](./README.zh-CN.md) **日本語** | [English](./README.md) | [中文](./README.zh-CN.md)
## 紹介 ## 紹介
@ -80,7 +78,7 @@ pnpm build
## 変更ログ ## 変更ログ
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases) [CHANGELOG](./CHANGELOG.zh_CN.md)
## 貢献方法 ## 貢献方法
@ -125,15 +123,11 @@ pnpm build
[@Vben](https://github.com/anncwb) [@Vben](https://github.com/anncwb)
## スター歴史
[![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](https://unpkg.com/@vbenjs/static-source@0.1.7/source/sponsor.png) ![donate](https://unpkg.com/@vbenjs/static-source@0.1.6/source/sponsor.png)
<a style="display: block;width: 100px;height: 50px;line-height: 50px; color: #fff;text-align: center; background: #408aed;border-radius: 4px;" href="https://www.paypal.com/paypalme/cvvben">Paypal Me</a> <a style="display: block;width: 100px;height: 50px;line-height: 50px; color: #fff;text-align: center; background: #408aed;border-radius: 4px;" href="https://www.paypal.com/paypalme/cvvben">Paypal Me</a>

View File

@ -1,12 +1,10 @@
<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> <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.6/source/logo-v1.webp"> </a> <br> <br>
[![license](https://img.shields.io/github/license/anncwb/vue-vben-admin.svg)](LICENSE) [![license](https://img.shields.io/github/license/anncwb/vue-vben-admin.svg)](LICENSE)
<h1>Vue Vben Admin</h1> <h1>Vue Vben Admin</h1>
</div> </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) ![build](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml/badge.svg) ![ci](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml/badge.svg) ![deploy](https://github.com/vbenjs/vue-vben-admin/actions/workflows/deploy.yml/badge.svg)
**English** | [中文](./README.zh-CN.md) | [日本語](./README.ja-JP.md) **English** | [中文](./README.zh-CN.md) | [日本語](./README.ja-JP.md)
## Introduction ## Introduction
@ -79,7 +77,7 @@ pnpm build
## Change Log ## Change Log
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases) [CHANGELOG](./CHANGELOG.zh_CN.md)
## How to contribute ## How to contribute
@ -124,15 +122,11 @@ Support modern browsers, not IE
[@Vben](https://github.com/anncwb) [@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 ## Donate
If you think this project is helpful to you, you can help the author buy a cup of coffee to show your support! 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) ![donate](https://unpkg.com/@vbenjs/static-source@0.1.6/source/sponsor.png)
<a style="display: block;width: 100px;height: 50px;line-height: 50px; color: #fff;text-align: center; background: #408aed;border-radius: 4px;" href="https://www.paypal.com/paypalme/cvvben">Paypal Me</a> <a style="display: block;width: 100px;height: 50px;line-height: 50px; color: #fff;text-align: center; background: #408aed;border-radius: 4px;" href="https://www.paypal.com/paypalme/cvvben">Paypal Me</a>

View File

@ -1,12 +1,10 @@
<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> <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.6/source/logo-v1.webp"> </a> <br> <br>
[![license](https://img.shields.io/github/license/anncwb/vue-vben-admin.svg)](LICENSE) [![license](https://img.shields.io/github/license/anncwb/vue-vben-admin.svg)](LICENSE)
<h1>Vue Vben Admin</h1> <h1>Vue Vben Admin</h1>
</div> </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) ![build](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml/badge.svg) ![ci](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml/badge.svg) ![deploy](https://github.com/vbenjs/vue-vben-admin/actions/workflows/deploy.yml/badge.svg)
**中文** | [English](./README.md) | [日本語](./README.ja-JP.md) **中文** | [English](./README.md) | [日本語](./README.ja-JP.md)
## 简介 ## 简介
@ -77,10 +75,6 @@ pnpm dev
pnpm build pnpm build
``` ```
## 更新日志
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases)
## 如何贡献 ## 如何贡献
非常欢迎你的加入![提一个 Issue](https://github.com/anncwb/vue-vben-admin/issues/new/choose) 或者提交一个 Pull Request。 非常欢迎你的加入![提一个 Issue](https://github.com/anncwb/vue-vben-admin/issues/new/choose) 或者提交一个 Pull Request。
@ -124,15 +118,11 @@ pnpm build
[@Vben](https://github.com/anncwb) [@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](https://unpkg.com/@vbenjs/static-source@0.1.7/source/sponsor.png) ![donate](https://unpkg.com/@vbenjs/static-source@0.1.6/source/sponsor.png)
<a style="display: block;width: 100px;height: 50px;line-height: 50px; color: #fff;text-align: center; background: #408aed;border-radius: 4px;" href="https://www.paypal.com/paypalme/cvvben">Paypal Me</a> <a style="display: block;width: 100px;height: 50px;line-height: 50px; color: #fff;text-align: center; background: #408aed;border-radius: 4px;" href="https://www.paypal.com/paypalme/cvvben">Paypal Me</a>

View File

@ -1,3 +1 @@
PORT=5320 PORT=5320
ACCESS_TOKEN_SECRET=access_token_secret
REFRESH_TOKEN_SECRET=refresh_token_secret

View File

@ -2,7 +2,7 @@
## Description ## Description
Vben Admin 数据 mock 服务,没有对接任何的数据库,所有数据都是模拟的,用于前端开发时提供数据支持。线上环境不再提供 mock 集成,可自行部署服务或者对接真实数据,由于 `mock.js` 等工具有一些限制,比如上传文件不行、无法模拟复杂的逻辑等,所以这里使用了真实的后端服务来实现。唯一麻烦的是本地需要同时启动后端服务和前端服务,但是这样可以更好的模拟真实环境。该服务不需要手动启动,已经集成在 vite 插件内,随应用一起启用。 Vben Admin 数据 mock 服务没有对接任何的数据库所有数据都是模拟的用于前端开发时提供数据支持。线上环境不再提供mock集成可自行部署服务或者对接真实数据mock.js 等工具有一些限制,比如上传文件不行、无法模拟复杂的逻辑等,所以这里使用了真实的后端服务来实现。唯一麻烦的是本地需要同时启动后端服务和前端服务,但是这样可以更好的模拟真实环境。
## Running the app ## Running the app

View File

@ -1,14 +1,15 @@
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
export default eventHandler((event) => { export default eventHandler((event) => {
const userinfo = verifyAccessToken(event); const token = getHeader(event, 'Authorization');
if (!userinfo) {
return unAuthorizedResponse(event); if (!token) {
setResponseStatus(event, 401);
return useResponseError('UnauthorizedException', 'Unauthorized Exception');
} }
const username = Buffer.from(token, 'base64').toString('utf8');
const codes = const codes =
MOCK_CODES.find((item) => item.username === userinfo.username)?.codes ?? []; MOCK_CODES.find((item) => item.username === username)?.codes ?? [];
return useResponseSuccess(codes); return useResponseSuccess(codes);
}); });

View File

@ -1,36 +1,20 @@
import {
clearRefreshTokenCookie,
setRefreshTokenCookie,
} from '~/utils/cookie-utils';
import { generateAccessToken, generateRefreshToken } from '~/utils/jwt-utils';
import { forbiddenResponse } 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);
if (!password || !username) {
setResponseStatus(event, 400);
return useResponseError(
'BadRequestException',
'Username and password are required',
);
}
const findUser = MOCK_USERS.find( const findUser = MOCK_USERS.find(
(item) => item.username === username && item.password === password, (item) => item.username === username && item.password === password,
); );
if (!findUser) { if (!findUser) {
clearRefreshTokenCookie(event); setResponseStatus(event, 403);
return forbiddenResponse(event); return useResponseError('UnauthorizedException', '用户名或密码错误');
} }
const accessToken = generateAccessToken(findUser); const accessToken = Buffer.from(username).toString('base64');
const refreshToken = generateRefreshToken(findUser);
setRefreshTokenCookie(event, refreshToken);
return useResponseSuccess({ return useResponseSuccess({
...findUser,
accessToken, accessToken,
// TODO: refresh token
refreshToken: accessToken,
}); });
}); });

View File

@ -1,15 +0,0 @@
import {
clearRefreshTokenCookie,
getRefreshTokenFromCookie,
} from '~/utils/cookie-utils';
export default defineEventHandler(async (event) => {
const refreshToken = getRefreshTokenFromCookie(event);
if (!refreshToken) {
return useResponseSuccess('');
}
clearRefreshTokenCookie(event);
return useResponseSuccess('');
});

View File

@ -1,33 +0,0 @@
import {
clearRefreshTokenCookie,
getRefreshTokenFromCookie,
setRefreshTokenCookie,
} from '~/utils/cookie-utils';
import { verifyRefreshToken } from '~/utils/jwt-utils';
import { forbiddenResponse } from '~/utils/response';
export default defineEventHandler(async (event) => {
const refreshToken = getRefreshTokenFromCookie(event);
if (!refreshToken) {
return forbiddenResponse(event);
}
clearRefreshTokenCookie(event);
const userinfo = verifyRefreshToken(refreshToken);
if (!userinfo) {
return forbiddenResponse(event);
}
const findUser = MOCK_USERS.find(
(item) => item.username === userinfo.username,
);
if (!findUser) {
return forbiddenResponse(event);
}
const accessToken = generateAccessToken(findUser);
setRefreshTokenCookie(event, refreshToken);
return accessToken;
});

View File

@ -1,13 +1,14 @@
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
export default eventHandler((event) => { export default eventHandler((event) => {
const userinfo = verifyAccessToken(event); const token = getHeader(event, 'Authorization');
if (!userinfo) {
return unAuthorizedResponse(event); if (!token) {
setResponseStatus(event, 401);
return useResponseError('UnauthorizedException', 'Unauthorized Exception');
} }
const username = Buffer.from(token, 'base64').toString('utf8');
const menus = const menus =
MOCK_MENUS.find((item) => item.username === userinfo.username)?.menus ?? []; MOCK_MENUS.find((item) => item.username === username)?.menus ?? [];
return useResponseSuccess(menus); return useResponseSuccess(menus);
}); });

View File

@ -1,48 +0,0 @@
import { faker } from '@faker-js/faker';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
function generateMockDataList(count: number) {
const dataList = [];
for (let i = 0; i < count; i++) {
const dataItem = {
id: faker.string.uuid(),
imageUrl: faker.image.avatar(),
imageUrl2: faker.image.avatar(),
open: faker.datatype.boolean(),
status: faker.helpers.arrayElement(['success', 'error', 'warning']),
productName: faker.commerce.productName(),
price: faker.commerce.price(),
currency: faker.finance.currencyCode(),
quantity: faker.number.int({ min: 1, max: 100 }),
available: faker.datatype.boolean(),
category: faker.commerce.department(),
releaseDate: faker.date.past(),
rating: faker.number.float({ min: 1, max: 5 }),
description: faker.commerce.productDescription(),
weight: faker.number.float({ min: 0.1, max: 10 }),
color: faker.color.human(),
inProduction: faker.datatype.boolean(),
tags: Array.from({ length: 3 }, () => faker.commerce.productAdjective()),
};
dataList.push(dataItem);
}
return dataList;
}
const mockData = generateMockDataList(100);
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
await sleep(600);
const { page, pageSize } = getQuery(event);
return usePageResponseSuccess(page as string, pageSize as string, mockData);
});

View File

@ -1,11 +1,14 @@
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
export default eventHandler((event) => { export default eventHandler((event) => {
const userinfo = verifyAccessToken(event); const token = getHeader(event, 'Authorization');
if (!userinfo) { if (!token) {
return unAuthorizedResponse(event); setResponseStatus(event, 401);
return useResponseError('UnauthorizedException', 'Unauthorized Exception');
} }
return useResponseSuccess(userinfo); const username = Buffer.from(token, 'base64').toString('utf8');
const user = MOCK_USERS.find((item) => item.username === username);
const { password: _pwd, ...userInfo } = user;
return useResponseSuccess(userInfo);
}); });

View File

@ -1,7 +1,7 @@
import type { NitroErrorHandler } from 'nitropack'; import type { NitroErrorHandler } from 'nitropack';
const errorHandler: NitroErrorHandler = function (error, event) { const errorHandler: NitroErrorHandler = function (error, event) {
event.node.res.end(`[Error Handler] ${error.stack}`); event.res.end(`[error handler] ${error.stack}`);
}; };
export default errorHandler; export default errorHandler;

View File

@ -1,4 +1,11 @@
export default defineEventHandler((event) => { export default defineEventHandler((event) => {
// setResponseHeaders(event, {
// 'Access-Control-Allow-Credentials': 'true',
// 'Access-Control-Allow-Headers': '*',
// 'Access-Control-Allow-Methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
// 'Access-Control-Allow-Origin': '*',
// 'Access-Control-Expose-Headers': '*',
// });
if (event.method === 'OPTIONS') { if (event.method === 'OPTIONS') {
event.node.res.statusCode = 204; event.node.res.statusCode = 204;
event.node.res.statusMessage = 'No Content.'; event.node.res.statusMessage = 'No Content.';

View File

@ -6,16 +6,10 @@
"license": "MIT", "license": "MIT",
"author": "", "author": "",
"scripts": { "scripts": {
"build": "nitro build", "start": "nitro dev",
"start": "nitro dev" "build": "nitro build"
}, },
"dependencies": { "dependencies": {
"@faker-js/faker": "catalog:", "nitropack": "^2.9.7"
"jsonwebtoken": "catalog:",
"nitropack": "catalog:"
},
"devDependencies": {
"@types/jsonwebtoken": "catalog:",
"h3": "catalog:"
} }
} }

View File

@ -1,26 +0,0 @@
import type { EventHandlerRequest, H3Event } from 'h3';
export function clearRefreshTokenCookie(event: H3Event<EventHandlerRequest>) {
deleteCookie(event, 'jwt', {
httpOnly: true,
sameSite: 'none',
secure: true,
});
}
export function setRefreshTokenCookie(
event: H3Event<EventHandlerRequest>,
refreshToken: string,
) {
setCookie(event, 'jwt', refreshToken, {
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000,
sameSite: 'none',
secure: true,
});
}
export function getRefreshTokenFromCookie(event: H3Event<EventHandlerRequest>) {
const refreshToken = getCookie(event, 'jwt');
return refreshToken;
}

View File

@ -1,59 +0,0 @@
import type { EventHandlerRequest, H3Event } from 'h3';
import jwt from 'jsonwebtoken';
import { UserInfo } from './mock-data';
// TODO: Replace with your own secret key
const ACCESS_TOKEN_SECRET = 'access_token_secret';
const REFRESH_TOKEN_SECRET = 'refresh_token_secret';
export interface UserPayload extends UserInfo {
iat: number;
exp: number;
}
export function generateAccessToken(user: UserInfo) {
return jwt.sign(user, ACCESS_TOKEN_SECRET, { expiresIn: '7d' });
}
export function generateRefreshToken(user: UserInfo) {
return jwt.sign(user, REFRESH_TOKEN_SECRET, {
expiresIn: '30d',
});
}
export function verifyAccessToken(
event: H3Event<EventHandlerRequest>,
): null | Omit<UserInfo, 'password'> {
const authHeader = getHeader(event, 'Authorization');
if (!authHeader?.startsWith('Bearer')) {
return null;
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, ACCESS_TOKEN_SECRET) as UserPayload;
const username = decoded.username;
const user = MOCK_USERS.find((item) => item.username === username);
const { password: _pwd, ...userinfo } = user;
return userinfo;
} catch {
return null;
}
}
export function verifyRefreshToken(
token: string,
): null | Omit<UserInfo, 'password'> {
try {
const decoded = jwt.verify(token, REFRESH_TOKEN_SECRET) as UserPayload;
const username = decoded.username;
const user = MOCK_USERS.find((item) => item.username === username);
const { password: _pwd, ...userinfo } = user;
return userinfo;
} catch {
return null;
}
}

View File

@ -1,12 +1,4 @@
export interface UserInfo { export const MOCK_USERS = [
id: number;
password: string;
realName: string;
roles: string[];
username: string;
}
export const MOCK_USERS: UserInfo[] = [
{ {
id: 0, id: 0,
password: '123456', password: '123456',

View File

@ -1,5 +1,3 @@
import type { EventHandlerRequest, H3Event } from 'h3';
export function useResponseSuccess<T = any>(data: T) { export function useResponseSuccess<T = any>(data: T) {
return { return {
code: 0, code: 0,
@ -9,27 +7,6 @@ export function useResponseSuccess<T = any>(data: T) {
}; };
} }
export function usePageResponseSuccess<T = any>(
page: number | string,
pageSize: number | string,
list: T[],
{ message = 'ok' } = {},
) {
const pageData = pagination(
Number.parseInt(`${page}`),
Number.parseInt(`${pageSize}`),
list,
);
return {
...useResponseSuccess({
items: pageData,
total: list.length,
}),
message,
};
}
export function useResponseError(message: string, error: any = null) { export function useResponseError(message: string, error: any = null) {
return { return {
code: -1, code: -1,
@ -38,28 +15,3 @@ export function useResponseError(message: string, error: any = null) {
message, message,
}; };
} }
export function forbiddenResponse(event: H3Event<EventHandlerRequest>) {
setResponseStatus(event, 403);
return useResponseError('ForbiddenException', 'Forbidden Exception');
}
export function unAuthorizedResponse(event: H3Event<EventHandlerRequest>) {
setResponseStatus(event, 401);
return useResponseError('UnauthorizedException', 'Unauthorized Exception');
}
export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function pagination<T = any>(
pageNo: number,
pageSize: number,
array: T[],
): T[] {
const offset = (pageNo - 1) * Number(pageSize);
return offset + Number(pageSize) >= array.length
? array.slice(offset)
: array.slice(offset, offset + Number(pageSize));
}

View File

@ -14,6 +14,3 @@ VITE_ROUTER_HISTORY=hash
# 是否注入全局loading # 是否注入全局loading
VITE_INJECT_APP_LOADING=true VITE_INJECT_APP_LOADING=true
# 打包后是否生成dist.zip
VITE_ARCHIVER=true

View File

@ -1,6 +1,6 @@
{ {
"name": "@vben/web-antd", "name": "@vben/web-antd",
"version": "5.3.2", "version": "5.1.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": {
@ -27,24 +27,24 @@
}, },
"dependencies": { "dependencies": {
"@vben/access": "workspace:*", "@vben/access": "workspace:*",
"@vben/chart-ui": "workspace:*",
"@vben/common-ui": "workspace:*", "@vben/common-ui": "workspace:*",
"@vben/constants": "workspace:*", "@vben/constants": "workspace:*",
"@vben/hooks": "workspace:*", "@vben/hooks": "workspace:*",
"@vben/icons": "workspace:*", "@vben/icons": "workspace:*",
"@vben/layouts": "workspace:*", "@vben/layouts": "workspace:*",
"@vben/locales": "workspace:*", "@vben/locales": "workspace:*",
"@vben/plugins": "workspace:*",
"@vben/preferences": "workspace:*", "@vben/preferences": "workspace:*",
"@vben/request": "workspace:*", "@vben/request": "workspace:*",
"@vben/stores": "workspace:*", "@vben/stores": "workspace:*",
"@vben/styles": "workspace:*", "@vben/styles": "workspace:*",
"@vben/types": "workspace:*", "@vben/types": "workspace:*",
"@vben/utils": "workspace:*", "@vben/utils": "workspace:*",
"@vueuse/core": "catalog:", "@vueuse/core": "^10.11.1",
"ant-design-vue": "catalog:", "ant-design-vue": "^4.2.3",
"dayjs": "catalog:", "dayjs": "^1.11.12",
"pinia": "catalog:", "pinia": "2.2.1",
"vue": "catalog:", "vue": "^3.4.37",
"vue-router": "catalog:" "vue-router": "^4.4.3"
} }
} }

View File

@ -1,138 +0,0 @@
import type {
BaseFormComponentType,
VbenFormSchema as FormSchema,
VbenFormProps,
} from '@vben/common-ui';
import type { Component, SetupContext } from 'vue';
import { h } from 'vue';
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
import {
AutoComplete,
Button,
Checkbox,
CheckboxGroup,
DatePicker,
Divider,
Input,
InputNumber,
InputPassword,
Mentions,
Radio,
RadioGroup,
RangePicker,
Rate,
Select,
Space,
Switch,
Textarea,
TimePicker,
TreeSelect,
Upload,
} from 'ant-design-vue';
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type FormComponentType =
| 'AutoComplete'
| 'Checkbox'
| 'CheckboxGroup'
| 'DatePicker'
| 'Divider'
| 'Input'
| 'InputNumber'
| 'InputPassword'
| 'Mentions'
| 'Radio'
| 'RadioGroup'
| 'RangePicker'
| 'Rate'
| 'Select'
| 'Space'
| 'Switch'
| 'Textarea'
| 'TimePicker'
| 'TreeSelect'
| 'Upload'
| BaseFormComponentType;
const withDefaultPlaceholder = <T extends Component>(
component: T,
type: 'input' | 'select',
) => {
return (props: any, { attrs, slots }: Omit<SetupContext, 'expose'>) => {
const placeholder = props?.placeholder || $t(`placeholder.${type}`);
return h(component, { ...props, ...attrs, placeholder }, slots);
};
};
// 初始化表单组件并注册到form组件内部
setupVbenForm<FormComponentType>({
components: {
AutoComplete,
Checkbox,
CheckboxGroup,
DatePicker,
// 自定义默认的重置按钮
DefaultResetActionButton: (props, { attrs, slots }) => {
return h(Button, { ...props, attrs, type: 'default' }, slots);
},
// 自定义默认的提交按钮
DefaultSubmitActionButton: (props, { attrs, slots }) => {
return h(Button, { ...props, attrs, type: 'primary' }, slots);
},
Divider,
Input: withDefaultPlaceholder(Input, 'input'),
InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
Mentions: withDefaultPlaceholder(Mentions, 'input'),
Radio,
RadioGroup,
RangePicker,
Rate,
Select: withDefaultPlaceholder(Select, 'select'),
Space,
Switch,
Textarea: withDefaultPlaceholder(Textarea, 'input'),
TimePicker,
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
Upload,
},
config: {
// ant design vue组件库默认都是 v-model:value
baseModelPropName: 'value',
// 一些组件是 v-model:checked 或者 v-model:fileList
modelPropNameMap: {
Checkbox: 'checked',
Radio: 'checked',
Switch: 'checked',
Upload: 'fileList',
},
},
defineRules: {
// 输入项目必填国际化适配
required: (value, _params, ctx) => {
if (value === undefined || value === null || value.length === 0) {
return $t('formRules.required', [ctx.label]);
}
return true;
},
// 选择项目必填国际化适配
selectRequired: (value, _params, ctx) => {
if (value === undefined || value === null) {
return $t('formRules.selectRequired', [ctx.label]);
}
return true;
},
},
});
const useVbenForm = useForm<FormComponentType>;
export { useVbenForm, z };
export type VbenFormSchema = FormSchema<FormComponentType>;
export type { VbenFormProps };

View File

@ -1,2 +0,0 @@
export * from './form';
export * from './vxe-table';

View File

@ -1,59 +0,0 @@
import { h } from 'vue';
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
import { Button, Image } from 'ant-design-vue';
import { useVbenForm } from './form';
setupVbenVxeTable({
configVxeTable: (vxeUI) => {
vxeUI.setConfig({
grid: {
align: 'center',
border: true,
minHeight: 180,
proxyConfig: {
autoLoad: true,
response: {
result: 'items',
total: 'total',
list: 'items',
},
showActiveMsg: true,
showResponseMsg: false,
},
round: true,
size: 'small',
},
});
// 表格配置项可以用 cellRender: { name: 'CellImage' },
vxeUI.renderer.add('CellImage', {
renderDefault(_renderOpts, params) {
const { column, row } = params;
return h(Image, { src: row[column.field] });
},
});
// 表格配置项可以用 cellRender: { name: 'CellLink' },
vxeUI.renderer.add('CellLink', {
renderDefault(renderOpts) {
const { props } = renderOpts;
return h(
Button,
{ size: 'small', type: 'link' },
{ default: () => props?.text },
);
},
});
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
// vxeUI.formats.add
},
useVbenForm,
});
export { useVbenVxeGrid };
export type * from '@vben/plugins/vxe-table';

View File

@ -1,4 +1,4 @@
import { baseRequestClient, requestClient } from '#/api/request'; import { requestClient } from '#/api/request';
export namespace AuthApi { export namespace AuthApi {
/** 登录接口参数 */ /** 登录接口参数 */
@ -10,11 +10,11 @@ export namespace AuthApi {
/** 登录接口返回值 */ /** 登录接口返回值 */
export interface LoginResult { export interface LoginResult {
accessToken: string; accessToken: string;
} desc: string;
realName: string;
export interface RefreshTokenResult { refreshToken: string;
data: string; userId: string;
status: number; username: string;
} }
} }
@ -25,24 +25,6 @@ export async function loginApi(data: AuthApi.LoginParams) {
return requestClient.post<AuthApi.LoginResult>('/auth/login', data); return requestClient.post<AuthApi.LoginResult>('/auth/login', data);
} }
/**
* accessToken
*/
export async function refreshTokenApi() {
return baseRequestClient.post<AuthApi.RefreshTokenResult>('/auth/refresh', {
withCredentials: true,
});
}
/**
* 退
*/
export async function logoutApi() {
return baseRequestClient.post('/auth/logout', {
withCredentials: true,
});
}
/** /**
* *
*/ */

View File

@ -5,105 +5,63 @@ import type { HttpResponse } from '@vben/request';
import { useAppConfig } from '@vben/hooks'; import { useAppConfig } from '@vben/hooks';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { import { RequestClient } from '@vben/request';
authenticateResponseInterceptor,
errorMessageResponseInterceptor,
RequestClient,
} from '@vben/request';
import { useAccessStore } from '@vben/stores'; import { useAccessStore } from '@vben/stores';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { useAuthStore } from '#/store'; import { useAuthStore } from '#/store';
import { refreshTokenApi } from './core';
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
function createRequestClient(baseURL: string) { function createRequestClient(baseURL: string) {
const client = new RequestClient({ const client = new RequestClient({
baseURL, baseURL,
}); // 为每个请求携带 Authorization
makeAuthorization: () => {
return {
// 默认
key: 'Authorization',
tokenHandler: () => {
const accessStore = useAccessStore();
return {
refreshToken: `${accessStore.refreshToken}`,
token: `${accessStore.accessToken}`,
};
},
unAuthorizedHandler: async () => {
const accessStore = useAccessStore();
const authStore = useAuthStore();
accessStore.setAccessToken(null);
/** if (preferences.app.loginExpiredMode === 'modal') {
* accessStore.setLoginExpired(true);
*/ } else {
async function doReAuthenticate() { // 退出登录
console.warn('Access token or refresh token is invalid or expired. '); await authStore.logout();
const accessStore = useAccessStore(); }
const authStore = useAuthStore(); },
accessStore.setAccessToken(null); };
if ( },
preferences.app.loginExpiredMode === 'modal' && makeErrorMessage: (msg) => message.error(msg),
accessStore.isAccessChecked
) { makeRequestHeaders: () => {
accessStore.setLoginExpired(true); return {
} else { // 为每个请求携带 Accept-Language
await authStore.logout(); 'Accept-Language': preferences.app.locale,
};
},
});
client.addResponseInterceptor<HttpResponse>((response) => {
const { data: responseData, status } = response;
const { code, data, message: msg } = responseData;
if (status >= 200 && status < 400 && code === 0) {
return data;
} }
} throw new Error(`Error ${status}: ${msg}`);
/**
* token逻辑
*/
async function doRefreshToken() {
const accessStore = useAccessStore();
const resp = await refreshTokenApi();
const newToken = resp.data;
accessStore.setAccessToken(newToken);
return newToken;
}
function formatToken(token: null | string) {
return token ? `Bearer ${token}` : null;
}
// 请求头处理
client.addRequestInterceptor({
fulfilled: async (config) => {
const accessStore = useAccessStore();
config.headers.Authorization = formatToken(accessStore.accessToken);
config.headers['Accept-Language'] = preferences.app.locale;
return config;
},
}); });
// response数据解构
client.addResponseInterceptor<HttpResponse>({
fulfilled: (response) => {
const { data: responseData, status } = response;
const { code, data, message: msg } = responseData;
if (status >= 200 && status < 400 && code === 0) {
return data;
}
throw new Error(`Error ${status}: ${msg}`);
},
});
// token过期的处理
client.addResponseInterceptor(
authenticateResponseInterceptor({
client,
doReAuthenticate,
doRefreshToken,
enableRefreshToken: preferences.app.enableRefreshToken,
formatToken,
}),
);
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
client.addResponseInterceptor(
errorMessageResponseInterceptor((msg: string, _error) => {
// 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
message.error(msg);
}),
);
return client; return client;
} }
export const requestClient = createRequestClient(apiURL); export const requestClient = createRequestClient(apiURL);
export const baseRequestClient = new RequestClient({ baseURL: apiURL });

View File

@ -1,23 +0,0 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { AuthPageLayout } from '@vben/layouts';
import { preferences } from '@vben/preferences';
import { $t } from '#/locales';
const appName = computed(() => preferences.app.name);
const logo = computed(() => preferences.logo.source);
</script>
<template>
<AuthPageLayout
:app-name="appName"
:logo="logo"
:page-description="$t('authentication.pageDesc')"
:page-title="$t('authentication.pageTitle')"
>
<!-- 自定义工具栏 -->
<!-- <template #toolbar></template> -->
</AuthPageLayout>
</template>

View File

@ -2,9 +2,10 @@
import type { NotificationItem } from '@vben/layouts'; import type { NotificationItem } from '@vben/layouts';
import { computed, ref } from 'vue'; import { computed, ref } 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 { LOGIN_PATH, VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons'; import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
import { import {
BasicLayout, BasicLayout,
@ -13,12 +14,17 @@ import {
UserDropdown, UserDropdown,
} from '@vben/layouts'; } from '@vben/layouts';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores'; import {
resetAllStores,
storeToRefs,
useAccessStore,
useUserStore,
} from '@vben/stores';
import { openWindow } from '@vben/utils'; import { openWindow } from '@vben/utils';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { resetRoutes } from '#/router';
import { useAuthStore } from '#/store'; import { useAuthStore } from '#/store';
import LoginForm from '#/views/_core/authentication/login.vue';
const notifications = ref<NotificationItem[]>([ const notifications = ref<NotificationItem[]>([
{ {
@ -88,12 +94,18 @@ const menus = computed(() => [
}, },
]); ]);
const { loginLoading } = storeToRefs(authStore);
const avatar = computed(() => { const avatar = computed(() => {
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar; return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
}); });
const router = useRouter();
async function handleLogout() { async function handleLogout() {
await authStore.logout(false); resetAllStores();
resetRoutes();
await router.replace(LOGIN_PATH);
} }
function handleNoticeClear() { function handleNoticeClear() {
@ -129,9 +141,11 @@ function handleMakeAll() {
<AuthenticationLoginExpiredModal <AuthenticationLoginExpiredModal
v-model:open="accessStore.loginExpired" v-model:open="accessStore.loginExpired"
:avatar :avatar
> :loading="loginLoading"
<LoginForm /> password-placeholder="123456"
</AuthenticationLoginExpiredModal> username-placeholder="vben"
@submit="authStore.authLogin"
/>
</template> </template>
<template #lock-screen> <template #lock-screen>
<LockScreen :avatar @to-login="handleLogout" /> <LockScreen :avatar @to-login="handleLogout" />

View File

@ -1,6 +1,8 @@
const BasicLayout = () => import('./basic.vue'); const BasicLayout = () => import('./basic.vue');
const AuthPageLayout = () => import('./auth.vue');
const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView); const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);
const AuthPageLayout = () =>
import('@vben/layouts').then((m) => m.AuthPageLayout);
export { AuthPageLayout, BasicLayout, IFrameView }; export { AuthPageLayout, BasicLayout, IFrameView };

View File

@ -91,4 +91,4 @@ async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
}); });
} }
export { $t, antdLocale, setupI18n }; export { $t, antdLocale, loadMessages, setupI18n };

View File

@ -92,8 +92,10 @@ function setupAccessGuard(router: Router) {
return to; return to;
} }
const accessRoutes = accessStore.accessRoutes;
// 是否已经生成过动态路由 // 是否已经生成过动态路由
if (accessStore.isAccessChecked) { if (accessRoutes && accessRoutes.length > 0) {
return true; return true;
} }
@ -113,7 +115,6 @@ function setupAccessGuard(router: Router) {
// 保存菜单信息和路由信息 // 保存菜单信息和路由信息
accessStore.setAccessMenus(accessibleMenus); accessStore.setAccessMenus(accessibleMenus);
accessStore.setAccessRoutes(accessibleRoutes); accessStore.setAccessRoutes(accessibleRoutes);
accessStore.setIsAccessChecked(true);
const redirectPath = (from.query.redirect ?? to.fullPath) as string; const redirectPath = (from.query.redirect ?? to.fullPath) as string;
return { return {

View File

@ -19,12 +19,7 @@ const router = createRouter({
: createWebHistory(import.meta.env.VITE_BASE), : createWebHistory(import.meta.env.VITE_BASE),
// 应该添加到路由的初始路由列表。 // 应该添加到路由的初始路由列表。
routes, routes,
scrollBehavior: (to, _from, savedPosition) => { scrollBehavior: () => ({ left: 0, top: 0 }),
if (savedPosition) {
return savedPosition;
}
return to.hash ? { behavior: 'smooth', el: to.hash } : { left: 0, top: 0 };
},
// 是否应该禁止尾部斜杠。 // 是否应该禁止尾部斜杠。
// strict: true, // strict: true,
}); });

View File

@ -29,7 +29,6 @@ const routes: RouteRecordRaw[] = [
path: '/workspace', path: '/workspace',
component: () => import('#/views/dashboard/workspace/index.vue'), component: () => import('#/views/dashboard/workspace/index.vue'),
meta: { meta: {
icon: 'carbon:workspace',
title: $t('page.dashboard.workspace'), title: $t('page.dashboard.workspace'),
}, },
}, },

View File

@ -26,7 +26,7 @@ const routes: RouteRecordRaw[] = [
{ {
name: 'VbenAbout', name: 'VbenAbout',
path: '/vben-admin/about', path: '/vben-admin/about',
component: () => import('#/views/_core/about/index.vue'), component: () => import('#/views/_core/vben/about/index.vue'),
meta: { meta: {
icon: 'lucide:copyright', icon: 'lucide:copyright',
title: $t('page.vben.about'), title: $t('page.vben.about'),
@ -58,7 +58,6 @@ const routes: RouteRecordRaw[] = [
component: IFrameView, component: IFrameView,
meta: { meta: {
badgeType: 'dot', badgeType: 'dot',
icon: 'logos:naiveui',
link: VBEN_NAIVE_PREVIEW_URL, link: VBEN_NAIVE_PREVIEW_URL,
title: $t('page.vben.naive-ui'), title: $t('page.vben.naive-ui'),
}, },
@ -69,7 +68,6 @@ const routes: RouteRecordRaw[] = [
component: IFrameView, component: IFrameView,
meta: { meta: {
badgeType: 'dot', badgeType: 'dot',
icon: 'logos:element',
link: VBEN_ELE_PREVIEW_URL, link: VBEN_ELE_PREVIEW_URL,
title: $t('page.vben.element-plus'), title: $t('page.vben.element-plus'),
}, },

View File

@ -10,7 +10,7 @@ import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
import { notification } from 'ant-design-vue'; import { notification } from 'ant-design-vue';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api'; import { getAccessCodesApi, getUserInfoApi, loginApi } from '#/api';
import { $t } from '#/locales'; import { $t } from '#/locales';
export const useAuthStore = defineStore('auth', () => { export const useAuthStore = defineStore('auth', () => {
@ -33,11 +33,13 @@ export const useAuthStore = defineStore('auth', () => {
let userInfo: null | UserInfo = null; let userInfo: null | UserInfo = null;
try { try {
loginLoading.value = true; loginLoading.value = true;
const { accessToken } = await loginApi(params); const { accessToken, refreshToken } = await loginApi(params);
// 如果成功获取到 accessToken // 如果成功获取到 accessToken
if (accessToken) { if (accessToken) {
// 将 accessToken 存储到 accessStore 中
accessStore.setAccessToken(accessToken); accessStore.setAccessToken(accessToken);
accessStore.setRefreshToken(refreshToken);
// 获取用户信息并存储到 accessStore 中 // 获取用户信息并存储到 accessStore 中
const [fetchUserInfoResult, accessCodes] = await Promise.all([ const [fetchUserInfoResult, accessCodes] = await Promise.all([
@ -75,23 +77,16 @@ export const useAuthStore = defineStore('auth', () => {
}; };
} }
async function logout(redirect: boolean = true) { async function logout() {
try {
await logoutApi();
} catch {
// 不做任何处理
}
resetAllStores(); resetAllStores();
accessStore.setLoginExpired(false); accessStore.setLoginExpired(false);
// 回登陆页带上当前路由地址 // 回登陆页带上当前路由地址
await router.replace({ await router.replace({
path: LOGIN_PATH, path: LOGIN_PATH,
query: redirect query: {
? { redirect: encodeURIComponent(router.currentRoute.value.fullPath),
redirect: encodeURIComponent(router.currentRoute.value.fullPath), },
}
: {},
}); });
} }

View File

@ -1,49 +1,15 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { LoginCodeParams, VbenFormSchema } from '@vben/common-ui'; import type { LoginCodeParams } from '@vben/common-ui';
import { computed, ref } from 'vue'; import { ref } from 'vue';
import { AuthenticationCodeLogin, z } from '@vben/common-ui'; import { AuthenticationCodeLogin } from '@vben/common-ui';
import { $t } from '@vben/locales'; import { LOGIN_PATH } from '@vben/constants';
defineOptions({ name: 'CodeLogin' }); defineOptions({ name: 'CodeLogin' });
const loading = ref(false); const loading = ref(false);
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenInput',
componentProps: {
placeholder: $t('authentication.mobile'),
},
fieldName: 'phoneNumber',
label: $t('authentication.mobile'),
rules: z
.string()
.min(1, { message: $t('authentication.mobileTip') })
.refine((v) => /^\d{11}$/.test(v), {
message: $t('authentication.mobileErrortip'),
}),
},
{
component: 'VbenPinInput',
componentProps: {
createText: (countdown: number) => {
const text =
countdown > 0
? $t('authentication.sendText', [countdown])
: $t('authentication.sendCode');
return text;
},
placeholder: $t('authentication.code'),
},
fieldName: 'code',
label: $t('authentication.code'),
rules: z.string().min(1, { message: $t('authentication.codeTip') }),
},
];
});
/** /**
* 异步处理登录操作 * 异步处理登录操作
* Asynchronously handle the login process * Asynchronously handle the login process
@ -57,8 +23,8 @@ async function handleLogin(values: LoginCodeParams) {
<template> <template>
<AuthenticationCodeLogin <AuthenticationCodeLogin
:form-schema="formSchema"
:loading="loading" :loading="loading"
:login-path="LOGIN_PATH"
@submit="handleLogin" @submit="handleLogin"
/> />
</template> </template>

View File

@ -1,32 +1,13 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VbenFormSchema } from '@vben/common-ui'; import { ref } from 'vue';
import { computed, ref } from 'vue'; import { AuthenticationForgetPassword } from '@vben/common-ui';
import { LOGIN_PATH } from '@vben/constants';
import { AuthenticationForgetPassword, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
defineOptions({ name: 'ForgetPassword' }); defineOptions({ name: 'ForgetPassword' });
const loading = ref(false); const loading = ref(false);
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenInput',
componentProps: {
placeholder: 'example@example.com',
},
fieldName: 'email',
label: $t('authentication.email'),
rules: z
.string()
.min(1, { message: $t('authentication.emailTip') })
.email($t('authentication.emailValidErrorTip')),
},
];
});
function handleSubmit(value: string) { function handleSubmit(value: string) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('reset email:', value); console.log('reset email:', value);
@ -35,8 +16,8 @@ function handleSubmit(value: string) {
<template> <template>
<AuthenticationForgetPassword <AuthenticationForgetPassword
:form-schema="formSchema"
:loading="loading" :loading="loading"
:login-path="LOGIN_PATH"
@submit="handleSubmit" @submit="handleSubmit"
/> />
</template> </template>

View File

@ -1,98 +1,18 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VbenFormSchema } from '@vben/common-ui'; import { AuthenticationLogin } from '@vben/common-ui';
import type { BasicOption } from '@vben/types';
import { computed, markRaw } from 'vue';
import { AuthenticationLogin, SliderCaptcha, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { useAuthStore } from '#/store'; import { useAuthStore } from '#/store';
defineOptions({ name: 'Login' }); defineOptions({ name: 'Login' });
const authStore = useAuthStore(); const authStore = useAuthStore();
const MOCK_USER_OPTIONS: BasicOption[] = [
{
label: 'Super',
value: 'vben',
},
{
label: 'Admin',
value: 'admin',
},
{
label: 'User',
value: 'jack',
},
];
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenSelect',
componentProps: {
options: MOCK_USER_OPTIONS,
placeholder: $t('authentication.selectAccount'),
},
fieldName: 'selectAccount',
label: $t('authentication.selectAccount'),
rules: z
.string()
.min(1, { message: $t('authentication.selectAccount') })
.optional()
.default('vben'),
},
{
component: 'VbenInput',
componentProps: {
placeholder: $t('authentication.usernameTip'),
},
dependencies: {
trigger(values, form) {
if (values.selectAccount) {
const findUser = MOCK_USER_OPTIONS.find(
(item) => item.value === values.selectAccount,
);
if (findUser) {
form.setValues({
password: '123456',
username: findUser.value,
});
}
}
},
triggerFields: ['selectAccount'],
},
fieldName: 'username',
label: $t('authentication.username'),
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
},
{
component: 'VbenInputPassword',
componentProps: {
placeholder: $t('authentication.password'),
},
fieldName: 'password',
label: $t('authentication.password'),
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
},
{
component: markRaw(SliderCaptcha),
fieldName: 'captcha',
rules: z.boolean().refine((value) => value, {
message: $t('authentication.verifyRequiredTip'),
}),
},
];
});
</script> </script>
<template> <template>
<AuthenticationLogin <AuthenticationLogin
:form-schema="formSchema"
:loading="authStore.loginLoading" :loading="authStore.loginLoading"
password-placeholder="123456"
username-placeholder="vben"
@submit="authStore.authLogin" @submit="authStore.authLogin"
/> />
</template> </template>

View File

@ -1,91 +1,15 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { LoginAndRegisterParams, VbenFormSchema } from '@vben/common-ui'; import type { LoginAndRegisterParams } from '@vben/common-ui';
import { computed, h, ref } from 'vue'; import { ref } from 'vue';
import { AuthenticationRegister, z } from '@vben/common-ui'; import { AuthenticationRegister } from '@vben/common-ui';
import { $t } from '@vben/locales'; import { LOGIN_PATH } from '@vben/constants';
defineOptions({ name: 'Register' }); defineOptions({ name: 'Register' });
const loading = ref(false); const loading = ref(false);
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenInput',
componentProps: {
placeholder: $t('authentication.usernameTip'),
},
fieldName: 'username',
label: $t('authentication.username'),
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
},
{
component: 'VbenInputPassword',
componentProps: {
passwordStrength: true,
placeholder: $t('authentication.password'),
},
fieldName: 'password',
label: $t('authentication.password'),
renderComponentContent() {
return {
strengthText: () => $t('authentication.passwordStrength'),
};
},
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
},
{
component: 'VbenInputPassword',
componentProps: {
placeholder: $t('authentication.confirmPassword'),
},
dependencies: {
rules(values) {
const { password } = values;
return z
.string()
.min(1, { message: $t('authentication.passwordTip') })
.refine((value) => value === password, {
message: $t('authentication.confirmPasswordTip'),
});
},
triggerFields: ['password'],
},
fieldName: 'confirmPassword',
label: $t('authentication.confirmPassword'),
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
},
{
component: 'VbenCheckbox',
fieldName: 'agreePolicy',
renderComponentContent: () => ({
default: () =>
h('span', [
$t('authentication.agree'),
h(
'a',
{
class:
'cursor-pointer text-primary ml-1 hover:text-primary-hover',
href: '',
},
[
$t('authentication.privacyPolicy'),
'&',
$t('authentication.terms'),
],
),
]),
}),
rules: z.boolean().refine((value) => !!value, {
message: $t('authentication.agreeTip'),
}),
},
];
});
function handleSubmit(value: LoginAndRegisterParams) { function handleSubmit(value: LoginAndRegisterParams) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('register submit:', value); console.log('register submit:', value);
@ -94,8 +18,8 @@ function handleSubmit(value: LoginAndRegisterParams) {
<template> <template>
<AuthenticationRegister <AuthenticationRegister
:form-schema="formSchema"
:loading="loading" :loading="loading"
:login-path="LOGIN_PATH"
@submit="handleSubmit" @submit="handleSubmit"
/> />
</template> </template>

View File

@ -1,11 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui';
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
const chartRef = ref<EchartsUIType>(); const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef); const { renderEcharts } = useEcharts(chartRef);
@ -55,27 +51,12 @@ onMounted(() => {
}, },
trigger: 'axis', trigger: 'axis',
}, },
// xAxis: {
// axisTick: {
// show: false,
// },
// boundaryGap: false,
// data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
// type: 'category',
// },
xAxis: { xAxis: {
axisTick: { axisTick: {
show: false, show: false,
}, },
boundaryGap: false, boundaryGap: false,
data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`), data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
splitLine: {
lineStyle: {
type: 'solid',
width: 1,
},
show: true,
},
type: 'category', type: 'category',
}, },
yAxis: [ yAxis: [
@ -84,10 +65,7 @@ onMounted(() => {
show: false, show: false,
}, },
max: 80_000, max: 80_000,
splitArea: {
show: true,
},
splitNumber: 4,
type: 'value', type: 'value',
}, },
], ],

View File

@ -1,11 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui';
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
const chartRef = ref<EchartsUIType>(); const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef); const { renderEcharts } = useEcharts(chartRef);

View File

@ -1,11 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui';
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
const chartRef = ref<EchartsUIType>(); const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef); const { renderEcharts } = useEcharts(chartRef);

View File

@ -1,11 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui';
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
const chartRef = ref<EchartsUIType>(); const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef); const { renderEcharts } = useEcharts(chartRef);

View File

@ -1,11 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui';
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
const chartRef = ref<EchartsUIType>(); const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef); const { renderEcharts } = useEcharts(chartRef);

View File

@ -214,11 +214,7 @@ const trendItems: WorkbenchTrendItem[] = [
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" /> <WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" />
</div> </div>
<div class="w-full lg:w-2/5"> <div class="w-full lg:w-2/5">
<WorkbenchQuickNav <WorkbenchQuickNav :items="quickNavItems" title="快捷导航" />
:items="quickNavItems"
class="mt-5 lg:mt-0"
title="快捷导航"
/>
<WorkbenchTodo :items="todoItems" class="mt-5" title="待办事项" /> <WorkbenchTodo :items="todoItems" class="mt-5" title="待办事项" />
<AnalysisChartCard class="mt-5" title="访问来源"> <AnalysisChartCard class="mt-5" title="访问来源">
<AnalyticsVisitsSource /> <AnalyticsVisitsSource />

View File

@ -37,7 +37,7 @@ function notify(type: NotificationType) {
description="支持多语言,主题功能集成切换等" description="支持多语言,主题功能集成切换等"
title="Ant Design Vue组件使用演示" title="Ant Design Vue组件使用演示"
> >
<Card class="mb-5" title="按钮"> <Card title="按钮">
<Space> <Space>
<Button>Default</Button> <Button>Default</Button>
<Button type="primary"> Primary </Button> <Button type="primary"> Primary </Button>

View File

@ -14,6 +14,3 @@ VITE_ROUTER_HISTORY=hash
# 是否注入全局loading # 是否注入全局loading
VITE_INJECT_APP_LOADING=true VITE_INJECT_APP_LOADING=true
# 打包后是否生成dist.zip
VITE_ARCHIVER=true

View File

@ -1,6 +1,6 @@
{ {
"name": "@vben/web-ele", "name": "@vben/web-ele",
"version": "5.3.2", "version": "5.1.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": {
@ -27,27 +27,27 @@
}, },
"dependencies": { "dependencies": {
"@vben/access": "workspace:*", "@vben/access": "workspace:*",
"@vben/chart-ui": "workspace:*",
"@vben/common-ui": "workspace:*", "@vben/common-ui": "workspace:*",
"@vben/constants": "workspace:*", "@vben/constants": "workspace:*",
"@vben/hooks": "workspace:*", "@vben/hooks": "workspace:*",
"@vben/icons": "workspace:*", "@vben/icons": "workspace:*",
"@vben/layouts": "workspace:*", "@vben/layouts": "workspace:*",
"@vben/locales": "workspace:*", "@vben/locales": "workspace:*",
"@vben/plugins": "workspace:*",
"@vben/preferences": "workspace:*", "@vben/preferences": "workspace:*",
"@vben/request": "workspace:*", "@vben/request": "workspace:*",
"@vben/stores": "workspace:*", "@vben/stores": "workspace:*",
"@vben/styles": "workspace:*", "@vben/styles": "workspace:*",
"@vben/types": "workspace:*", "@vben/types": "workspace:*",
"@vben/utils": "workspace:*", "@vben/utils": "workspace:*",
"@vueuse/core": "catalog:", "@vueuse/core": "^10.11.1",
"dayjs": "catalog:", "dayjs": "^1.11.12",
"element-plus": "catalog:", "element-plus": "^2.8.0",
"pinia": "catalog:", "pinia": "2.2.1",
"vue": "catalog:", "vue": "^3.4.37",
"vue-router": "catalog:" "vue-router": "^4.4.3"
}, },
"devDependencies": { "devDependencies": {
"unplugin-element-plus": "catalog:" "unplugin-element-plus": "^0.8.0"
} }
} }

View File

@ -1,106 +0,0 @@
import type {
BaseFormComponentType,
VbenFormSchema as FormSchema,
VbenFormProps,
} from '@vben/common-ui';
import type { Component, SetupContext } from 'vue';
import { h } from 'vue';
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
import {
ElButton,
ElCheckbox,
ElCheckboxGroup,
ElDivider,
ElInput,
ElInputNumber,
ElRadioGroup,
ElSelect,
ElSpace,
ElSwitch,
ElTimePicker,
ElTreeSelect,
ElUpload,
} from 'element-plus';
// 业务表单组件适配
export type FormComponentType =
| 'Checkbox'
| 'CheckboxGroup'
| 'DatePicker'
| 'Divider'
| 'Input'
| 'InputNumber'
| 'RadioGroup'
| 'Select'
| 'Space'
| 'Switch'
| 'TimePicker'
| 'TreeSelect'
| 'Upload'
| BaseFormComponentType;
const withDefaultPlaceholder = <T extends Component>(
component: T,
type: 'input' | 'select',
) => {
return (props: any, { attrs, slots }: Omit<SetupContext, 'expose'>) => {
const placeholder = props?.placeholder || $t(`placeholder.${type}`);
return h(component, { ...props, ...attrs, placeholder }, slots);
};
};
// 初始化表单组件并注册到form组件内部
setupVbenForm<FormComponentType>({
components: {
Checkbox: ElCheckbox,
CheckboxGroup: ElCheckboxGroup,
// 自定义默认的重置按钮
DefaultResetActionButton: (props, { attrs, slots }) => {
return h(ElButton, { ...props, attrs, type: 'info' }, slots);
},
// 自定义默认的提交按钮
DefaultSubmitActionButton: (props, { attrs, slots }) => {
return h(ElButton, { ...props, attrs, type: 'primary' }, slots);
},
Divider: ElDivider,
Input: withDefaultPlaceholder(ElInput, 'input'),
InputNumber: withDefaultPlaceholder(ElInputNumber, 'input'),
RadioGroup: ElRadioGroup,
Select: withDefaultPlaceholder(ElSelect, 'select'),
Space: ElSpace,
Switch: ElSwitch,
TimePicker: ElTimePicker,
TreeSelect: withDefaultPlaceholder(ElTreeSelect, 'select'),
Upload: ElUpload,
},
config: {
modelPropNameMap: {
Upload: 'fileList',
},
},
defineRules: {
required: (value, _params, ctx) => {
if (value === undefined || value === null || value.length === 0) {
return $t('formRules.required', [ctx.label]);
}
return true;
},
selectRequired: (value, _params, ctx) => {
if (value === undefined || value === null) {
return $t('formRules.selectRequired', [ctx.label]);
}
return true;
},
},
});
const useVbenForm = useForm<FormComponentType>;
export { useVbenForm, z };
export type VbenFormSchema = FormSchema<FormComponentType>;
export type { VbenFormProps };

View File

@ -1,2 +0,0 @@
export * from './form';
export * from './vxe-table';

View File

@ -1,60 +0,0 @@
import { h } from 'vue';
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
import { ElButton, ElImage } from 'element-plus';
import { useVbenForm } from './form';
setupVbenVxeTable({
configVxeTable: (vxeUI) => {
vxeUI.setConfig({
grid: {
align: 'center',
border: true,
minHeight: 180,
proxyConfig: {
autoLoad: true,
response: {
result: 'items',
total: 'total',
list: 'items',
},
showActiveMsg: true,
showResponseMsg: false,
},
round: true,
size: 'small',
},
});
// 表格配置项可以用 cellRender: { name: 'CellImage' },
vxeUI.renderer.add('CellImage', {
renderDefault(_renderOpts, params) {
const { column, row } = params;
const src = row[column.field];
return h(ElImage, { src, previewSrcList: [src] });
},
});
// 表格配置项可以用 cellRender: { name: 'CellLink' },
vxeUI.renderer.add('CellLink', {
renderDefault(renderOpts) {
const { props } = renderOpts;
return h(
ElButton,
{ size: 'small', link: true },
{ default: () => props?.text },
);
},
});
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
// vxeUI.formats.add
},
useVbenForm,
});
export { useVbenVxeGrid };
export type * from '@vben/plugins/vxe-table';

View File

@ -1,4 +1,4 @@
import { baseRequestClient, requestClient } from '#/api/request'; import { requestClient } from '#/api/request';
export namespace AuthApi { export namespace AuthApi {
/** 登录接口参数 */ /** 登录接口参数 */
@ -10,11 +10,11 @@ export namespace AuthApi {
/** 登录接口返回值 */ /** 登录接口返回值 */
export interface LoginResult { export interface LoginResult {
accessToken: string; accessToken: string;
} desc: string;
realName: string;
export interface RefreshTokenResult { refreshToken: string;
data: string; userId: string;
status: number; username: string;
} }
} }
@ -25,24 +25,6 @@ export async function loginApi(data: AuthApi.LoginParams) {
return requestClient.post<AuthApi.LoginResult>('/auth/login', data); return requestClient.post<AuthApi.LoginResult>('/auth/login', data);
} }
/**
* accessToken
*/
export async function refreshTokenApi() {
return baseRequestClient.post<AuthApi.RefreshTokenResult>('/auth/refresh', {
withCredentials: true,
});
}
/**
* 退
*/
export async function logoutApi() {
return baseRequestClient.post('/auth/logout', {
withCredentials: true,
});
}
/** /**
* *
*/ */

View File

@ -5,105 +5,63 @@ import type { HttpResponse } from '@vben/request';
import { useAppConfig } from '@vben/hooks'; import { useAppConfig } from '@vben/hooks';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { import { RequestClient } from '@vben/request';
authenticateResponseInterceptor,
errorMessageResponseInterceptor,
RequestClient,
} from '@vben/request';
import { useAccessStore } from '@vben/stores'; import { useAccessStore } from '@vben/stores';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { useAuthStore } from '#/store'; import { useAuthStore } from '#/store';
import { refreshTokenApi } from './core';
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
function createRequestClient(baseURL: string) { function createRequestClient(baseURL: string) {
const client = new RequestClient({ const client = new RequestClient({
baseURL, baseURL,
}); // 为每个请求携带 Authorization
makeAuthorization: () => {
return {
// 默认
key: 'Authorization',
tokenHandler: () => {
const accessStore = useAccessStore();
return {
refreshToken: `${accessStore.refreshToken}`,
token: `${accessStore.accessToken}`,
};
},
unAuthorizedHandler: async () => {
const accessStore = useAccessStore();
const authStore = useAuthStore();
accessStore.setAccessToken(null);
/** if (preferences.app.loginExpiredMode === 'modal') {
* accessStore.setLoginExpired(true);
*/ } else {
async function doReAuthenticate() { // 退出登录
console.warn('Access token or refresh token is invalid or expired. '); await authStore.logout();
const accessStore = useAccessStore(); }
const authStore = useAuthStore(); },
accessStore.setAccessToken(null); };
if ( },
preferences.app.loginExpiredMode === 'modal' && makeErrorMessage: (msg) => ElMessage.error(msg),
accessStore.isAccessChecked
) { makeRequestHeaders: () => {
accessStore.setLoginExpired(true); return {
} else { // 为每个请求携带 Accept-Language
await authStore.logout(); 'Accept-Language': preferences.app.locale,
};
},
});
client.addResponseInterceptor<HttpResponse>((response) => {
const { data: responseData, status } = response;
const { code, data, message: msg } = responseData;
if (status >= 200 && status < 400 && code === 0) {
return data;
} }
} throw new Error(`Error ${status}: ${msg}`);
/**
* token逻辑
*/
async function doRefreshToken() {
const accessStore = useAccessStore();
const resp = await refreshTokenApi();
const newToken = resp.data;
accessStore.setAccessToken(newToken);
return newToken;
}
function formatToken(token: null | string) {
return token ? `Bearer ${token}` : null;
}
// 请求头处理
client.addRequestInterceptor({
fulfilled: async (config) => {
const accessStore = useAccessStore();
config.headers.Authorization = formatToken(accessStore.accessToken);
config.headers['Accept-Language'] = preferences.app.locale;
return config;
},
}); });
// response数据解构
client.addResponseInterceptor<HttpResponse>({
fulfilled: (response) => {
const { data: responseData, status } = response;
const { code, data, message: msg } = responseData;
if (status >= 200 && status < 400 && code === 0) {
return data;
}
throw new Error(`Error ${status}: ${msg}`);
},
});
// token过期的处理
client.addResponseInterceptor(
authenticateResponseInterceptor({
client,
doReAuthenticate,
doRefreshToken,
enableRefreshToken: preferences.app.enableRefreshToken,
formatToken,
}),
);
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
client.addResponseInterceptor(
errorMessageResponseInterceptor((msg: string, _error) => {
// 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
ElMessage.error(msg);
}),
);
return client; return client;
} }
export const requestClient = createRequestClient(apiURL); export const requestClient = createRequestClient(apiURL);
export const baseRequestClient = new RequestClient({ baseURL: apiURL });

View File

@ -3,7 +3,6 @@ import { createApp } from 'vue';
import { registerAccessDirective } from '@vben/access'; import { registerAccessDirective } from '@vben/access';
import { initStores } from '@vben/stores'; import { initStores } from '@vben/stores';
import '@vben/styles'; import '@vben/styles';
import '@vben/styles/ele';
import { setupI18n } from '#/locales'; import { setupI18n } from '#/locales';

View File

@ -1,23 +0,0 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { AuthPageLayout } from '@vben/layouts';
import { preferences } from '@vben/preferences';
import { $t } from '#/locales';
const appName = computed(() => preferences.app.name);
const logo = computed(() => preferences.logo.source);
</script>
<template>
<AuthPageLayout
:app-name="appName"
:logo="logo"
:page-description="$t('authentication.pageDesc')"
:page-title="$t('authentication.pageTitle')"
>
<!-- 自定义工具栏 -->
<!-- <template #toolbar></template> -->
</AuthPageLayout>
</template>

View File

@ -2,9 +2,10 @@
import type { NotificationItem } from '@vben/layouts'; import type { NotificationItem } from '@vben/layouts';
import { computed, ref } from 'vue'; import { computed, ref } 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 { LOGIN_PATH, VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons'; import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
import { import {
BasicLayout, BasicLayout,
@ -13,12 +14,17 @@ import {
UserDropdown, UserDropdown,
} from '@vben/layouts'; } from '@vben/layouts';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores'; import {
resetAllStores,
storeToRefs,
useAccessStore,
useUserStore,
} from '@vben/stores';
import { openWindow } from '@vben/utils'; import { openWindow } from '@vben/utils';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { resetRoutes } from '#/router';
import { useAuthStore } from '#/store'; import { useAuthStore } from '#/store';
import LoginForm from '#/views/_core/authentication/login.vue';
const notifications = ref<NotificationItem[]>([ const notifications = ref<NotificationItem[]>([
{ {
@ -88,12 +94,18 @@ const menus = computed(() => [
}, },
]); ]);
const { loginLoading } = storeToRefs(authStore);
const avatar = computed(() => { const avatar = computed(() => {
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar; return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
}); });
const router = useRouter();
async function handleLogout() { async function handleLogout() {
await authStore.logout(false); resetAllStores();
resetRoutes();
await router.replace(LOGIN_PATH);
} }
function handleNoticeClear() { function handleNoticeClear() {
@ -129,9 +141,11 @@ function handleMakeAll() {
<AuthenticationLoginExpiredModal <AuthenticationLoginExpiredModal
v-model:open="accessStore.loginExpired" v-model:open="accessStore.loginExpired"
:avatar :avatar
> :loading="loginLoading"
<LoginForm /> password-placeholder="123456"
</AuthenticationLoginExpiredModal> username-placeholder="vben"
@submit="authStore.authLogin"
/>
</template> </template>
<template #lock-screen> <template #lock-screen>
<LockScreen :avatar @to-login="handleLogout" /> <LockScreen :avatar @to-login="handleLogout" />

View File

@ -1,6 +1,8 @@
const BasicLayout = () => import('./basic.vue'); const BasicLayout = () => import('./basic.vue');
const AuthPageLayout = () => import('./auth.vue');
const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView); const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);
const AuthPageLayout = () =>
import('@vben/layouts').then((m) => m.AuthPageLayout);
export { AuthPageLayout, BasicLayout, IFrameView }; export { AuthPageLayout, BasicLayout, IFrameView };

View File

@ -91,4 +91,4 @@ async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
}); });
} }
export { $t, elementLocale, setupI18n }; export { $t, elementLocale, loadMessages, setupI18n };

View File

@ -92,8 +92,10 @@ function setupAccessGuard(router: Router) {
return to; return to;
} }
const accessRoutes = accessStore.accessRoutes;
// 是否已经生成过动态路由 // 是否已经生成过动态路由
if (accessStore.isAccessChecked) { if (accessRoutes && accessRoutes.length > 0) {
return true; return true;
} }
@ -113,7 +115,6 @@ function setupAccessGuard(router: Router) {
// 保存菜单信息和路由信息 // 保存菜单信息和路由信息
accessStore.setAccessMenus(accessibleMenus); accessStore.setAccessMenus(accessibleMenus);
accessStore.setAccessRoutes(accessibleRoutes); accessStore.setAccessRoutes(accessibleRoutes);
accessStore.setIsAccessChecked(true);
const redirectPath = (from.query.redirect ?? to.fullPath) as string; const redirectPath = (from.query.redirect ?? to.fullPath) as string;
return { return {

View File

@ -19,12 +19,7 @@ const router = createRouter({
: createWebHistory(import.meta.env.VITE_BASE), : createWebHistory(import.meta.env.VITE_BASE),
// 应该添加到路由的初始路由列表。 // 应该添加到路由的初始路由列表。
routes, routes,
scrollBehavior: (to, _from, savedPosition) => { scrollBehavior: () => ({ left: 0, top: 0 }),
if (savedPosition) {
return savedPosition;
}
return to.hash ? { behavior: 'smooth', el: to.hash } : { left: 0, top: 0 };
},
// 是否应该禁止尾部斜杠。 // 是否应该禁止尾部斜杠。
// strict: true, // strict: true,
}); });

View File

@ -29,7 +29,6 @@ const routes: RouteRecordRaw[] = [
path: '/workspace', path: '/workspace',
component: () => import('#/views/dashboard/workspace/index.vue'), component: () => import('#/views/dashboard/workspace/index.vue'),
meta: { meta: {
icon: 'carbon:workspace',
title: $t('page.dashboard.workspace'), title: $t('page.dashboard.workspace'),
}, },
}, },

View File

@ -7,7 +7,6 @@ import {
VBEN_LOGO_URL, VBEN_LOGO_URL,
VBEN_NAIVE_PREVIEW_URL, VBEN_NAIVE_PREVIEW_URL,
} from '@vben/constants'; } from '@vben/constants';
import { SvgAntdvLogoIcon } from '@vben/icons';
import { BasicLayout, IFrameView } from '#/layouts'; import { BasicLayout, IFrameView } from '#/layouts';
import { $t } from '#/locales'; import { $t } from '#/locales';
@ -27,7 +26,7 @@ const routes: RouteRecordRaw[] = [
{ {
name: 'VbenAbout', name: 'VbenAbout',
path: '/vben-admin/about', path: '/vben-admin/about',
component: () => import('#/views/_core/about/index.vue'), component: () => import('#/views/_core/vben/about/index.vue'),
meta: { meta: {
icon: 'lucide:copyright', icon: 'lucide:copyright',
title: $t('page.vben.about'), title: $t('page.vben.about'),
@ -59,7 +58,6 @@ const routes: RouteRecordRaw[] = [
component: IFrameView, component: IFrameView,
meta: { meta: {
badgeType: 'dot', badgeType: 'dot',
icon: 'logos:naiveui',
link: VBEN_NAIVE_PREVIEW_URL, link: VBEN_NAIVE_PREVIEW_URL,
title: $t('page.vben.naive-ui'), title: $t('page.vben.naive-ui'),
}, },
@ -70,7 +68,6 @@ const routes: RouteRecordRaw[] = [
component: IFrameView, component: IFrameView,
meta: { meta: {
badgeType: 'dot', badgeType: 'dot',
icon: SvgAntdvLogoIcon,
link: VBEN_ANT_PREVIEW_URL, link: VBEN_ANT_PREVIEW_URL,
title: $t('page.vben.antdv'), title: $t('page.vben.antdv'),
}, },

View File

@ -10,7 +10,7 @@ import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
import { ElNotification } from 'element-plus'; import { ElNotification } from 'element-plus';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api'; import { getAccessCodesApi, getUserInfoApi, loginApi } from '#/api';
import { $t } from '#/locales'; import { $t } from '#/locales';
export const useAuthStore = defineStore('auth', () => { export const useAuthStore = defineStore('auth', () => {
@ -33,12 +33,13 @@ export const useAuthStore = defineStore('auth', () => {
let userInfo: null | UserInfo = null; let userInfo: null | UserInfo = null;
try { try {
loginLoading.value = true; loginLoading.value = true;
const { accessToken } = await loginApi(params); const { accessToken, refreshToken } = await loginApi(params);
// 如果成功获取到 accessToken // 如果成功获取到 accessToken
if (accessToken) { if (accessToken) {
// 将 accessToken 存储到 accessStore 中 // 将 accessToken 存储到 accessStore 中
accessStore.setAccessToken(accessToken); accessStore.setAccessToken(accessToken);
accessStore.setRefreshToken(refreshToken);
// 获取用户信息并存储到 accessStore 中 // 获取用户信息并存储到 accessStore 中
const [fetchUserInfoResult, accessCodes] = await Promise.all([ const [fetchUserInfoResult, accessCodes] = await Promise.all([
@ -76,23 +77,16 @@ export const useAuthStore = defineStore('auth', () => {
}; };
} }
async function logout(redirect: boolean = true) { async function logout() {
try {
await logoutApi();
} catch {
// 不做任何处理
}
resetAllStores(); resetAllStores();
accessStore.setLoginExpired(false); accessStore.setLoginExpired(false);
// 回登陆页带上当前路由地址 // 回登陆页带上当前路由地址
await router.replace({ await router.replace({
path: LOGIN_PATH, path: LOGIN_PATH,
query: redirect query: {
? { redirect: encodeURIComponent(router.currentRoute.value.fullPath),
redirect: encodeURIComponent(router.currentRoute.value.fullPath), },
}
: {},
}); });
} }

View File

@ -1,49 +1,15 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { LoginCodeParams, VbenFormSchema } from '@vben/common-ui'; import type { LoginCodeParams } from '@vben/common-ui';
import { computed, ref } from 'vue'; import { ref } from 'vue';
import { AuthenticationCodeLogin, z } from '@vben/common-ui'; import { AuthenticationCodeLogin } from '@vben/common-ui';
import { $t } from '@vben/locales'; import { LOGIN_PATH } from '@vben/constants';
defineOptions({ name: 'CodeLogin' }); defineOptions({ name: 'CodeLogin' });
const loading = ref(false); const loading = ref(false);
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenInput',
componentProps: {
placeholder: $t('authentication.mobile'),
},
fieldName: 'phoneNumber',
label: $t('authentication.mobile'),
rules: z
.string()
.min(1, { message: $t('authentication.mobileTip') })
.refine((v) => /^\d{11}$/.test(v), {
message: $t('authentication.mobileErrortip'),
}),
},
{
component: 'VbenPinInput',
componentProps: {
createText: (countdown: number) => {
const text =
countdown > 0
? $t('authentication.sendText', [countdown])
: $t('authentication.sendCode');
return text;
},
placeholder: $t('authentication.code'),
},
fieldName: 'code',
label: $t('authentication.code'),
rules: z.string().min(1, { message: $t('authentication.codeTip') }),
},
];
});
/** /**
* 异步处理登录操作 * 异步处理登录操作
* Asynchronously handle the login process * Asynchronously handle the login process
@ -57,8 +23,8 @@ async function handleLogin(values: LoginCodeParams) {
<template> <template>
<AuthenticationCodeLogin <AuthenticationCodeLogin
:form-schema="formSchema"
:loading="loading" :loading="loading"
:login-path="LOGIN_PATH"
@submit="handleLogin" @submit="handleLogin"
/> />
</template> </template>

View File

@ -1,32 +1,13 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VbenFormSchema } from '@vben/common-ui'; import { ref } from 'vue';
import { computed, ref } from 'vue'; import { AuthenticationForgetPassword } from '@vben/common-ui';
import { LOGIN_PATH } from '@vben/constants';
import { AuthenticationForgetPassword, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
defineOptions({ name: 'ForgetPassword' }); defineOptions({ name: 'ForgetPassword' });
const loading = ref(false); const loading = ref(false);
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenInput',
componentProps: {
placeholder: 'example@example.com',
},
fieldName: 'email',
label: $t('authentication.email'),
rules: z
.string()
.min(1, { message: $t('authentication.emailTip') })
.email($t('authentication.emailValidErrorTip')),
},
];
});
function handleSubmit(value: string) { function handleSubmit(value: string) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('reset email:', value); console.log('reset email:', value);
@ -35,8 +16,8 @@ function handleSubmit(value: string) {
<template> <template>
<AuthenticationForgetPassword <AuthenticationForgetPassword
:form-schema="formSchema"
:loading="loading" :loading="loading"
:login-path="LOGIN_PATH"
@submit="handleSubmit" @submit="handleSubmit"
/> />
</template> </template>

View File

@ -1,98 +1,18 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VbenFormSchema } from '@vben/common-ui'; import { AuthenticationLogin } from '@vben/common-ui';
import type { BasicOption } from '@vben/types';
import { computed, markRaw } from 'vue';
import { AuthenticationLogin, SliderCaptcha, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { useAuthStore } from '#/store'; import { useAuthStore } from '#/store';
defineOptions({ name: 'Login' }); defineOptions({ name: 'Login' });
const authStore = useAuthStore(); const authStore = useAuthStore();
const MOCK_USER_OPTIONS: BasicOption[] = [
{
label: 'Super',
value: 'vben',
},
{
label: 'Admin',
value: 'admin',
},
{
label: 'User',
value: 'jack',
},
];
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenSelect',
componentProps: {
options: MOCK_USER_OPTIONS,
placeholder: $t('authentication.selectAccount'),
},
fieldName: 'selectAccount',
label: $t('authentication.selectAccount'),
rules: z
.string()
.min(1, { message: $t('authentication.selectAccount') })
.optional()
.default('vben'),
},
{
component: 'VbenInput',
componentProps: {
placeholder: $t('authentication.usernameTip'),
},
dependencies: {
trigger(values, form) {
if (values.selectAccount) {
const findUser = MOCK_USER_OPTIONS.find(
(item) => item.value === values.selectAccount,
);
if (findUser) {
form.setValues({
password: '123456',
username: findUser.value,
});
}
}
},
triggerFields: ['selectAccount'],
},
fieldName: 'username',
label: $t('authentication.username'),
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
},
{
component: 'VbenInputPassword',
componentProps: {
placeholder: $t('authentication.password'),
},
fieldName: 'password',
label: $t('authentication.password'),
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
},
{
component: markRaw(SliderCaptcha),
fieldName: 'captcha',
rules: z.boolean().refine((value) => value, {
message: $t('authentication.verifyRequiredTip'),
}),
},
];
});
</script> </script>
<template> <template>
<AuthenticationLogin <AuthenticationLogin
:form-schema="formSchema"
:loading="authStore.loginLoading" :loading="authStore.loginLoading"
password-placeholder="123456"
username-placeholder="vben"
@submit="authStore.authLogin" @submit="authStore.authLogin"
/> />
</template> </template>

View File

@ -1,91 +1,15 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { LoginAndRegisterParams, VbenFormSchema } from '@vben/common-ui'; import type { LoginAndRegisterParams } from '@vben/common-ui';
import { computed, h, ref } from 'vue'; import { ref } from 'vue';
import { AuthenticationRegister, z } from '@vben/common-ui'; import { AuthenticationRegister } from '@vben/common-ui';
import { $t } from '@vben/locales'; import { LOGIN_PATH } from '@vben/constants';
defineOptions({ name: 'Register' }); defineOptions({ name: 'Register' });
const loading = ref(false); const loading = ref(false);
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenInput',
componentProps: {
placeholder: $t('authentication.usernameTip'),
},
fieldName: 'username',
label: $t('authentication.username'),
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
},
{
component: 'VbenInputPassword',
componentProps: {
passwordStrength: true,
placeholder: $t('authentication.password'),
},
fieldName: 'password',
label: $t('authentication.password'),
renderComponentContent() {
return {
strengthText: () => $t('authentication.passwordStrength'),
};
},
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
},
{
component: 'VbenInputPassword',
componentProps: {
placeholder: $t('authentication.confirmPassword'),
},
dependencies: {
rules(values) {
const { password } = values;
return z
.string()
.min(1, { message: $t('authentication.passwordTip') })
.refine((value) => value === password, {
message: $t('authentication.confirmPasswordTip'),
});
},
triggerFields: ['password'],
},
fieldName: 'confirmPassword',
label: $t('authentication.confirmPassword'),
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
},
{
component: 'VbenCheckbox',
fieldName: 'agreePolicy',
renderComponentContent: () => ({
default: () =>
h('span', [
$t('authentication.agree'),
h(
'a',
{
class:
'cursor-pointer text-primary ml-1 hover:text-primary-hover',
href: '',
},
[
$t('authentication.privacyPolicy'),
'&',
$t('authentication.terms'),
],
),
]),
}),
rules: z.boolean().refine((value) => !!value, {
message: $t('authentication.agreeTip'),
}),
},
];
});
function handleSubmit(value: LoginAndRegisterParams) { function handleSubmit(value: LoginAndRegisterParams) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('register submit:', value); console.log('register submit:', value);
@ -94,8 +18,8 @@ function handleSubmit(value: LoginAndRegisterParams) {
<template> <template>
<AuthenticationRegister <AuthenticationRegister
:form-schema="formSchema"
:loading="loading" :loading="loading"
:login-path="LOGIN_PATH"
@submit="handleSubmit" @submit="handleSubmit"
/> />
</template> </template>

View File

@ -1,11 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui';
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
const chartRef = ref<EchartsUIType>(); const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef); const { renderEcharts } = useEcharts(chartRef);
@ -55,27 +51,12 @@ onMounted(() => {
}, },
trigger: 'axis', trigger: 'axis',
}, },
// xAxis: {
// axisTick: {
// show: false,
// },
// boundaryGap: false,
// data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
// type: 'category',
// },
xAxis: { xAxis: {
axisTick: { axisTick: {
show: false, show: false,
}, },
boundaryGap: false, boundaryGap: false,
data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`), data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
splitLine: {
lineStyle: {
type: 'solid',
width: 1,
},
show: true,
},
type: 'category', type: 'category',
}, },
yAxis: [ yAxis: [
@ -84,10 +65,7 @@ onMounted(() => {
show: false, show: false,
}, },
max: 80_000, max: 80_000,
splitArea: {
show: true,
},
splitNumber: 4,
type: 'value', type: 'value',
}, },
], ],

View File

@ -1,11 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui';
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
const chartRef = ref<EchartsUIType>(); const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef); const { renderEcharts } = useEcharts(chartRef);

View File

@ -1,11 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui';
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
const chartRef = ref<EchartsUIType>(); const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef); const { renderEcharts } = useEcharts(chartRef);

View File

@ -1,11 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui';
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
const chartRef = ref<EchartsUIType>(); const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef); const { renderEcharts } = useEcharts(chartRef);

View File

@ -1,11 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui';
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
const chartRef = ref<EchartsUIType>(); const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef); const { renderEcharts } = useEcharts(chartRef);

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