Compare commits

..

1 Commits
main ... doc

Author SHA1 Message Date
jinmao dc2b8f00e0 chore: 添加3群链接 2024-08-07 16:21:34 +08:00
957 changed files with 10489 additions and 38988 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,42 +1,5 @@
# Vben Admin Contributing Guide # Contributing Guide
Hi! We're really excited that you are interested in contributing to Vben Admin. Before submitting your contribution, please make sure to take a moment and read through the following guidelines: 1. Make sure you put things in the right category!
2. Always add your items to the end of a list. To be fair, the order is first-come-first-serve.
- [Pull Request Guidelines](#pull-request-guidelines) 3. If you think something belongs in the wrong category, or think there needs to be a new category, feel free to edit things too.
## Contributor Code of Conduct
As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
We are committed to making participation in this project a harassment-free experience for everyone, regardless of the level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion.
Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
## Pull Request Guidelines
- Checkout a topic branch from the relevant branch, e.g. main, and merge back against that branch.
- If adding a new feature:
- Provide a convincing reason to add this feature. Ideally, you should open a suggestion issue first and have it approved before working on it.
- If fixing bug:
- Provide a detailed description of the bug in the PR. Live demo preferred.
- It's OK to have multiple small commits as you work on the PR - GitHub can automatically squash them before merging.
## Development Setup
You will need [pnpm](https://pnpm.io/)
After cloning the repo, run:
```bash
# install the dependencies of the project
$ pnpm install
# start the project
$ pnpm run dev
```

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,46 @@ 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: 📝 Documentation updates
labels: labels:
- 'perf' - "documentation"
- 'enhancement'
- title: 📝 Documentation
labels:
- 'documentation'
- title: 👻 Maintenance - title: 👻 Maintenance
labels: labels:
- 'chore' - "chore"
- 'dependencies' - "dependencies"
# collapse-after: 12 collapse-after: 5
- 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' - "breaking"
- 'breaking'
minor: minor:
labels: labels:
- 'minor' - "feature"
patch: patch:
labels: labels:
- 'feature' - "bug"
- 'patch' - "maintenance"
- 'bug' - "docs"
- 'maintenance' - "dependencies"
- 'docs' - "security"
- 'dependencies'
- '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,64 +6,9 @@ on:
- main - main
jobs: jobs:
deploy-playground-ftp: deploy-push-ftp:
name: Deploy Push Playground Ftp name: Deploy Push 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
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Sed Config Base
shell: bash
run: |
sed -i "s#VITE_COMPRESS\s*=.*#VITE_COMPRESS = gzip#g" ./playground/.env.production
sed -i "s#VITE_PWA\s*=.*#VITE_PWA = true#g" ./playground/.env.production
cat ./playground/.env.production
- name: Setup Node
uses: ./.github/actions/setup-node
- name: Build
run: pnpm build:play
- name: Sync Playground files
uses: SamKirkland/FTP-Deploy-Action@v4.3.5
with:
server: ${{ secrets.PRO_FTP_HOST }}
username: ${{ secrets.WEB_PLAYGROUND_FTP_ACCOUNT }}
password: ${{ secrets.WEB_PLAYGROUND_FTP_PWSSWORD }}
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
uses: SamKirkland/FTP-Deploy-Action@v4.3.5
with:
server: ${{ secrets.PRO_FTP_HOST }}
username: ${{ secrets.WEBSITE_FTP_ACCOUNT }}
password: ${{ secrets.WEBSITE_FTP_PASSWORD }}
local-dir: ./docs/.vitepress/dist/
deploy-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'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
@ -77,65 +22,9 @@ jobs:
sed -i "s#VITE_COMPRESS\s*=.*#VITE_COMPRESS = gzip#g" ./apps/web-antd/.env.production sed -i "s#VITE_COMPRESS\s*=.*#VITE_COMPRESS = gzip#g" ./apps/web-antd/.env.production
sed -i "s#VITE_PWA\s*=.*#VITE_PWA = true#g" ./apps/web-antd/.env.production sed -i "s#VITE_PWA\s*=.*#VITE_PWA = true#g" ./apps/web-antd/.env.production
cat ./apps/web-antd/.env.production cat ./apps/web-antd/.env.production
- name: Setup Node
uses: ./.github/actions/setup-node
- name: Build
run: pnpm run build:antd
- name: Sync files
uses: SamKirkland/FTP-Deploy-Action@v4.3.5
with:
server: ${{ secrets.PRO_FTP_HOST }}
username: ${{ secrets.WEB_ANTD_FTP_ACCOUNT }}
password: ${{ secrets.WEB_ANTD_FTP_PASSWORD }}
local-dir: ./apps/web-antd/dist/
deploy-ele-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'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Sed Config Base
shell: bash
run: |
sed -i "s#VITE_COMPRESS\s*=.*#VITE_COMPRESS = gzip#g" ./apps/web-ele/.env.production sed -i "s#VITE_COMPRESS\s*=.*#VITE_COMPRESS = gzip#g" ./apps/web-ele/.env.production
sed -i "s#VITE_PWA\s*=.*#VITE_PWA = true#g" ./apps/web-ele/.env.production sed -i "s#VITE_PWA\s*=.*#VITE_PWA = true#g" ./apps/web-ele/.env.production
cat ./apps/web-ele/.env.production cat ./apps/web-ele/.env.production
- name: Setup Node
uses: ./.github/actions/setup-node
- name: Build
run: pnpm run build:ele
- name: Sync files
uses: SamKirkland/FTP-Deploy-Action@v4.3.5
with:
server: ${{ secrets.PRO_FTP_HOST }}
username: ${{ secrets.WEB_ELE_FTP_ACCOUNT }}
password: ${{ secrets.WEB_ELE_FTP_PASSWORD }}
local-dir: ./apps/web-ele/dist/
deploy-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'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Sed Config Base
shell: bash
run: |
sed -i "s#VITE_COMPRESS\s*=.*#VITE_COMPRESS = gzip#g" ./apps/web-naive/.env.production sed -i "s#VITE_COMPRESS\s*=.*#VITE_COMPRESS = gzip#g" ./apps/web-naive/.env.production
sed -i "s#VITE_PWA\s*=.*#VITE_PWA = true#g" ./apps/web-naive/.env.production sed -i "s#VITE_PWA\s*=.*#VITE_PWA = true#g" ./apps/web-naive/.env.production
cat ./apps/web-naive/.env.production cat ./apps/web-naive/.env.production
@ -144,12 +33,36 @@ 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 Web Antd files
uses: SamKirkland/FTP-Deploy-Action@v4.3.5
with:
server: ${{ secrets.PRO_FTP_HOST }}
username: ${{ secrets.WEB_ANTD_FTP_ACCOUNT }}
password: ${{ secrets.WEB_ANTD_FTP_PASSWORD }}
local-dir: ./apps/web-antd/dist/
- name: Sync Web Naive files
uses: SamKirkland/FTP-Deploy-Action@v4.3.5 uses: SamKirkland/FTP-Deploy-Action@v4.3.5
with: with:
server: ${{ secrets.PRO_FTP_HOST }} server: ${{ secrets.PRO_FTP_HOST }}
username: ${{ secrets.WEB_NAIVE_FTP_ACCOUNT }} username: ${{ secrets.WEB_NAIVE_FTP_ACCOUNT }}
password: ${{ secrets.WEB_NAIVE_FTP_PASSWORD }} password: ${{ secrets.WEB_NAIVE_FTP_PASSWORD }}
local-dir: ./apps/web-naive/dist/ local-dir: ./apps/web-naive/dist/
- name: Sync Web Ele files
uses: SamKirkland/FTP-Deploy-Action@v4.3.5
with:
server: ${{ secrets.PRO_FTP_HOST }}
username: ${{ secrets.WEB_ELE_FTP_ACCOUNT }}
password: ${{ secrets.WEB_ELE_FTP_PASSWORD }}
local-dir: ./apps/web-ele/dist/
- name: Sync Docs files
uses: SamKirkland/FTP-Deploy-Action@v4.3.5
with:
server: ${{ secrets.PRO_FTP_HOST }}
username: ${{ secrets.WEBSITE_FTP_ACCOUNT }}
password: ${{ secrets.WEBSITE_FTP_PASSWORD }}
local-dir: ./docs/.vitepress/dist/

View File

@ -7,17 +7,9 @@ on:
permissions: permissions:
contents: read contents: read
pull-requests: write
jobs: jobs:
update_release_draft: update_release_draft:
permissions:
# write permission is required to create a github release
contents: write
# write permission is required for autolabeler
# otherwise, read permission is required at least
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

View File

@ -10,6 +10,10 @@
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
// dotenv // dotenv
"mikestead.dotenv", "mikestead.dotenv",
// CSS
"dzhavat.css-initial-value",
// 使 VSCode TypeScript
"yoavbls.pretty-ts-errors",
// //
"streetsidesoftware.code-spell-checker", "streetsidesoftware.code-spell-checker",
// Tailwind CSS VS Code // Tailwind CSS VS Code

31
.vscode/launch.json vendored
View File

@ -4,39 +4,12 @@
"configurations": [ "configurations": [
{ {
"type": "chrome", "type": "chrome",
"name": "vben admin playground dev", "name": "vben admin antd dev",
"request": "launch", "request": "launch",
"url": "http://localhost:5555", "url": "http://localhost:5555",
"env": { "NODE_ENV": "development" }, "env": { "NODE_ENV": "development" },
"sourceMaps": true, "sourceMaps": true,
"webRoot": "${workspaceFolder}" "webRoot": "${workspaceFolder}/apps/web-antd/src"
},
{
"type": "chrome",
"name": "vben admin antd dev",
"request": "launch",
"url": "http://localhost:5666",
"env": { "NODE_ENV": "development" },
"sourceMaps": true,
"webRoot": "${workspaceFolder}"
},
{
"type": "chrome",
"name": "vben admin ele dev",
"request": "launch",
"url": "http://localhost:5777",
"env": { "NODE_ENV": "development" },
"sourceMaps": true,
"webRoot": "${workspaceFolder}"
},
{
"type": "chrome",
"name": "vben admin naive dev",
"request": "launch",
"url": "http://localhost:5888",
"env": { "NODE_ENV": "development" },
"sourceMaps": true,
"webRoot": "${workspaceFolder}"
} }
] ]
} }

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,8 @@
"i18n-ally.localesPaths": [ "i18n-ally.localesPaths": [
"packages/locales/src/langs", "packages/locales/src/langs",
"playground/src/locales/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 +183,11 @@
"*.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*",
"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,
"typescript.tsdk": "node_modules/typescript/lib"
} }

View File

@ -21,9 +21,9 @@ RUN echo "Builder Success 🎉"
FROM nginx:stable-alpine as production FROM nginx:stable-alpine as production
RUN echo "types { application/javascript js mjs; }" > /etc/nginx/conf.d/mjs.conf RUN echo "types { application/javascript js mjs; }" > /etc/nginx/conf.d/mjs.conf
COPY --from=builder /app/playground/dist /usr/share/nginx/html COPY --from=builder /app/apps/web-antd/dist /usr/share/nginx/html
COPY ./nginx.conf /etc/nginx/nginx.conf COPY ./deploy/nginx.conf /etc/nginx/nginx.conf
EXPOSE 8080 EXPOSE 8080

View File

@ -1,153 +0,0 @@
<div align="center"> <a href="https://github.com/anncwb/vue-vben-admin"> <img alt="VbenAdmin Logo" width="215" src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp"> </a> <br> <br>
[![license](https://img.shields.io/github/license/anncwb/vue-vben-admin.svg)](LICENSE)
<h1>Vue Vben Admin</h1>
</div>
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=vbenjs_vue-vben-admin&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin) ![codeql](https://github.com/vbenjs/vue-vben-admin/actions/workflows/codeql.yml/badge.svg) ![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)
## 紹介
Vue Vben Adminは、最新の`vue3`、`vite`、`TypeScript`などの主流技術を使用して開発された、無料でオープンソースの中・後端テンプレートです。すぐに使える中・後端のフロントエンドソリューションとして、学習の参考にもなります。
## アップグレード通知
これは最新バージョン5.0であり、以前のバージョンとは互換性がありません。新しいプロジェクトを開始する場合は、最新バージョンを使用することをお勧めします。古いバージョンを表示したい場合は、[v2ブランチ](https://github.com/vbenjs/vue-vben-admin/tree/v2)を使用してください。
## 特徴
- **最新技術スタック**: Vue 3やViteなどの最先端フロントエンド技術で開発
- **TypeScript**: アプリケーション規模のJavaScriptのための言語
- **テーマ**: 複数のテーマカラーが利用可能で、カスタマイズオプションも豊富
- **国際化**: 完全な内蔵国際化サポート
- **権限管理**: 動的ルートベースの権限生成ソリューションを内蔵
## プレビュー
- [Vben Admin](https://vben.pro/) - フルバージョンの中国語サイト
テストアカウント: vben/123456
<p align="center">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview1.png">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview2.png">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview3.png">
</p>
### Gitpodを使用
GitpodGitHub用の無料オンライン開発環境でプロジェクトを開き、すぐにコーディングを開始します。
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/vbenjs/vue-vben-admin)
## ドキュメント
[ドキュメント](https://doc.vben.pro/)
## インストールと使用
- プロジェクトコードを取得
```bash
git clone https://github.com/vbenjs/vue-vben-admin.git
```
- 依存関係のインストール
```bash
cd vue-vben-admin
corepack enable
pnpm install
```
- 実行
```bash
pnpm dev
```
- ビルド
```bash
pnpm build
```
## 変更ログ
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases)
## 貢献方法
ご参加をお待ちしております![Issueを提出](https://github.com/anncwb/vue-vben-admin/issues/new/choose)するか、Pull Requestを送信してください。
**Pull Request:**
1. コードをフォーク!
2. 自分のブランチを作成: `git checkout -b feat/xxxx`
3. 変更をコミット: `git commit -am 'feat(function): add xxxxx'`
4. ブランチをプッシュ: `git push origin feat/xxxx`
5. `pull request`を送信
## Git貢献提出規則
- 参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 規則 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
- `feat` 新機能の追加
- `fix` 問題/バグの修正
- `style` コードスタイルに関連し、実行結果に影響しない
- `perf` 最適化/パフォーマンス向上
- `refactor` リファクタリング
- `revert` 変更の取り消し
- `test` テスト関連
- `docs` ドキュメント/注釈
- `chore` 依存関係の更新/スキャフォールディング設定の変更など
- `ci` 継続的インテグレーション
- `types` 型定義ファイルの変更
- `wip` 開発中
## ブラウザサポート
ローカル開発には`Chrome 80+`ブラウザを推奨します
モダンブラウザをサポートし、IEはサポートしません
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
| :-: | :-: | :-: | :-: | :-: |
| サポートしない | 最新2バージョン | 最新2バージョン | 最新2バージョン | 最新2バージョン |
## メンテナー
[@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)
<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 href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
<img alt="Contributors"
src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
</a>
## Discord
- [Github Discussions](https://github.com/anncwb/vue-vben-admin/discussions)
## ライセンス
[MIT © Vben-2020](./LICENSE)

View File

@ -1,13 +1,11 @@
<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="200" height="200" src="https://unpkg.com/@vbenjs/static-source@0.1.5/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)
**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,23 +122,18 @@ 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.5/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>
## Contributor ## Contributor
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors"> <a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
<img alt="Contributors" <img src="https://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
</a> </a>
## Discord ## Discord

View File

@ -1,13 +1,11 @@
<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="200" height="200" src="https://unpkg.com/@vbenjs/static-source@0.1.5/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)
**中文** | [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,25 +118,14 @@ 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.5/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>
## Contributor
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
<img alt="Contributors"
src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
</a>
## Discord ## Discord
- [Github Discussions](https://github.com/anncwb/vue-vben-admin/discussions) - [Github Discussions](https://github.com/anncwb/vue-vben-admin/discussions)

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 服务,没有对接任何的数据库,所有数据都是模拟的,用于前端开发时提供数据支持。由于 sqlite 安装需要在本地进行编译,所以这里接口是直接返回的。线上环境不再提供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

@ -1,5 +1,5 @@
# 应用标题 # 应用标题
VITE_APP_TITLE=Vben Admin Antd VITE_APP_TITLE=Vben Admin
# 应用命名空间用于缓存、store等功能的前缀确保隔离 # 应用命名空间用于缓存、store等功能的前缀确保隔离
VITE_APP_NAMESPACE=vben-web-antd VITE_APP_NAMESPACE=vben-web-antd

View File

@ -1,5 +1,5 @@
# 端口号 # 端口号
VITE_PORT=5666 VITE_PORT=5555
VITE_BASE=/ VITE_BASE=/

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

@ -21,7 +21,7 @@
(function () { (function () {
var hm = document.createElement('script'); var hm = document.createElement('script');
hm.src = hm.src =
'https://hm.baidu.com/hm.js?b38e689f40558f20a9a686d7f6f33edf'; 'https://hm.baidu.com/hm.js?d20a01273820422b6aa2ee41b6c9414d';
var s = document.getElementsByTagName('script')[0]; var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(hm, s); s.parentNode.insertBefore(hm, s);
})(); })();

View File

@ -1,6 +1,6 @@
{ {
"name": "@vben/web-antd", "name": "@vben/web-antd",
"version": "5.3.2", "version": "5.0.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.0",
"ant-design-vue": "catalog:", "ant-design-vue": "^4.2.3",
"dayjs": "catalog:", "dayjs": "^1.11.12",
"pinia": "catalog:", "pinia": "2.2.0",
"vue": "catalog:", "vue": "^3.4.35",
"vue-router": "catalog:" "vue-router": "^4.4.2"
} }
} }

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

@ -1,2 +1 @@
export * from './status'; export * from './status';
export * from './table';

View File

@ -1 +1,2 @@
export * from './core'; export * from './core';
export * from './demos';

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

@ -58,11 +58,7 @@ async function loadDayjsLocale(lang: SupportedLanguagesType) {
locale = await import('dayjs/locale/en'); locale = await import('dayjs/locale/en');
} }
} }
if (locale) { dayjs.locale(locale);
dayjs.locale(locale);
} else {
console.error(`Failed to load dayjs locale for ${lang}`);
}
} }
/** /**
@ -91,4 +87,4 @@ async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
}); });
} }
export { $t, antdLocale, setupI18n }; export { $t, antdLocale, loadMessages, setupI18n };

View File

@ -2,7 +2,58 @@
"page": { "page": {
"demos": { "demos": {
"title": "Demos", "title": "Demos",
"antd": "Ant Design Vue" "access": {
"frontendPermissions": "Frontend Permissions",
"backendPermissions": "Backend Permissions",
"pageAccess": "Page Access",
"buttonControl": "Button Control",
"menuVisible403": "Menu Visible(403)",
"superVisible": "Visible to Super",
"adminVisible": "Visible to Admin",
"userVisible": "Visible to User"
},
"nested": {
"title": "Nested Menu",
"menu1": "Menu 1",
"menu2": "Menu 2",
"menu2_1": "Menu 2-1",
"menu3": "Menu 3",
"menu3_1": "Menu 3-1",
"menu3_2": "Menu 3-2",
"menu3_2_1": "Menu 3-2-1"
},
"outside": {
"title": "External Pages",
"embedded": "Embedded",
"externalLink": "External Link"
},
"badge": {
"title": "Menu Badge",
"dot": "Dot Badge",
"text": "Text Badge",
"color": "Badge Color"
},
"activeIcon": {
"title": "Active Menu Icon",
"children": "Children Active Icon"
},
"fallback": { "title": "Fallback Page" },
"features": {
"title": "Features",
"hideChildrenInMenu": "Hide Menu Children",
"loginExpired": "Login Expired",
"icons": "Icons",
"watermark": "Watermark",
"tabs": "Tabs",
"tabDetail": "Tab Detail Page"
},
"breadcrumb": {
"navigation": "Breadcrumb Navigation",
"lateral": "Lateral Mode",
"lateralDetail": "Lateral Mode Detail",
"level": "Level Mode",
"levelDetail": "Level Mode Detail"
}
} }
} }
} }

View File

@ -2,7 +2,60 @@
"page": { "page": {
"demos": { "demos": {
"title": "演示", "title": "演示",
"antd": "Ant Design Vue" "access": {
"frontendPermissions": "前端权限",
"backendPermissions": "后端权限",
"pageAccess": "页面访问",
"buttonControl": "按钮控制",
"menuVisible403": "菜单可见(403)",
"superVisible": "Super 可见",
"adminVisible": "Admin 可见",
"userVisible": "User 可见"
},
"nested": {
"title": "嵌套菜单",
"menu1": "菜单 1",
"menu2": "菜单 2",
"menu2_1": "菜单 2-1",
"menu3": "菜单 3",
"menu3_1": "菜单 3-1",
"menu3_2": "菜单 3-2",
"menu3_2_1": "菜单 3-2-1"
},
"outside": {
"title": "外部页面",
"embedded": "内嵌",
"externalLink": "外链"
},
"badge": {
"title": "菜单徽标",
"dot": "点徽标",
"text": "文本徽标",
"color": "徽标颜色"
},
"activeIcon": {
"title": "菜单激活图标",
"children": "子级激活图标"
},
"fallback": {
"title": "缺省页"
},
"features": {
"title": "功能",
"hideChildrenInMenu": "隐藏子菜单",
"loginExpired": "登录过期",
"icons": "图标",
"watermark": "水印",
"tabs": "标签页",
"tabDetail": "标签详情页"
},
"breadcrumb": {
"navigation": "面包屑导航",
"lateral": "平级模式",
"level": "层级模式",
"levelDetail": "层级模式详情",
"lateralDetail": "平级模式详情"
}
} }
} }
} }

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,11 +115,10 @@ 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.path) as string;
const redirectPath = (from.query.redirect ?? to.fullPath) as string;
return { return {
...router.resolve(decodeURIComponent(redirectPath)), path: decodeURIComponent(redirectPath),
replace: true, replace: true,
}; };
}); });

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

@ -9,19 +9,19 @@ const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
}); });
// 有需要可以自行打开注释,并创建文件夹 // 有需要可以自行打开注释,并创建文件夹
// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true }); // const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
/** 动态路由 */ /** 动态路由 */
const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles); const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
/** 外部路由列表访问这些页面可以不需要Layout可能用于内嵌在别的系统 */ /** 静态路由列表,访问这些页面可以不需要权限 */
// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles); // const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
const externalRoutes: RouteRecordRaw[] = []; const staticRoutes: RouteRecordRaw[] = [];
/** 路由列表,由基本路由+静态路由组成 */ /** 路由列表,由基本路由+静态路由组成 */
const routes: RouteRecordRaw[] = [ const routes: RouteRecordRaw[] = [
...coreRoutes, ...coreRoutes,
...externalRoutes, ...staticRoutes,
fallbackNotFoundRoute, fallbackNotFoundRoute,
]; ];

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

@ -1,6 +1,6 @@
import type { RouteRecordRaw } from 'vue-router'; import type { RouteRecordRaw } from 'vue-router';
import { BasicLayout } from '#/layouts'; import { BasicLayout, IFrameView } from '#/layouts';
import { $t } from '#/locales'; import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [ const routes: RouteRecordRaw[] = [
@ -15,13 +15,477 @@ const routes: RouteRecordRaw[] = [
name: 'Demos', name: 'Demos',
path: '/demos', path: '/demos',
children: [ children: [
// 权限控制
{ {
meta: { meta: {
title: $t('page.demos.antd'), icon: 'mdi:shield-key-outline',
title: $t('page.demos.access.frontendPermissions'),
}, },
name: 'AntDesignDemos', name: 'AccessDemos',
path: '/demos/ant-design', path: '/demos/access',
component: () => import('#/views/demos/antd/index.vue'), children: [
{
name: 'AccessPageControlDemo',
path: '/demos/access/page-control',
component: () => import('#/views/demos/access/index.vue'),
meta: {
icon: 'mdi:page-previous-outline',
title: $t('page.demos.access.pageAccess'),
},
},
{
name: 'AccessButtonControlDemo',
path: '/demos/access/button-control',
component: () => import('#/views/demos/access/button-control.vue'),
meta: {
icon: 'mdi:button-cursor',
title: $t('page.demos.access.buttonControl'),
},
},
{
name: 'AccessMenuVisible403Demo',
path: '/demos/access/menu-visible-403',
component: () =>
import('#/views/demos/access/menu-visible-403.vue'),
meta: {
authority: ['no-body'],
icon: 'mdi:button-cursor',
menuVisibleWithForbidden: true,
title: $t('page.demos.access.menuVisible403'),
},
},
{
name: 'AccessSuperVisibleDemo',
path: '/demos/access/super-visible',
component: () => import('#/views/demos/access/super-visible.vue'),
meta: {
authority: ['super'],
icon: 'mdi:button-cursor',
title: $t('page.demos.access.superVisible'),
},
},
{
name: 'AccessAdminVisibleDemo',
path: '/demos/access/admin-visible',
component: () => import('#/views/demos/access/admin-visible.vue'),
meta: {
authority: ['admin'],
icon: 'mdi:button-cursor',
title: $t('page.demos.access.adminVisible'),
},
},
{
name: 'AccessUserVisibleDemo',
path: '/demos/access/user-visible',
component: () => import('#/views/demos/access/user-visible.vue'),
meta: {
authority: ['user'],
icon: 'mdi:button-cursor',
title: $t('page.demos.access.userVisible'),
},
},
],
},
// 功能
{
meta: {
icon: 'mdi:feature-highlight',
title: $t('page.demos.features.title'),
},
name: 'FeaturesDemos',
path: '/demos/features',
children: [
{
name: 'LoginExpiredDemo',
path: '/demos/features/login-expired',
component: () =>
import('#/views/demos/features/login-expired/index.vue'),
meta: {
icon: 'mdi:encryption-expiration',
title: $t('page.demos.features.loginExpired'),
},
},
{
name: 'IconsDemo',
path: '/demos/features/icons',
component: () => import('#/views/demos/features/icons/index.vue'),
meta: {
title: $t('page.demos.features.icons'),
},
},
{
name: 'WatermarkDemo',
path: '/demos/features/watermark',
component: () =>
import('#/views/demos/features/watermark/index.vue'),
meta: {
title: $t('page.demos.features.watermark'),
},
},
{
name: 'FeatureTabsDemo',
path: '/demos/features/tabs',
component: () => import('#/views/demos/features/tabs/index.vue'),
meta: {
icon: 'lucide:app-window',
title: $t('page.demos.features.tabs'),
},
},
{
name: 'FeatureTabDetailDemo',
path: '/demos/features/tabs/detail/:id',
component: () =>
import('#/views/demos/features/tabs/tab-detail.vue'),
meta: {
activePath: '/demos/features/tabs',
hideInMenu: true,
maxNumOfOpenTab: 3,
title: $t('page.demos.features.tabDetail'),
},
},
{
name: 'HideChildrenInMenuParentDemo',
path: '/demos/features/hide-menu-children',
component: () =>
import('#/views/demos/features/hide-menu-children/parent.vue'),
meta: {
hideChildrenInMenu: true,
icon: 'ic:round-menu',
title: $t('page.demos.features.hideChildrenInMenu'),
},
children: [
{
name: 'HideChildrenInMenuChildrenDemo',
path: '/demos/features/hide-menu-children/children',
component: () =>
import(
'#/views/demos/features/hide-menu-children/children.vue'
),
meta: { title: 'HideChildrenInMenuChildrenDemo' },
},
],
},
],
},
// 面包屑导航
{
name: 'BreadcrumbDemos',
path: '/demos/breadcrumb',
meta: {
icon: 'lucide:navigation',
title: $t('page.demos.breadcrumb.navigation'),
},
children: [
{
name: 'BreadcrumbLateralDemo',
path: '/demos/breadcrumb/lateral',
component: () => import('#/views/demos/breadcrumb/lateral.vue'),
meta: {
icon: 'lucide:navigation',
title: $t('page.demos.breadcrumb.lateral'),
},
},
{
name: 'BreadcrumbLateralDetailDemo',
path: '/demos/breadcrumb/lateral-detail',
component: () =>
import('#/views/demos/breadcrumb/lateral-detail.vue'),
meta: {
activePath: '/demos/breadcrumb/lateral',
hideInMenu: true,
title: $t('page.demos.breadcrumb.lateralDetail'),
},
},
{
name: 'BreadcrumbLevelDemo',
path: '/demos/breadcrumb/level',
meta: {
icon: 'lucide:navigation',
title: $t('page.demos.breadcrumb.level'),
},
children: [
{
name: 'BreadcrumbLevelDetailDemo',
path: '/demos/breadcrumb/level/detail',
component: () =>
import('#/views/demos/breadcrumb/level-detail.vue'),
meta: {
title: $t('page.demos.breadcrumb.levelDetail'),
},
},
],
},
],
},
// 缺省页
{
meta: {
icon: 'mdi:lightbulb-error-outline',
title: $t('page.demos.fallback.title'),
},
name: 'FallbackDemos',
path: '/demos/fallback',
children: [
{
name: 'Fallback403Demo',
path: '/demos/fallback/403',
component: () => import('#/views/_core/fallback/forbidden.vue'),
meta: {
icon: 'mdi:do-not-disturb-alt',
title: '403',
},
},
{
name: 'Fallback404Demo',
path: '/demos/fallback/404',
component: () => import('#/views/_core/fallback/not-found.vue'),
meta: {
icon: 'mdi:table-off',
title: '404',
},
},
{
name: 'Fallback500Demo',
path: '/demos/fallback/500',
component: () =>
import('#/views/_core/fallback/internal-error.vue'),
meta: {
icon: 'mdi:server-network-off',
title: '500',
},
},
{
name: 'FallbackOfflineDemo',
path: '/demos/fallback/offline',
component: () => import('#/views/_core/fallback/offline.vue'),
meta: {
icon: 'mdi:offline',
title: $t('fallback.offline'),
},
},
],
},
// 菜单徽标
{
meta: {
badgeType: 'dot',
badgeVariants: 'destructive',
icon: 'lucide:circle-dot',
title: $t('page.demos.badge.title'),
},
name: 'BadgeDemos',
path: '/demos/badge',
children: [
{
name: 'BadgeDotDemo',
component: () => import('#/views/demos/badge/index.vue'),
path: '/demos/badge/dot',
meta: {
badgeType: 'dot',
icon: 'lucide:square-dot',
title: $t('page.demos.badge.dot'),
},
},
{
name: 'BadgeTextDemo',
component: () => import('#/views/demos/badge/index.vue'),
path: '/demos/badge/text',
meta: {
badge: '10',
icon: 'lucide:square-dot',
title: $t('page.demos.badge.text'),
},
},
{
name: 'BadgeColorDemo',
component: () => import('#/views/demos/badge/index.vue'),
path: '/demos/badge/color',
meta: {
badge: 'Hot',
badgeVariants: 'destructive',
icon: 'lucide:square-dot',
title: $t('page.demos.badge.color'),
},
},
],
},
// 菜单激活图标
{
meta: {
activeIcon: 'fluent-emoji:radioactive',
icon: 'bi:radioactive',
title: $t('page.demos.activeIcon.title'),
},
name: 'ActiveIconDemos',
path: '/demos/active-icon',
children: [
{
name: 'ActiveIconDemo',
component: () => import('#/views/demos/active-icon/index.vue'),
path: '/demos/active-icon/children',
meta: {
activeIcon: 'fluent-emoji:radioactive',
icon: 'bi:radioactive',
title: $t('page.demos.activeIcon.children'),
},
},
],
},
// 外部链接
{
meta: {
icon: 'ic:round-settings-input-composite',
title: $t('page.demos.outside.title'),
},
name: 'OutsideDemos',
path: '/demos/outside',
children: [
{
name: 'IframeDemos',
path: '/demos/outside/iframe',
meta: {
icon: 'mdi:newspaper-variant-outline',
title: $t('page.demos.outside.embedded'),
},
children: [
{
name: 'VueDocumentDemo',
path: '/demos/outside/iframe/vue-document',
component: IFrameView,
meta: {
icon: 'logos:vue',
iframeSrc: 'https://cn.vuejs.org/',
keepAlive: true,
title: 'Vue',
},
},
{
name: 'TailwindcssDemo',
path: '/demos/outside/iframe/tailwindcss',
component: IFrameView,
meta: {
icon: 'devicon:tailwindcss',
iframeSrc: 'https://tailwindcss.com/',
// keepAlive: true,
title: 'Tailwindcss',
},
},
],
},
{
name: 'ExternalLinkDemos',
path: '/demos/outside/external-link',
meta: {
icon: 'mdi:newspaper-variant-multiple-outline',
title: $t('page.demos.outside.externalLink'),
},
children: [
{
name: 'ViteDemo',
path: '/demos/outside/external-link/vite',
component: IFrameView,
meta: {
icon: 'logos:vitejs',
link: 'https://vitejs.dev/',
title: 'Vite',
},
},
{
name: 'VueUseDemo',
path: '/demos/outside/external-link/vue-use',
component: IFrameView,
meta: {
icon: 'logos:vueuse',
link: 'https://vueuse.org',
title: 'VueUse',
},
},
],
},
],
},
// 嵌套菜单
{
meta: {
icon: 'ic:round-menu',
title: $t('page.demos.nested.title'),
},
name: 'NestedDemos',
path: '/demos/nested',
children: [
{
name: 'Menu1Demo',
path: '/demos/nested/menu1',
component: () => import('#/views/demos/nested/menu-1.vue'),
meta: {
icon: 'ic:round-menu',
keepAlive: true,
title: $t('page.demos.nested.menu1'),
},
},
{
name: 'Menu2Demo',
path: '/demos/nested/menu2',
meta: {
icon: 'ic:round-menu',
keepAlive: true,
title: $t('page.demos.nested.menu2'),
},
children: [
{
name: 'Menu21Demo',
path: '/demos/nested/menu2/menu2-1',
component: () => import('#/views/demos/nested/menu-2-1.vue'),
meta: {
icon: 'ic:round-menu',
keepAlive: true,
title: $t('page.demos.nested.menu2_1'),
},
},
],
},
{
name: 'Menu3Demo',
path: '/demos/nested/menu3',
meta: {
icon: 'ic:round-menu',
title: $t('page.demos.nested.menu3'),
},
children: [
{
name: 'Menu31Demo',
path: 'menu3-1',
component: () => import('#/views/demos/nested/menu-3-1.vue'),
meta: {
icon: 'ic:round-menu',
keepAlive: true,
title: $t('page.demos.nested.menu3_1'),
},
},
{
name: 'Menu32Demo',
path: 'menu3-2',
meta: {
icon: 'ic:round-menu',
title: $t('page.demos.nested.menu3_2'),
},
children: [
{
name: 'Menu321Demo',
path: '/demos/nested/menu3/menu3-2/menu3-2-1',
component: () =>
import('#/views/demos/nested/menu-3-2-1.vue'),
meta: {
icon: 'ic:round-menu',
keepAlive: true,
title: $t('page.demos.nested.menu3_2_1'),
},
},
],
},
],
},
],
}, },
], ],
}, },

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'),
@ -38,7 +38,8 @@ const routes: RouteRecordRaw[] = [
component: IFrameView, component: IFrameView,
meta: { meta: {
icon: 'lucide:book-open-text', icon: 'lucide:book-open-text',
link: VBEN_DOC_URL, iframeSrc: VBEN_DOC_URL,
keepAlive: true,
title: $t('page.vben.document'), title: $t('page.vben.document'),
}, },
}, },
@ -58,7 +59,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 +69,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);
@ -17,7 +13,7 @@ onMounted(() => {
containLabel: true, containLabel: true,
left: '1%', left: '1%',
right: '1%', right: '1%',
top: '2 %', top: '2 %',
}, },
series: [ series: [
{ {
@ -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);
@ -17,7 +13,7 @@ onMounted(() => {
containLabel: true, containLabel: true,
left: '1%', left: '1%',
right: '1%', right: '1%',
top: '2 %', top: '2 %',
}, },
series: [ series: [
{ {

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

@ -4,10 +4,9 @@ import type { LoginAndRegisterParams } from '@vben/common-ui';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { AccessControl, useAccess } from '@vben/access'; import { AccessControl, useAccess } from '@vben/access';
import { Page } from '@vben/common-ui';
import { resetAllStores, useUserStore } from '@vben/stores'; import { resetAllStores, useUserStore } from '@vben/stores';
import { Button, Card } from 'ant-design-vue'; import { Button } from 'ant-design-vue';
import { useAuthStore } from '#/store'; import { useAuthStore } from '#/store';
@ -51,17 +50,21 @@ async function changeAccount(role: string) {
</script> </script>
<template> <template>
<Page <div class="p-5">
:title="`${accessMode === 'frontend' ? '前端' : '后端'}按钮访问权限演示`" <div class="card-box p-5">
description="切换不同的账号,观察按钮变化。" <h1 class="text-xl font-semibold">
> {{ accessMode === 'frontend' ? '前端' : '后端' }}页面访问权限演示
<Card class="mb-5"> </h1>
<template #title> <div class="text-foreground/80 mt-2">切换不同的账号观察按钮变化</div>
<span class="font-semibold">当前角色:</span> </div>
<div class="card-box mt-5 p-5">
<div class="mb-3">
<span class="text-lg font-semibold">当前角色:</span>
<span class="text-primary mx-4 text-lg"> <span class="text-primary mx-4 text-lg">
{{ userStore.userRoles?.[0] }} {{ userStore.userRoles?.[0] }}
</span> </span>
</template> </div>
<Button :type="roleButtonType('super')" @click="changeAccount('super')"> <Button :type="roleButtonType('super')" @click="changeAccount('super')">
切换为 Super 账号 切换为 Super 账号
@ -77,9 +80,10 @@ async function changeAccount(role: string) {
<Button :type="roleButtonType('user')" @click="changeAccount('user')"> <Button :type="roleButtonType('user')" @click="changeAccount('user')">
切换为 User 账号 切换为 User 账号
</Button> </Button>
</Card> </div>
<Card class="mb-5" title="组件形式控制 - 权限码"> <div class="card-box mt-5 p-5">
<div class="mb-3 text-lg font-semibold">组件形式控制 - 权限码方式</div>
<AccessControl :codes="['AC_100100']" type="code"> <AccessControl :codes="['AC_100100']" type="code">
<Button class="mr-4"> Super 账号可见 ["AC_1000001"] </Button> <Button class="mr-4"> Super 账号可见 ["AC_1000001"] </Button>
</AccessControl> </AccessControl>
@ -94,13 +98,10 @@ async function changeAccount(role: string) {
Super & Admin 账号可见 ["AC_100100","AC_1000001"] Super & Admin 账号可见 ["AC_100100","AC_1000001"]
</Button> </Button>
</AccessControl> </AccessControl>
</Card> </div>
<Card <div v-if="accessMode === 'frontend'" class="card-box mt-5 p-5">
v-if="accessMode === 'frontend'" <div class="mb-3 text-lg font-semibold">组件形式控制 - 用户角色方式</div>
class="mb-5"
title="组件形式控制 - 角色"
>
<AccessControl :codes="['super']" type="role"> <AccessControl :codes="['super']" type="role">
<Button class="mr-4"> Super 角色可见 </Button> <Button class="mr-4"> Super 角色可见 </Button>
</AccessControl> </AccessControl>
@ -113,9 +114,10 @@ async function changeAccount(role: string) {
<AccessControl :codes="['super', 'admin']" type="role"> <AccessControl :codes="['super', 'admin']" type="role">
<Button class="mr-4"> Super & Admin 角色可见 </Button> <Button class="mr-4"> Super & Admin 角色可见 </Button>
</AccessControl> </AccessControl>
</Card> </div>
<Card class="mb-5" title="函数形式控制"> <div class="card-box mt-5 p-5">
<div class="mb-3 text-lg font-semibold">函数形式控制</div>
<Button v-if="hasAccessByCodes(['AC_100100'])" class="mr-4"> <Button v-if="hasAccessByCodes(['AC_100100'])" class="mr-4">
Super 账号可见 ["AC_1000001"] Super 账号可见 ["AC_1000001"]
</Button> </Button>
@ -128,9 +130,10 @@ async function changeAccount(role: string) {
<Button v-if="hasAccessByCodes(['AC_100100', 'AC_1000001'])" class="mr-4"> <Button v-if="hasAccessByCodes(['AC_100100', 'AC_1000001'])" class="mr-4">
Super & Admin 账号可见 ["AC_100100","AC_1000001"] Super & Admin 账号可见 ["AC_100100","AC_1000001"]
</Button> </Button>
</Card> </div>
<Card class="mb-5" title="指令方式 - 权限码"> <div class="card-box mt-5 p-5">
<div class="mb-3 text-lg font-semibold">指令方式 - 权限码</div>
<Button class="mr-4" v-access:code="['AC_100100']"> <Button class="mr-4" v-access:code="['AC_100100']">
Super 账号可见 ["AC_1000001"] Super 账号可见 ["AC_1000001"]
</Button> </Button>
@ -143,15 +146,16 @@ async function changeAccount(role: string) {
<Button class="mr-4" v-access:code="['AC_100100', 'AC_1000001']"> <Button class="mr-4" v-access:code="['AC_100100', 'AC_1000001']">
Super & Admin 账号可见 ["AC_100100","AC_1000001"] Super & Admin 账号可见 ["AC_100100","AC_1000001"]
</Button> </Button>
</Card> </div>
<Card class="mb-5" title="指令方式 - 角色"> <div v-if="accessMode === 'frontend'" class="card-box mt-5 p-5">
<div class="mb-3 text-lg font-semibold">指令方式 - 角色</div>
<Button class="mr-4" v-access:role="['super']"> Super 角色可见 </Button> <Button class="mr-4" v-access:role="['super']"> Super 角色可见 </Button>
<Button class="mr-4" v-access:role="['admin']"> Admin 角色可见 </Button> <Button class="mr-4" v-access:role="['admin']"> Admin 角色可见 </Button>
<Button class="mr-4" v-access:role="['user']"> User 角色可见 </Button> <Button class="mr-4" v-access:role="['user']"> User 角色可见 </Button>
<Button class="mr-4" v-access:role="['super', 'admin']"> <Button class="mr-4" v-access:role="['super', 'admin']">
Super & Admin 角色可见 Super & Admin 角色可见
</Button> </Button>
</Card> </div>
</Page> </div>
</template> </template>

View File

@ -4,10 +4,9 @@ import type { LoginAndRegisterParams } from '@vben/common-ui';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useAccess } from '@vben/access'; import { useAccess } from '@vben/access';
import { Page } from '@vben/common-ui';
import { resetAllStores, useUserStore } from '@vben/stores'; import { resetAllStores, useUserStore } from '@vben/stores';
import { Button, Card } from 'ant-design-vue'; import { Button } from 'ant-design-vue';
import { useAuthStore } from '#/store'; import { useAuthStore } from '#/store';
@ -65,20 +64,33 @@ async function handleToggleAccessMode() {
</script> </script>
<template> <template>
<Page <div class="p-5">
:title="`${accessMode === 'frontend' ? '前端' : '后端'}页面访问权限演示`" <div class="card-box p-5">
description="切换不同的账号,观察左侧菜单变化。" <h1 class="text-xl font-semibold">
> {{ accessMode === 'frontend' ? '前端' : '后端' }}页面访问权限演示
<Card class="mb-5" title="权限模式"> </h1>
<span class="font-semibold">当前权限模式:</span> <div class="text-foreground/80 mt-2">
切换不同的账号观察左侧菜单变化
</div>
</div>
<div class="card-box mt-5 p-5">
<span class="text-lg font-semibold">当前权限模式:</span>
<span class="text-primary mx-4">{{ <span class="text-primary mx-4">{{
accessMode === 'frontend' ? '前端权限控制' : '后端权限控制' accessMode === 'frontend' ? '前端权限控制' : '后端权限控制'
}}</span> }}</span>
<Button type="primary" @click="handleToggleAccessMode"> <Button type="primary" @click="handleToggleAccessMode">
切换为{{ accessMode === 'frontend' ? '后端' : '前端' }}权限模式 切换为{{ accessMode === 'frontend' ? '后端' : '前端' }}权限模式
</Button> </Button>
</Card> </div>
<Card title="账号切换"> <div class="card-box mt-5 p-5">
<div class="mb-3">
<span class="text-lg font-semibold">当前账号:</span>
<span class="text-primary mx-4 text-lg">
{{ userStore.userRoles?.[0] }}
</span>
</div>
<Button :type="roleButtonType('super')" @click="changeAccount('super')"> <Button :type="roleButtonType('super')" @click="changeAccount('super')">
切换为 Super 账号 切换为 Super 账号
</Button> </Button>
@ -93,6 +105,6 @@ async function handleToggleAccessMode() {
<Button :type="roleButtonType('user')" @click="changeAccount('user')"> <Button :type="roleButtonType('user')" @click="changeAccount('user')">
切换为 User 账号 切换为 User 账号
</Button> </Button>
</Card> </div>
</Page> </div>
</template> </template>

View File

@ -4,7 +4,7 @@ import { Fallback } from '@vben/common-ui';
<template> <template>
<Fallback <Fallback
description="当前页面仅 User 账号可见" description="当前页面仅 User 可见"
status="coming-soon" status="coming-soon"
title="页面访问测试" title="页面访问测试"
/> />

View File

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

View File

@ -1,5 +1,4 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Page } from '@vben/common-ui';
import { import {
MdiGithub, MdiGithub,
MdiGoogle, MdiGoogle,
@ -15,13 +14,12 @@ import {
SvgCardIcon, SvgCardIcon,
SvgDownloadIcon, SvgDownloadIcon,
} from '@vben/icons'; } from '@vben/icons';
import { Card } from 'ant-design-vue';
</script> </script>
<template> <template>
<Page title="图标"> <div class="p-5">
<template #description> <div class="card-box p-5">
<h1 class="text-xl font-semibold">图标</h1>
<div class="text-foreground/80 mt-2"> <div class="text-foreground/80 mt-2">
图标可在 图标可在
<a <a
@ -33,9 +31,10 @@ import { Card } from 'ant-design-vue';
</a> </a>
中查找支持多种图标库 Material Design, Font Awesome, Jam Icons 中查找支持多种图标库 Material Design, Font Awesome, Jam Icons
</div> </div>
</template> </div>
<Card class="mb-5" title="Iconify"> <div class="card-box mt-5 p-5">
<div class="mb-3 text-lg font-semibold">Iconify</div>
<div class="flex items-center gap-5"> <div class="flex items-center gap-5">
<MdiGithub class="size-8" /> <MdiGithub class="size-8" />
<MdiGoogle class="size-8 text-red-500" /> <MdiGoogle class="size-8 text-red-500" />
@ -43,9 +42,10 @@ import { Card } from 'ant-design-vue';
<MdiWechat class="size-8" /> <MdiWechat class="size-8" />
<MdiKeyboardEsc class="size-8" /> <MdiKeyboardEsc class="size-8" />
</div> </div>
</Card> </div>
<Card title="Svg Icons"> <div class="card-box mt-5 p-5">
<div class="mb-3 text-lg font-semibold">Svg Icons</div>
<div class="flex items-center gap-5"> <div class="flex items-center gap-5">
<SvgAvatar1Icon class="size-8" /> <SvgAvatar1Icon class="size-8" />
<SvgAvatar2Icon class="size-8 text-red-500" /> <SvgAvatar2Icon class="size-8 text-red-500" />
@ -56,6 +56,6 @@ import { Card } from 'ant-design-vue';
<SvgCardIcon class="size-8" /> <SvgCardIcon class="size-8" />
<SvgDownloadIcon class="size-8" /> <SvgDownloadIcon class="size-8" />
</div> </div>
</Card> </div>
</Page> </div>
</template> </template>

View File

@ -1,10 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { LoginExpiredModeType } from '@vben/types'; import type { LoginExpiredModeType } from '@vben/types';
import { Page } from '@vben/common-ui';
import { preferences, updatePreferences } from '@vben/preferences'; import { preferences, updatePreferences } from '@vben/preferences';
import { Button, Card } from 'ant-design-vue'; import { Button } from 'ant-design-vue';
import { getMockStatusApi } from '#/api'; import { getMockStatusApi } from '#/api';
@ -18,22 +17,26 @@ async function handleClick(type: LoginExpiredModeType) {
</script> </script>
<template> <template>
<Page title="登录过期演示"> <div class="p-5">
<template #description> <div class="card-box p-5">
<h1 class="text-xl font-semibold">登录过期演示</h1>
<div class="text-foreground/80 mt-2"> <div class="text-foreground/80 mt-2">
接口请求遇到401状态码时需要重新登录有两种方式 接口请求遇到401状态码时需要重新登录有两种方式
<p>1.转到登录页登录成功后跳转回原页面</p> <div>1.转到登录页登录成功后跳转回原页面</div>
<p> <div>
2.弹出重新登录弹窗登录后关闭弹窗不进行任何页面跳转刷新后还是会跳转登录页面 2.弹出重新登录弹窗登录后关闭弹窗不进行任何页面跳转刷新后调整登录页面
</p> </div>
</div> </div>
</template> </div>
<Card class="mb-5" title="跳转登录页面方式"> <div class="card-box mt-5 p-5">
<div class="mb-3 text-lg font-semibold">跳转登录页面方式</div>
<Button type="primary" @click="handleClick('page')"> 点击触发 </Button> <Button type="primary" @click="handleClick('page')"> 点击触发 </Button>
</Card> </div>
<Card class="mb-5" title="登录弹窗方式">
<div class="card-box mt-5 p-5">
<div class="mb-3 text-lg font-semibold">登录弹窗方式</div>
<Button type="primary" @click="handleClick('modal')"> 点击触发 </Button> <Button type="primary" @click="handleClick('modal')"> 点击触发 </Button>
</Card> </div>
</Page> </div>
</template> </template>

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