Compare commits

..

1 Commits
v5.2.1 ... doc

Author SHA1 Message Date
jinmao
dc2b8f00e0 chore: 添加3群链接 2024-08-07 16:21:34 +08:00
644 changed files with 6767 additions and 21099 deletions

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:
- [Pull Request Guidelines](#pull-request-guidelines)
## 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
```
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.
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.

View File

@@ -13,21 +13,18 @@ categories:
- title: "🚀 Features"
labels:
- "feature"
- "enhancement"
- title: "🐞 Bug Fixes"
labels:
- "bug"
- title: "📈 Performance"
labels:
- "perf"
- "enhancement"
- title: 📝 Documentation
- title: 📝 Documentation updates
labels:
- "documentation"
- title: 👻 Maintenance
labels:
- "chore"
- "dependencies"
# collapse-after: 12
collapse-after: 5
- title: 🚦 Tests
labels:
- "tests"
@@ -37,15 +34,12 @@ categories:
version-resolver:
major:
labels:
- "major"
- "breaking"
minor:
labels:
- "minor"
- "feature"
patch:
labels:
- "feature"
- "patch"
- "bug"
- "maintenance"
- "docs"

View File

@@ -23,7 +23,7 @@ jobs:
matrix:
os:
- ubuntu-latest
# - macos-latest
- macos-latest
- windows-latest
timeout-minutes: 20
steps:
@@ -62,7 +62,7 @@ jobs:
matrix:
os:
- ubuntu-latest
# - macos-latest
- macos-latest
- windows-latest
steps:
@@ -85,7 +85,7 @@ jobs:
matrix:
os:
- ubuntu-latest
# - macos-latest
- macos-latest
- windows-latest
steps:
- name: Checkout code

View File

@@ -6,63 +6,8 @@ on:
- main
jobs:
deploy-playground-ftp:
name: Deploy Push Playground Ftp
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]')
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
deploy-push-ftp:
name: Deploy Push Ftp
if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]')
runs-on: ubuntu-latest
steps:
@@ -77,65 +22,9 @@ jobs:
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
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]')
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_PWA\s*=.*#VITE_PWA = true#g" ./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]')
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_PWA\s*=.*#VITE_PWA = true#g" ./apps/web-naive/.env.production
cat ./apps/web-naive/.env.production
@@ -144,12 +33,36 @@ jobs:
uses: ./.github/actions/setup-node
- 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
with:
server: ${{ secrets.PRO_FTP_HOST }}
username: ${{ secrets.WEB_NAIVE_FTP_ACCOUNT }}
password: ${{ secrets.WEB_NAIVE_FTP_PASSWORD }}
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,16 +7,9 @@ on:
permissions:
contents: read
pull-requests: write
jobs:
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
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@v6

View File

@@ -21,5 +21,5 @@ jobs:
with:
actions: "close-issues" # 执行动作:关闭 Issues
token: ${{ secrets.GITHUB_TOKEN }} # GitHub Token用于认证
labels: "needs reproduction" # 目标标签
labels: "need reproduction" # 目标标签
inactive-day: 3 # 未活动天数阈值

1
.gitignore vendored
View File

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

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[]=eslint
public-hoist-pattern[]=prettier

View File

@@ -10,6 +10,10 @@
"esbenp.prettier-vscode",
// 支持 dotenv 文件语法
"mikestead.dotenv",
// 获取每个 CSS 属性的初始值。
"dzhavat.css-initial-value",
// 使 VSCode 中的 TypeScript 错误更漂亮、更易于理解
"yoavbls.pretty-ts-errors",
// 源代码的拼写检查器
"streetsidesoftware.code-spell-checker",
// Tailwind CSS 的官方 VS Code 插件

31
.vscode/launch.json vendored
View File

@@ -4,39 +4,12 @@
"configurations": [
{
"type": "chrome",
"name": "vben admin playground dev",
"name": "vben admin antd dev",
"request": "launch",
"url": "http://localhost:5555",
"env": { "NODE_ENV": "development" },
"sourceMaps": true,
"webRoot": "${workspaceFolder}"
},
{
"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}"
"webRoot": "${workspaceFolder}/apps/web-antd/src"
}
]
}

43
.vscode/settings.json vendored
View File

@@ -14,6 +14,7 @@
"editor.tabSize": 2,
"editor.detectIndentation": false,
"editor.cursorBlinking": "expand",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.largeFileOptimizations": false,
"editor.accessibilitySupport": "off",
"editor.cursorSmoothCaretAnimation": "on",
@@ -36,34 +37,7 @@
"source.fixAll.stylelint": "explicit",
"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.ignoreRecommendations": true,
@@ -82,8 +56,7 @@
"*.ejs": "html",
"*.art": "html",
"**/tsconfig.json": "jsonc",
"*.json": "jsonc",
"package.json": "json"
"*.json": "jsonc"
},
"files.exclude": {
@@ -194,10 +167,8 @@
"i18n-ally.localesPaths": [
"packages/locales/src/langs",
"playground/src/locales/langs",
"apps/*/src/locales/langs"
],
"i18n-ally.pathMatcher": "{locale}.json",
"i18n-ally.enabledParsers": ["json", "ts", "js", "yaml"],
"i18n-ally.sourceLanguage": "en",
"i18n-ally.displayLanguage": "zh-CN",
@@ -212,13 +183,11 @@
"*.env": "$(capture).env.*",
"README.md": "README*,CHANGELOG*,LICENSE,CNAME",
"package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,.gitattributes,.gitignore,.gitpod.yml,.npmrc,.browserslistrc,.node-version,.git*,.tazerc.json",
"Dockerfile": "Dockerfile,.docker*,docker-entrypoint.sh,build-local-docker*,nginx.conf",
"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.*"
},
"commentTranslate.hover.enabled": false,
"i18n-ally.keystyle": "nested",
"commentTranslate.multiLineMerge": true,
"vue.server.hybridMode": true,
"typescript.tsdk": "node_modules/typescript/lib"
"commentTranslate.multiLineMerge": true
}

View File

@@ -21,9 +21,9 @@ RUN echo "Builder Success 🎉"
FROM nginx:stable-alpine as production
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

View File

@@ -1,149 +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.6/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)
## 寄付
このプロジェクトが役に立つと思われた場合、作者にコーヒーを一杯おごってサポートを示すことができます!
![donate](https://unpkg.com/@vbenjs/static-source@0.1.6/source/sponsor.png)
<a style="display: block;width: 100px;height: 50px;line-height: 50px; color: #fff;text-align: center; background: #408aed;border-radius: 4px;" href="https://www.paypal.com/paypalme/cvvben">Paypal Me</a>
## 貢献者
<a 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.6/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)
<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.zh-CN.md) | [日本語](./README.ja-JP.md)
**English** | [中文](./README.zh-CN.md)
## Introduction
@@ -79,7 +77,7 @@ pnpm build
## Change Log
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases)
[CHANGELOG](./CHANGELOG.zh_CN.md)
## How to contribute
@@ -128,15 +126,14 @@ Support modern browsers, not IE
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.6/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>
## Contributor
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
<img alt="Contributors"
src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
<img src="https://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
</a>
## 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.6/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)
<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.ja-JP.md)
**中文** | [English](./README.md)
## 简介
@@ -124,21 +122,10 @@ pnpm build
如果你觉得这个项目对你有帮助,你可以帮作者买一杯咖啡表示支持!
![donate](https://unpkg.com/@vbenjs/static-source@0.1.6/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>
## 更新日志
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases)
## 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
- [Github Discussions](https://github.com/anncwb/vue-vben-admin/discussions)

View File

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

View File

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

View File

@@ -1,14 +1,15 @@
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
const token = getHeader(event, 'Authorization');
if (!token) {
setResponseStatus(event, 401);
return useResponseError('UnauthorizedException', 'Unauthorized Exception');
}
const username = Buffer.from(token, 'base64').toString('utf8');
const codes =
MOCK_CODES.find((item) => item.username === userinfo.username)?.codes ?? [];
MOCK_CODES.find((item) => item.username === username)?.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) => {
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(
(item) => item.username === username && item.password === password,
);
if (!findUser) {
clearRefreshTokenCookie(event);
return forbiddenResponse(event);
setResponseStatus(event, 403);
return useResponseError('UnauthorizedException', '用户名或密码错误');
}
const accessToken = generateAccessToken(findUser);
const refreshToken = generateRefreshToken(findUser);
setRefreshTokenCookie(event, refreshToken);
const accessToken = Buffer.from(username).toString('base64');
return useResponseSuccess({
...findUser,
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) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
const token = getHeader(event, 'Authorization');
if (!token) {
setResponseStatus(event, 401);
return useResponseError('UnauthorizedException', 'Unauthorized Exception');
}
const username = Buffer.from(token, 'base64').toString('utf8');
const menus =
MOCK_MENUS.find((item) => item.username === userinfo.username)?.menus ?? [];
MOCK_MENUS.find((item) => item.username === username)?.menus ?? [];
return useResponseSuccess(menus);
});

View File

@@ -1,11 +1,14 @@
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
const token = getHeader(event, 'Authorization');
if (!token) {
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';
const errorHandler: NitroErrorHandler = function (error, event) {
event.node.res.end(`[Error Handler] ${error.stack}`);
event.res.end(`[error handler] ${error.stack}`);
};
export default errorHandler;

View File

@@ -1,4 +1,11 @@
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') {
event.node.res.statusCode = 204;
event.node.res.statusMessage = 'No Content.';

View File

@@ -6,15 +6,10 @@
"license": "MIT",
"author": "",
"scripts": {
"build": "nitro build",
"start": "nitro dev"
"start": "nitro dev",
"build": "nitro build"
},
"dependencies": {
"jsonwebtoken": "^9.0.2",
"nitropack": "^2.9.7"
},
"devDependencies": {
"@types/jsonwebtoken": "^9.0.6",
"h3": "^1.12.0"
}
}

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 {
id: number;
password: string;
realName: string;
roles: string[];
username: string;
}
export const MOCK_USERS: UserInfo[] = [
export const MOCK_USERS = [
{
id: 0,
password: '123456',

View File

@@ -1,5 +1,3 @@
import type { EventHandlerRequest, H3Event } from 'h3';
export function useResponseSuccess<T = any>(data: T) {
return {
code: 0,
@@ -17,13 +15,3 @@ export function useResponseError(message: string, error: any = null) {
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');
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/web-antd",
"version": "5.2.1",
"version": "5.0.0",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
@@ -27,24 +27,24 @@
},
"dependencies": {
"@vben/access": "workspace:*",
"@vben/chart-ui": "workspace:*",
"@vben/common-ui": "workspace:*",
"@vben/constants": "workspace:*",
"@vben/hooks": "workspace:*",
"@vben/icons": "workspace:*",
"@vben/layouts": "workspace:*",
"@vben/locales": "workspace:*",
"@vben/plugins": "workspace:*",
"@vben/preferences": "workspace:*",
"@vben/request": "workspace:*",
"@vben/stores": "workspace:*",
"@vben/styles": "workspace:*",
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"@vueuse/core": "^11.0.3",
"@vueuse/core": "^10.11.0",
"ant-design-vue": "^4.2.3",
"dayjs": "^1.11.13",
"pinia": "2.2.2",
"vue": "^3.4.38",
"vue-router": "^4.4.3"
"dayjs": "^1.11.12",
"pinia": "2.2.0",
"vue": "^3.4.35",
"vue-router": "^4.4.2"
}
}

View File

@@ -1,4 +1,4 @@
import { baseRequestClient, requestClient } from '#/api/request';
import { requestClient } from '#/api/request';
export namespace AuthApi {
/** 登录接口参数 */
@@ -12,14 +12,10 @@ export namespace AuthApi {
accessToken: string;
desc: string;
realName: string;
refreshToken: string;
userId: string;
username: string;
}
export interface RefreshTokenResult {
data: string;
status: number;
}
}
/**
@@ -29,24 +25,6 @@ export async function loginApi(data: AuthApi.LoginParams) {
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 +1,2 @@
export * from './core';
export * from './demos';

View File

@@ -1,104 +1,67 @@
/**
* 该文件可自行根据业务逻辑进行调整
*/
import type { HttpResponse } from '@vben/request';
import { useAppConfig } from '@vben/hooks';
import { preferences } from '@vben/preferences';
import {
authenticateResponseInterceptor,
errorMessageResponseInterceptor,
RequestClient,
} from '@vben/request';
import { RequestClient } from '@vben/request';
import { useAccessStore } from '@vben/stores';
import { message } from 'ant-design-vue';
import { useAuthStore } from '#/store';
import { refreshTokenApi } from './core';
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
function createRequestClient(baseURL: string) {
const client = new RequestClient({
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);
/**
* 重新认证逻辑
*/
async function doReAuthenticate() {
console.warn('Access token or refresh token is invalid or expired. ');
const accessStore = useAccessStore();
const authStore = useAuthStore();
accessStore.setAccessToken(null);
if (
preferences.app.loginExpiredMode === 'modal' &&
accessStore.isAccessChecked
) {
accessStore.setLoginExpired(true);
} else {
await authStore.logout();
if (preferences.app.loginExpiredMode === 'modal') {
accessStore.setLoginExpired(true);
} else {
// 退出登录
await authStore.logout();
}
},
};
},
makeErrorMessage: (msg) => message.error(msg),
makeRequestHeaders: () => {
return {
// 为每个请求携带 Accept-Language
'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;
}
}
/**
* 刷新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;
},
throw new Error(msg);
});
// response数据解构
client.addResponseInterceptor({
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) => message.error(msg)),
);
return client;
}
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 { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
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 {
BasicLayout,
@@ -13,10 +14,16 @@ import {
UserDropdown,
} from '@vben/layouts';
import { preferences } from '@vben/preferences';
import { storeToRefs, useAccessStore, useUserStore } from '@vben/stores';
import {
resetAllStores,
storeToRefs,
useAccessStore,
useUserStore,
} from '@vben/stores';
import { openWindow } from '@vben/utils';
import { $t } from '#/locales';
import { resetRoutes } from '#/router';
import { useAuthStore } from '#/store';
const notifications = ref<NotificationItem[]>([
@@ -93,8 +100,12 @@ const avatar = computed(() => {
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
});
const router = useRouter();
async function handleLogout() {
await authStore.logout(false);
resetAllStores();
resetRoutes();
await router.replace(LOGIN_PATH);
}
function handleNoticeClear() {

View File

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

View File

@@ -58,11 +58,7 @@ async function loadDayjsLocale(lang: SupportedLanguagesType) {
locale = await import('dayjs/locale/en');
}
}
if (locale) {
dayjs.locale(locale);
} else {
console.error(`Failed to load dayjs locale for ${lang}`);
}
dayjs.locale(locale);
}
/**
@@ -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": {
"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": {
"demos": {
"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;
}
const accessRoutes = accessStore.accessRoutes;
// 是否已经生成过动态路由
if (accessStore.isAccessChecked) {
if (accessRoutes && accessRoutes.length > 0) {
return true;
}
@@ -113,11 +115,10 @@ function setupAccessGuard(router: Router) {
// 保存菜单信息和路由信息
accessStore.setAccessMenus(accessibleMenus);
accessStore.setAccessRoutes(accessibleRoutes);
accessStore.setIsAccessChecked(true);
const redirectPath = (from.query.redirect ?? to.fullPath) as string;
const redirectPath = (from.query.redirect ?? to.path) as string;
return {
...router.resolve(decodeURIComponent(redirectPath)),
path: decodeURIComponent(redirectPath),
replace: 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);
/** 外部路由列表,访问这些页面可以不需要Layout可能用于内嵌在别的系统 */
// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
const externalRoutes: RouteRecordRaw[] = [];
/** 静态路由列表,访问这些页面可以不需要权限 */
// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
const staticRoutes: RouteRecordRaw[] = [];
/** 路由列表,由基本路由+静态路由组成 */
const routes: RouteRecordRaw[] = [
...coreRoutes,
...externalRoutes,
...staticRoutes,
fallbackNotFoundRoute,
];

View File

@@ -1,6 +1,6 @@
import type { RouteRecordRaw } from 'vue-router';
import { BasicLayout } from '#/layouts';
import { BasicLayout, IFrameView } from '#/layouts';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
@@ -15,13 +15,477 @@ const routes: RouteRecordRaw[] = [
name: 'Demos',
path: '/demos',
children: [
// 权限控制
{
meta: {
title: $t('page.demos.antd'),
icon: 'mdi:shield-key-outline',
title: $t('page.demos.access.frontendPermissions'),
},
name: 'AntDesignDemos',
path: '/demos/ant-design',
component: () => import('#/views/demos/antd/index.vue'),
name: 'AccessDemos',
path: '/demos/access',
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',
path: '/vben-admin/about',
component: () => import('#/views/_core/about/index.vue'),
component: () => import('#/views/_core/vben/about/index.vue'),
meta: {
icon: 'lucide:copyright',
title: $t('page.vben.about'),
@@ -38,7 +38,8 @@ const routes: RouteRecordRaw[] = [
component: IFrameView,
meta: {
icon: 'lucide:book-open-text',
link: VBEN_DOC_URL,
iframeSrc: VBEN_DOC_URL,
keepAlive: true,
title: $t('page.vben.document'),
},
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,7 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);
@@ -17,7 +13,7 @@ onMounted(() => {
containLabel: true,
left: '1%',
right: '1%',
top: '2 %',
top: '2 %',
},
series: [
{

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ import { Fallback } from '@vben/common-ui';
<template>
<Fallback
description="当前页面仅 User 账号可见"
description="当前页面仅 User 可见"
status="coming-soon"
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>
import { Page } from '@vben/common-ui';
import {
MdiGithub,
MdiGoogle,
@@ -15,13 +14,12 @@ import {
SvgCardIcon,
SvgDownloadIcon,
} from '@vben/icons';
import { Card } from 'ant-design-vue';
</script>
<template>
<Page title="图标">
<template #description>
<div class="p-5">
<div class="card-box p-5">
<h1 class="text-xl font-semibold">图标</h1>
<div class="text-foreground/80 mt-2">
图标可在
<a
@@ -33,9 +31,10 @@ import { Card } from 'ant-design-vue';
</a>
中查找支持多种图标库 Material Design, Font Awesome, Jam Icons
</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">
<MdiGithub class="size-8" />
<MdiGoogle class="size-8 text-red-500" />
@@ -43,9 +42,10 @@ import { Card } from 'ant-design-vue';
<MdiWechat class="size-8" />
<MdiKeyboardEsc class="size-8" />
</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">
<SvgAvatar1Icon class="size-8" />
<SvgAvatar2Icon class="size-8 text-red-500" />
@@ -56,6 +56,6 @@ import { Card } from 'ant-design-vue';
<SvgCardIcon class="size-8" />
<SvgDownloadIcon class="size-8" />
</div>
</Card>
</Page>
</div>
</div>
</template>

View File

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

View File

@@ -2,10 +2,9 @@
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { Page } from '@vben/common-ui';
import { useTabs } from '@vben/hooks';
import { Button, Card, Input } from 'ant-design-vue';
import { Input as AInput, Button } from 'ant-design-vue';
const router = useRouter();
const newTabTitle = ref('');
@@ -39,9 +38,15 @@ function reset() {
</script>
<template>
<Page description="用于需要操作标签页的场景" title="标签页">
<Card class="mb-5" title="打开/关闭标签页">
<div class="text-foreground/80 mb-3">
<div class="p-5">
<div class="card-box p-5">
<h1 class="text-xl font-semibold">标签页</h1>
<div class="text-foreground/80 mt-2">用于需要操作标签页的场景</div>
</div>
<div class="card-box mt-5 p-5">
<div class="text-lg font-semibold">打开/关闭标签页</div>
<div class="text-foreground/80 my-3">
如果标签页存在直接跳转切换如果标签页不存在则打开新的标签页
</div>
<div class="flex flex-wrap gap-3">
@@ -50,10 +55,11 @@ function reset() {
关闭 "关于" 标签页
</Button>
</div>
</Card>
</div>
<Card class="mb-5" title="标签页操作">
<div class="text-foreground/80 mb-3">用于动态控制标签页的各种操作</div>
<div class="card-box mt-5 p-5">
<div class="text-lg font-semibold">标签页操作</div>
<div class="text-foreground/80 my-3">用于动态控制标签页的各种操作</div>
<div class="flex flex-wrap gap-3">
<Button type="primary" @click="closeCurrentTab()">
关闭当前标签页
@@ -70,14 +76,15 @@ function reset() {
</Button>
<Button type="primary" @click="refreshTab()"> 刷新当前标签页 </Button>
</div>
</Card>
</div>
<Card class="mb-5" title="动态标题">
<div class="text-foreground/80 mb-3">
<div class="card-box mt-5 p-5">
<div class="text-lg font-semibold">动态标题</div>
<div class="text-foreground/80 my-3">
该操作不会影响页面标题仅修改Tab标题
</div>
<div class="flex flex-wrap items-center gap-3">
<Input
<AInput
v-model:value="newTabTitle"
class="w-40"
placeholder="请输入新标题"
@@ -87,10 +94,11 @@ function reset() {
</Button>
<Button @click="reset"> 重置 </Button>
</div>
</Card>
</div>
<Card class="mb-5" title="最大打开数量">
<div class="text-foreground/80 mb-3">
<div class="card-box mt-5 p-5">
<div class="text-lg font-semibold">最大打开数量</div>
<div class="text-foreground/80 my-3">
限制带参数的tab打开的最大数量 `route.meta.maxNumOfOpenTab` 控制
</div>
<div class="flex flex-wrap items-center gap-3">
@@ -100,6 +108,6 @@ function reset() {
</Button>
</template>
</div>
</Card>
</Page>
</div>
</div>
</template>

View File

@@ -2,7 +2,6 @@
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { Page } from '@vben/common-ui';
import { useTabs } from '@vben/hooks';
const route = useRoute();
@@ -17,7 +16,12 @@ setTabTitle(`No.${index.value} - 详情信息`);
</script>
<template>
<Page :title="`标签页${index}详情页`">
<template #description> {{ index }} - 详情页内容在此 </template>
</Page>
<div class="p-5">
<div class="card-box p-5">
<h1 class="text-xl font-semibold">标签详情页</h1>
<div class="text-foreground/80 mt-2">
<div>{{ index }} - 详情页内容在此</div>
</div>
</div>
</div>
</template>

View File

@@ -1,8 +1,7 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import { useWatermark } from '@vben/hooks';
import { Button, Card } from 'ant-design-vue';
import { Button } from 'ant-design-vue';
const { destroyWatermark, updateWatermark } = useWatermark();
@@ -41,8 +40,9 @@ async function createWaterMark() {
</script>
<template>
<Page title="水印">
<template #description>
<div class="p-5">
<div class="card-box p-5">
<h1 class="text-xl font-semibold">水印</h1>
<div class="text-foreground/80 mt-2">
水印使用了
<a
@@ -54,13 +54,13 @@ async function createWaterMark() {
</a>
开源插件详细配置可见插件配置
</div>
</template>
</div>
<Card title="使用">
<Button class="mr-2" type="primary" @click="createWaterMark()">
创建水印
</Button>
<Button danger @click="destroyWatermark">移除水印</Button>
</Card>
</Page>
<div class="card-box mt-5 p-5">
<div class="mb-3 flex gap-3 text-lg font-semibold">
<Button type="primary" @click="createWaterMark()">创建水印</Button>
<Button danger @click="destroyWatermark">移除水印</Button>
</div>
</div>
</div>
</template>

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/web-ele",
"version": "5.2.1",
"version": "5.0.0",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
@@ -27,25 +27,25 @@
},
"dependencies": {
"@vben/access": "workspace:*",
"@vben/chart-ui": "workspace:*",
"@vben/common-ui": "workspace:*",
"@vben/constants": "workspace:*",
"@vben/hooks": "workspace:*",
"@vben/icons": "workspace:*",
"@vben/layouts": "workspace:*",
"@vben/locales": "workspace:*",
"@vben/plugins": "workspace:*",
"@vben/preferences": "workspace:*",
"@vben/request": "workspace:*",
"@vben/stores": "workspace:*",
"@vben/styles": "workspace:*",
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"@vueuse/core": "^11.0.3",
"dayjs": "^1.11.13",
"element-plus": "^2.8.1",
"pinia": "2.2.2",
"vue": "^3.4.38",
"vue-router": "^4.4.3"
"@vueuse/core": "^10.11.0",
"dayjs": "^1.11.12",
"element-plus": "^2.7.8",
"pinia": "2.2.0",
"vue": "^3.4.35",
"vue-router": "^4.4.2"
},
"devDependencies": {
"unplugin-element-plus": "^0.8.0"

View File

@@ -1,4 +1,4 @@
import { baseRequestClient, requestClient } from '#/api/request';
import { requestClient } from '#/api/request';
export namespace AuthApi {
/** 登录接口参数 */
@@ -12,14 +12,10 @@ export namespace AuthApi {
accessToken: string;
desc: string;
realName: string;
refreshToken: string;
userId: string;
username: string;
}
export interface RefreshTokenResult {
data: string;
status: number;
}
}
/**
@@ -29,24 +25,6 @@ export async function loginApi(data: AuthApi.LoginParams) {
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,104 +1,67 @@
/**
* 该文件可自行根据业务逻辑进行调整
*/
import type { HttpResponse } from '@vben/request';
import { useAppConfig } from '@vben/hooks';
import { preferences } from '@vben/preferences';
import {
authenticateResponseInterceptor,
errorMessageResponseInterceptor,
RequestClient,
} from '@vben/request';
import { RequestClient } from '@vben/request';
import { useAccessStore } from '@vben/stores';
import { ElMessage } from 'element-plus';
import { useAuthStore } from '#/store';
import { refreshTokenApi } from './core';
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
function createRequestClient(baseURL: string) {
const client = new RequestClient({
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);
/**
* 重新认证逻辑
*/
async function doReAuthenticate() {
console.warn('Access token or refresh token is invalid or expired. ');
const accessStore = useAccessStore();
const authStore = useAuthStore();
accessStore.setAccessToken(null);
if (
preferences.app.loginExpiredMode === 'modal' &&
accessStore.isAccessChecked
) {
accessStore.setLoginExpired(true);
} else {
await authStore.logout();
if (preferences.app.loginExpiredMode === 'modal') {
accessStore.setLoginExpired(true);
} else {
// 退出登录
await authStore.logout();
}
},
};
},
makeErrorMessage: (msg) => ElMessage.error(msg),
makeRequestHeaders: () => {
return {
// 为每个请求携带 Accept-Language
'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;
}
}
/**
* 刷新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;
},
throw new Error(msg);
});
// response数据解构
client.addResponseInterceptor({
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) => ElMessage.error(msg)),
);
return client;
}
export const requestClient = createRequestClient(apiURL);
export const baseRequestClient = new RequestClient({ baseURL: apiURL });

View File

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

View File

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

View File

@@ -2,9 +2,10 @@
import type { NotificationItem } from '@vben/layouts';
import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
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 {
BasicLayout,
@@ -13,10 +14,16 @@ import {
UserDropdown,
} from '@vben/layouts';
import { preferences } from '@vben/preferences';
import { storeToRefs, useAccessStore, useUserStore } from '@vben/stores';
import {
resetAllStores,
storeToRefs,
useAccessStore,
useUserStore,
} from '@vben/stores';
import { openWindow } from '@vben/utils';
import { $t } from '#/locales';
import { resetRoutes } from '#/router';
import { useAuthStore } from '#/store';
const notifications = ref<NotificationItem[]>([
@@ -93,8 +100,12 @@ const avatar = computed(() => {
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
});
const router = useRouter();
async function handleLogout() {
await authStore.logout(false);
resetAllStores();
resetRoutes();
await router.replace(LOGIN_PATH);
}
function handleNoticeClear() {

View File

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

View File

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

View File

@@ -92,8 +92,10 @@ function setupAccessGuard(router: Router) {
return to;
}
const accessRoutes = accessStore.accessRoutes;
// 是否已经生成过动态路由
if (accessStore.isAccessChecked) {
if (accessRoutes && accessRoutes.length > 0) {
return true;
}
@@ -113,11 +115,10 @@ function setupAccessGuard(router: Router) {
// 保存菜单信息和路由信息
accessStore.setAccessMenus(accessibleMenus);
accessStore.setAccessRoutes(accessibleRoutes);
accessStore.setIsAccessChecked(true);
const redirectPath = (from.query.redirect ?? to.fullPath) as string;
const redirectPath = (from.query.redirect ?? to.path) as string;
return {
...router.resolve(decodeURIComponent(redirectPath)),
path: decodeURIComponent(redirectPath),
replace: 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);
/** 外部路由列表,访问这些页面可以不需要Layout可能用于内嵌在别的系统 */
// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
const externalRoutes: RouteRecordRaw[] = [];
/** 静态路由列表,访问这些页面可以不需要权限 */
// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
const staticRoutes: RouteRecordRaw[] = [];
/** 路由列表,由基本路由+静态路由组成 */
const routes: RouteRecordRaw[] = [
...coreRoutes,
...externalRoutes,
...staticRoutes,
fallbackNotFoundRoute,
];

View File

@@ -17,6 +17,7 @@ const routes: RouteRecordRaw[] = [
children: [
{
meta: {
icon: 'mdi:shield-key-outline',
title: $t('page.demos.element-plus'),
},
name: 'NaiveDemos',

View File

@@ -1,11 +1,11 @@
import type { RouteRecordRaw } from 'vue-router';
import {
VBEN_ANT_PREVIEW_URL,
VBEN_DOC_URL,
VBEN_GITHUB_URL,
VBEN_LOGO_URL,
VBEN_NAIVE_PREVIEW_URL,
VBEN_PREVIEW_URL,
} from '@vben/constants';
import { BasicLayout, IFrameView } from '#/layouts';
@@ -26,7 +26,7 @@ const routes: RouteRecordRaw[] = [
{
name: 'VbenAbout',
path: '/vben-admin/about',
component: () => import('#/views/_core/about/index.vue'),
component: () => import('#/views/_core/vben/about/index.vue'),
meta: {
icon: 'lucide:copyright',
title: $t('page.vben.about'),
@@ -38,7 +38,8 @@ const routes: RouteRecordRaw[] = [
component: IFrameView,
meta: {
icon: 'lucide:book-open-text',
link: VBEN_DOC_URL,
iframeSrc: VBEN_DOC_URL,
keepAlive: true,
title: $t('page.vben.document'),
},
},
@@ -68,7 +69,7 @@ const routes: RouteRecordRaw[] = [
component: IFrameView,
meta: {
badgeType: 'dot',
link: VBEN_ANT_PREVIEW_URL,
link: VBEN_PREVIEW_URL,
title: $t('page.vben.antdv'),
},
},

View File

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

View File

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

View File

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

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