mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-08-26 08:36:19 +08:00
Compare commits
148 Commits
doc
...
v5.3.0-bet
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f3e7438a49 | ||
![]() |
b417ac2469 | ||
![]() |
524b9badf2 | ||
![]() |
86ed732ca8 | ||
![]() |
56e66193fc | ||
![]() |
b1636405fc | ||
![]() |
ad89ea7a75 | ||
![]() |
7b3c9d56be | ||
![]() |
1cff0b9753 | ||
![]() |
31d5f03b45 | ||
![]() |
2b65e935c1 | ||
![]() |
58f2b17bde | ||
![]() |
46a9fac38e | ||
![]() |
41612f7723 | ||
![]() |
83ecae7c4e | ||
![]() |
3332b20fd0 | ||
![]() |
2d0153a841 | ||
![]() |
95a4a85c3b | ||
![]() |
3f2dcb8281 | ||
![]() |
67f3d63066 | ||
![]() |
277e98c42c | ||
![]() |
1063b2268e | ||
![]() |
8404c12129 | ||
![]() |
a7d322019e | ||
![]() |
f23821ff46 | ||
![]() |
2b0aedbaba | ||
![]() |
071cc0dcec | ||
![]() |
7db7d6ec5f | ||
![]() |
b3e3e05990 | ||
![]() |
cc678a2b51 | ||
![]() |
7d2bcf476f | ||
![]() |
cfbe379ee4 | ||
![]() |
388e5b5cb3 | ||
![]() |
c1dfbc1ebf | ||
![]() |
05a52b0540 | ||
![]() |
a77cc00e9f | ||
![]() |
98da0672e7 | ||
![]() |
be3bcc1122 | ||
![]() |
88a7a9b1ee | ||
![]() |
3b2ed948f5 | ||
![]() |
08acaf05f1 | ||
![]() |
36e7ca19a1 | ||
![]() |
84816ef769 | ||
![]() |
1bd6c2e5f2 | ||
![]() |
c439d5601f | ||
![]() |
453a3a8f84 | ||
![]() |
c6b9a56b73 | ||
![]() |
6d24369272 | ||
![]() |
cbf601581d | ||
![]() |
d47d051b19 | ||
![]() |
3b5c935bb8 | ||
![]() |
8adb22847d | ||
![]() |
2ba28488a4 | ||
![]() |
d2f3a9d04f | ||
![]() |
dc5cfab319 | ||
![]() |
87cc8a154c | ||
![]() |
54a622ad05 | ||
![]() |
8a0b1e0c72 | ||
![]() |
fd7b3479b4 | ||
![]() |
577cc85851 | ||
![]() |
5ca746b021 | ||
![]() |
04f29614aa | ||
![]() |
df7757d001 | ||
![]() |
20a3868594 | ||
![]() |
edb55b1fc0 | ||
![]() |
137e2073cb | ||
![]() |
bc04286f1d | ||
![]() |
7fa4410eab | ||
![]() |
8230493651 | ||
![]() |
c27b97f933 | ||
![]() |
d5655f02e3 | ||
![]() |
e2732f334c | ||
![]() |
5b24661b78 | ||
![]() |
97e7bd6827 | ||
![]() |
08265262e1 | ||
![]() |
c3631ef555 | ||
![]() |
18627833e9 | ||
![]() |
cd652941cd | ||
![]() |
fab92ee7e1 | ||
![]() |
04c2b9d242 | ||
![]() |
2288827265 | ||
![]() |
fecf55139d | ||
![]() |
5962804f92 | ||
![]() |
4d7e6bba56 | ||
![]() |
33637584a8 | ||
![]() |
e7a4ab70d5 | ||
![]() |
1db87ff7ce | ||
![]() |
01d60336a6 | ||
![]() |
f8485e8861 | ||
![]() |
9120d20143 | ||
![]() |
5f41c51770 | ||
![]() |
3c17f4e9f8 | ||
![]() |
66808582ff | ||
![]() |
0faf7810b6 | ||
![]() |
8987067b5a | ||
![]() |
d71a20ad0a | ||
![]() |
eb280ffeb7 | ||
![]() |
debb32d353 | ||
![]() |
11551903f0 | ||
![]() |
187f946d2a | ||
![]() |
e2d9d08ced | ||
![]() |
30223f18db | ||
![]() |
9c6e059aac | ||
![]() |
8f40d5107c | ||
![]() |
83fcdec37c | ||
![]() |
b28740042b | ||
![]() |
6a1e6ad99f | ||
![]() |
3b0f72330a | ||
![]() |
963e79063f | ||
![]() |
4793c4c0db | ||
![]() |
1a4d61cc17 | ||
![]() |
738bc456c8 | ||
![]() |
e8dff517ba | ||
![]() |
843ec1e749 | ||
![]() |
f20c5d9e2e | ||
![]() |
6e6e35ae4a | ||
![]() |
bf021a0578 | ||
![]() |
7b46780af7 | ||
![]() |
3f9ce63868 | ||
![]() |
517acada1a | ||
![]() |
3015912f1a | ||
![]() |
b464b87ac5 | ||
![]() |
654bf90c0d | ||
![]() |
9d6cc22dfa | ||
![]() |
7a9ad7de63 | ||
![]() |
cdeadafda5 | ||
![]() |
ec49a04151 | ||
![]() |
4d4327cb25 | ||
![]() |
6c117b4130 | ||
![]() |
8725a01301 | ||
![]() |
ed14282999 | ||
![]() |
07cdebb7f1 | ||
![]() |
344c499462 | ||
![]() |
992b9bae6c | ||
![]() |
5b56c300ab | ||
![]() |
ca164b0f9b | ||
![]() |
9487156938 | ||
![]() |
f1e0278bd8 | ||
![]() |
de90b07b6f | ||
![]() |
3f04f6b01f | ||
![]() |
31dd0d5b40 | ||
![]() |
8ffc853b86 | ||
![]() |
a9a14fd81a | ||
![]() |
d41bd31fcd | ||
![]() |
5f4d3f2bce | ||
![]() |
a27b1c40e5 | ||
![]() |
1d38fb647e | ||
![]() |
861f39b519 |
45
.github/contributing.md
vendored
45
.github/contributing.md
vendored
@@ -1,5 +1,42 @@
|
||||
# Contributing Guide
|
||||
# Vben Admin Contributing Guide
|
||||
|
||||
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.
|
||||
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
|
||||
```
|
||||
|
14
.github/release-drafter.yml
vendored
14
.github/release-drafter.yml
vendored
@@ -13,18 +13,21 @@ categories:
|
||||
- title: "🚀 Features"
|
||||
labels:
|
||||
- "feature"
|
||||
- "enhancement"
|
||||
- title: "🐞 Bug Fixes"
|
||||
labels:
|
||||
- "bug"
|
||||
- title: 📝 Documentation updates
|
||||
- title: "📈 Performance"
|
||||
labels:
|
||||
- "perf"
|
||||
- "enhancement"
|
||||
- title: 📝 Documentation
|
||||
labels:
|
||||
- "documentation"
|
||||
- title: 👻 Maintenance
|
||||
labels:
|
||||
- "chore"
|
||||
- "dependencies"
|
||||
collapse-after: 5
|
||||
# collapse-after: 12
|
||||
- title: 🚦 Tests
|
||||
labels:
|
||||
- "tests"
|
||||
@@ -34,12 +37,15 @@ categories:
|
||||
version-resolver:
|
||||
major:
|
||||
labels:
|
||||
- "major"
|
||||
- "breaking"
|
||||
minor:
|
||||
labels:
|
||||
- "feature"
|
||||
- "minor"
|
||||
patch:
|
||||
labels:
|
||||
- "feature"
|
||||
- "patch"
|
||||
- "bug"
|
||||
- "maintenance"
|
||||
- "docs"
|
||||
|
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
143
.github/workflows/deploy.yml
vendored
143
.github/workflows/deploy.yml
vendored
@@ -6,8 +6,63 @@ on:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy-push-ftp:
|
||||
name: Deploy Push Ftp
|
||||
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
|
||||
if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -22,9 +77,65 @@ 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
|
||||
@@ -33,36 +144,12 @@ jobs:
|
||||
uses: ./.github/actions/setup-node
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
run: pnpm run build:naive
|
||||
|
||||
- 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
|
||||
- name: Sync 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/
|
||||
|
7
.github/workflows/draft.yml
vendored
7
.github/workflows/draft.yml
vendored
@@ -7,9 +7,16 @@ 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
|
||||
|
2
.github/workflows/issue-close-require.yml
vendored
2
.github/workflows/issue-close-require.yml
vendored
@@ -21,5 +21,5 @@ jobs:
|
||||
with:
|
||||
actions: "close-issues" # 执行动作:关闭 Issues
|
||||
token: ${{ secrets.GITHUB_TOKEN }} # GitHub Token,用于认证
|
||||
labels: "need reproduction" # 目标标签
|
||||
labels: "needs reproduction" # 目标标签
|
||||
inactive-day: 3 # 未活动天数阈值
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -48,3 +48,4 @@ vite.config.ts.*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.history
|
||||
|
28
.ls-lint.yml
28
.ls-lint.yml
@@ -1,28 +0,0 @@
|
||||
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
2
.npmrc
@@ -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
|
||||
|
4
.vscode/extensions.json
vendored
4
.vscode/extensions.json
vendored
@@ -10,10 +10,6 @@
|
||||
"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
31
.vscode/launch.json
vendored
@@ -4,12 +4,39 @@
|
||||
"configurations": [
|
||||
{
|
||||
"type": "chrome",
|
||||
"name": "vben admin antd dev",
|
||||
"name": "vben admin playground dev",
|
||||
"request": "launch",
|
||||
"url": "http://localhost:5555",
|
||||
"env": { "NODE_ENV": "development" },
|
||||
"sourceMaps": true,
|
||||
"webRoot": "${workspaceFolder}/apps/web-antd/src"
|
||||
"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}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
43
.vscode/settings.json
vendored
43
.vscode/settings.json
vendored
@@ -14,7 +14,6 @@
|
||||
"editor.tabSize": 2,
|
||||
"editor.detectIndentation": false,
|
||||
"editor.cursorBlinking": "expand",
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.largeFileOptimizations": false,
|
||||
"editor.accessibilitySupport": "off",
|
||||
"editor.cursorSmoothCaretAnimation": "on",
|
||||
@@ -37,7 +36,34 @@
|
||||
"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,
|
||||
|
||||
@@ -56,7 +82,8 @@
|
||||
"*.ejs": "html",
|
||||
"*.art": "html",
|
||||
"**/tsconfig.json": "jsonc",
|
||||
"*.json": "jsonc"
|
||||
"*.json": "jsonc",
|
||||
"package.json": "json"
|
||||
},
|
||||
|
||||
"files.exclude": {
|
||||
@@ -167,8 +194,10 @@
|
||||
|
||||
"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",
|
||||
@@ -183,11 +212,13 @@
|
||||
"*.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*",
|
||||
"eslint.config.mjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,.ls-lint*,cspell.json",
|
||||
"Dockerfile": "Dockerfile,.docker*,docker-entrypoint.sh,build-local-docker*,nginx.conf",
|
||||
"eslint.config.mjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json",
|
||||
"tailwind.config.mjs": "postcss.*"
|
||||
},
|
||||
"commentTranslate.hover.enabled": false,
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"commentTranslate.multiLineMerge": true
|
||||
"commentTranslate.multiLineMerge": true,
|
||||
"vue.server.hybridMode": true,
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
|
@@ -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/apps/web-antd/dist /usr/share/nginx/html
|
||||
COPY --from=builder /app/playground/dist /usr/share/nginx/html
|
||||
|
||||
COPY ./deploy/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY ./nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
|
149
README.ja-JP.md
Normal file
149
README.ja-JP.md
Normal file
@@ -0,0 +1,149 @@
|
||||
<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)
|
||||
|
||||
<h1>Vue Vben Admin</h1>
|
||||
</div>
|
||||
|
||||
[](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin)    
|
||||
|
||||
**日本語** | [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を使用
|
||||
|
||||
Gitpod(GitHub用の無料オンライン開発環境)でプロジェクトを開き、すぐにコーディングを開始します。
|
||||
|
||||
[](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)
|
||||
|
||||
## 貢献方法
|
||||
|
||||
ご参加をお待ちしておりますするか、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)
|
||||
|
||||
## 寄付
|
||||
|
||||
このプロジェクトが役に立つと思われた場合、作者にコーヒーを一杯おごってサポートを示すことができます!
|
||||
|
||||

|
||||
|
||||
<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)
|
13
README.md
13
README.md
@@ -1,11 +1,13 @@
|
||||
<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>
|
||||
<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)
|
||||
|
||||
<h1>Vue Vben Admin</h1>
|
||||
</div>
|
||||
|
||||
**English** | [中文](./README.zh-CN.md)
|
||||
[](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin)    
|
||||
|
||||
**English** | [中文](./README.zh-CN.md) | [日本語](./README.ja-JP.md)
|
||||
|
||||
## Introduction
|
||||
|
||||
@@ -77,7 +79,7 @@ pnpm build
|
||||
|
||||
## Change Log
|
||||
|
||||
[CHANGELOG](./CHANGELOG.zh_CN.md)
|
||||
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases)
|
||||
|
||||
## How to contribute
|
||||
|
||||
@@ -126,14 +128,15 @@ 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!
|
||||
|
||||

|
||||

|
||||
|
||||
<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 src="https://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
|
||||
<img alt="Contributors"
|
||||
src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
|
||||
</a>
|
||||
|
||||
## Discord
|
||||
|
@@ -1,11 +1,13 @@
|
||||
<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>
|
||||
<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)
|
||||
|
||||
<h1>Vue Vben Admin</h1>
|
||||
</div>
|
||||
|
||||
**中文** | [English](./README.md)
|
||||
[](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin)    
|
||||
|
||||
**中文** | [English](./README.md) | [日本語](./README.ja-JP.md)
|
||||
|
||||
## 简介
|
||||
|
||||
@@ -122,10 +124,21 @@ pnpm build
|
||||
|
||||
如果你觉得这个项目对你有帮助,你可以帮作者买一杯咖啡表示支持!
|
||||
|
||||

|
||||

|
||||
|
||||
<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)
|
||||
|
@@ -1 +1,3 @@
|
||||
PORT=5320
|
||||
ACCESS_TOKEN_SECRET=access_token_secret
|
||||
REFRESH_TOKEN_SECRET=refresh_token_secret
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
## Description
|
||||
|
||||
Vben Admin 数据 mock 服务,没有对接任何的数据库,所有数据都是模拟的,用于前端开发时提供数据支持。由于 sqlite 安装需要在本地进行编译,所以这里接口是直接返回的。线上环境不再提供mock集成,可自行部署服务或者对接真实数据,同步 mock.js等工具有一些限制,比如上传文件不行、无法模拟复杂的逻辑等,所以这里使用了 真是的后端服务来实现。唯一麻烦的是本地需要同时启动后端服务和前端服务,但是这样可以更好的模拟真实环境。
|
||||
Vben Admin 数据 mock 服务,没有对接任何的数据库,所有数据都是模拟的,用于前端开发时提供数据支持。线上环境不再提供 mock 集成,可自行部署服务或者对接真实数据,由于 `mock.js` 等工具有一些限制,比如上传文件不行、无法模拟复杂的逻辑等,所以这里使用了真实的后端服务来实现。唯一麻烦的是本地需要同时启动后端服务和前端服务,但是这样可以更好的模拟真实环境。该服务不需要手动启动,已经集成在 vite 插件内,随应用一起启用。
|
||||
|
||||
## Running the app
|
||||
|
||||
|
@@ -1,15 +1,14 @@
|
||||
export default eventHandler((event) => {
|
||||
const token = getHeader(event, 'Authorization');
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { unAuthorizedResponse } from '~/utils/response';
|
||||
|
||||
if (!token) {
|
||||
setResponseStatus(event, 401);
|
||||
return useResponseError('UnauthorizedException', 'Unauthorized Exception');
|
||||
export default eventHandler((event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
if (!userinfo) {
|
||||
return unAuthorizedResponse(event);
|
||||
}
|
||||
|
||||
const username = Buffer.from(token, 'base64').toString('utf8');
|
||||
|
||||
const codes =
|
||||
MOCK_CODES.find((item) => item.username === username)?.codes ?? [];
|
||||
MOCK_CODES.find((item) => item.username === userinfo.username)?.codes ?? [];
|
||||
|
||||
return useResponseSuccess(codes);
|
||||
});
|
||||
|
@@ -1,20 +1,36 @@
|
||||
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) {
|
||||
setResponseStatus(event, 403);
|
||||
return useResponseError('UnauthorizedException', '用户名或密码错误');
|
||||
clearRefreshTokenCookie(event);
|
||||
return forbiddenResponse(event);
|
||||
}
|
||||
|
||||
const accessToken = Buffer.from(username).toString('base64');
|
||||
const accessToken = generateAccessToken(findUser);
|
||||
const refreshToken = generateRefreshToken(findUser);
|
||||
|
||||
setRefreshTokenCookie(event, refreshToken);
|
||||
|
||||
return useResponseSuccess({
|
||||
...findUser,
|
||||
accessToken,
|
||||
// TODO: refresh token
|
||||
refreshToken: accessToken,
|
||||
});
|
||||
});
|
||||
|
15
apps/backend-mock/api/auth/logout.post.ts
Normal file
15
apps/backend-mock/api/auth/logout.post.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import {
|
||||
clearRefreshTokenCookie,
|
||||
getRefreshTokenFromCookie,
|
||||
} from '~/utils/cookie-utils';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const refreshToken = getRefreshTokenFromCookie(event);
|
||||
if (!refreshToken) {
|
||||
return useResponseSuccess('');
|
||||
}
|
||||
|
||||
clearRefreshTokenCookie(event);
|
||||
|
||||
return useResponseSuccess('');
|
||||
});
|
33
apps/backend-mock/api/auth/refresh.post.ts
Normal file
33
apps/backend-mock/api/auth/refresh.post.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
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;
|
||||
});
|
@@ -1,14 +1,13 @@
|
||||
export default eventHandler((event) => {
|
||||
const token = getHeader(event, 'Authorization');
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { unAuthorizedResponse } from '~/utils/response';
|
||||
|
||||
if (!token) {
|
||||
setResponseStatus(event, 401);
|
||||
return useResponseError('UnauthorizedException', 'Unauthorized Exception');
|
||||
export default eventHandler((event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
if (!userinfo) {
|
||||
return unAuthorizedResponse(event);
|
||||
}
|
||||
|
||||
const username = Buffer.from(token, 'base64').toString('utf8');
|
||||
|
||||
const menus =
|
||||
MOCK_MENUS.find((item) => item.username === username)?.menus ?? [];
|
||||
MOCK_MENUS.find((item) => item.username === userinfo.username)?.menus ?? [];
|
||||
return useResponseSuccess(menus);
|
||||
});
|
||||
|
@@ -1,14 +1,11 @@
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { unAuthorizedResponse } from '~/utils/response';
|
||||
|
||||
export default eventHandler((event) => {
|
||||
const token = getHeader(event, 'Authorization');
|
||||
if (!token) {
|
||||
setResponseStatus(event, 401);
|
||||
return useResponseError('UnauthorizedException', 'Unauthorized Exception');
|
||||
const userinfo = verifyAccessToken(event);
|
||||
if (!userinfo) {
|
||||
return unAuthorizedResponse(event);
|
||||
}
|
||||
|
||||
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);
|
||||
return useResponseSuccess(userinfo);
|
||||
});
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import type { NitroErrorHandler } from 'nitropack';
|
||||
|
||||
const errorHandler: NitroErrorHandler = function (error, event) {
|
||||
event.res.end(`[error handler] ${error.stack}`);
|
||||
event.node.res.end(`[Error Handler] ${error.stack}`);
|
||||
};
|
||||
|
||||
export default errorHandler;
|
||||
|
@@ -1,11 +1,4 @@
|
||||
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.';
|
||||
|
@@ -6,10 +6,15 @@
|
||||
"license": "MIT",
|
||||
"author": "",
|
||||
"scripts": {
|
||||
"start": "nitro dev",
|
||||
"build": "nitro build"
|
||||
"build": "nitro build",
|
||||
"start": "nitro dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"nitropack": "^2.9.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"h3": "^1.12.0"
|
||||
}
|
||||
}
|
||||
|
26
apps/backend-mock/utils/cookie-utils.ts
Normal file
26
apps/backend-mock/utils/cookie-utils.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
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;
|
||||
}
|
59
apps/backend-mock/utils/jwt-utils.ts
Normal file
59
apps/backend-mock/utils/jwt-utils.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
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;
|
||||
}
|
||||
}
|
@@ -1,4 +1,12 @@
|
||||
export const MOCK_USERS = [
|
||||
export interface UserInfo {
|
||||
id: number;
|
||||
password: string;
|
||||
realName: string;
|
||||
roles: string[];
|
||||
username: string;
|
||||
}
|
||||
|
||||
export const MOCK_USERS: UserInfo[] = [
|
||||
{
|
||||
id: 0,
|
||||
password: '123456',
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import type { EventHandlerRequest, H3Event } from 'h3';
|
||||
|
||||
export function useResponseSuccess<T = any>(data: T) {
|
||||
return {
|
||||
code: 0,
|
||||
@@ -15,3 +17,13 @@ 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');
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
# 应用标题
|
||||
VITE_APP_TITLE=Vben Admin
|
||||
VITE_APP_TITLE=Vben Admin Antd
|
||||
|
||||
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离
|
||||
VITE_APP_NAMESPACE=vben-web-antd
|
||||
|
@@ -1,5 +1,5 @@
|
||||
# 端口号
|
||||
VITE_PORT=5555
|
||||
VITE_PORT=5666
|
||||
|
||||
VITE_BASE=/
|
||||
|
||||
|
@@ -14,3 +14,6 @@ VITE_ROUTER_HISTORY=hash
|
||||
|
||||
# 是否注入全局loading
|
||||
VITE_INJECT_APP_LOADING=true
|
||||
|
||||
# 打包后是否生成dist.zip
|
||||
VITE_ARCHIVER=true
|
||||
|
@@ -21,7 +21,7 @@
|
||||
(function () {
|
||||
var hm = document.createElement('script');
|
||||
hm.src =
|
||||
'https://hm.baidu.com/hm.js?d20a01273820422b6aa2ee41b6c9414d';
|
||||
'https://hm.baidu.com/hm.js?b38e689f40558f20a9a686d7f6f33edf';
|
||||
var s = document.getElementsByTagName('script')[0];
|
||||
s.parentNode.insertBefore(hm, s);
|
||||
})();
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/web-antd",
|
||||
"version": "5.0.0",
|
||||
"version": "5.3.0-beta.1",
|
||||
"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": "^10.11.0",
|
||||
"@vueuse/core": "^11.0.3",
|
||||
"ant-design-vue": "^4.2.3",
|
||||
"dayjs": "^1.11.12",
|
||||
"pinia": "2.2.0",
|
||||
"vue": "^3.4.35",
|
||||
"vue-router": "^4.4.2"
|
||||
"dayjs": "^1.11.13",
|
||||
"pinia": "2.2.2",
|
||||
"vue": "^3.5.4",
|
||||
"vue-router": "^4.4.3"
|
||||
}
|
||||
}
|
||||
|
114
apps/web-antd/src/adapter/form.ts
Normal file
114
apps/web-antd/src/adapter/form.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import type {
|
||||
BaseFormComponentType,
|
||||
VbenFormSchema as FormSchema,
|
||||
VbenFormProps,
|
||||
} from '@vben/common-ui';
|
||||
|
||||
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,
|
||||
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'
|
||||
| 'TimePicker'
|
||||
| 'TreeSelect'
|
||||
| 'Upload'
|
||||
| BaseFormComponentType;
|
||||
|
||||
// 初始化表单组件,并注册到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,
|
||||
InputNumber,
|
||||
InputPassword,
|
||||
Mentions,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
RangePicker,
|
||||
Rate,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
TimePicker,
|
||||
TreeSelect,
|
||||
Upload,
|
||||
},
|
||||
config: {
|
||||
baseModelPropName: 'value',
|
||||
modelPropNameMap: {
|
||||
Checkbox: 'checked',
|
||||
Radio: 'checked',
|
||||
Switch: 'checked',
|
||||
Upload: 'fileList',
|
||||
},
|
||||
},
|
||||
defineRules: {
|
||||
required: (value, _params, ctx) => {
|
||||
if ((!value && value !== 0) || value.length === 0) {
|
||||
return $t('formRules.required', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const useVbenForm = useForm<FormComponentType>;
|
||||
|
||||
export { useVbenForm, z };
|
||||
|
||||
export type VbenFormSchema = FormSchema<FormComponentType>;
|
||||
export type { VbenFormProps };
|
1
apps/web-antd/src/adapter/index.ts
Normal file
1
apps/web-antd/src/adapter/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './form';
|
@@ -1,4 +1,4 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
import { baseRequestClient, requestClient } from '#/api/request';
|
||||
|
||||
export namespace AuthApi {
|
||||
/** 登录接口参数 */
|
||||
@@ -12,10 +12,14 @@ export namespace AuthApi {
|
||||
accessToken: string;
|
||||
desc: string;
|
||||
realName: string;
|
||||
refreshToken: string;
|
||||
userId: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface RefreshTokenResult {
|
||||
data: string;
|
||||
status: number;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,6 +29,24 @@ 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,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户权限码
|
||||
*/
|
||||
|
@@ -1,2 +1 @@
|
||||
export * from './core';
|
||||
export * from './demos';
|
||||
|
@@ -1,67 +1,104 @@
|
||||
/**
|
||||
* 该文件可自行根据业务逻辑进行调整
|
||||
*/
|
||||
import type { HttpResponse } from '@vben/request';
|
||||
|
||||
import { useAppConfig } from '@vben/hooks';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { RequestClient } from '@vben/request';
|
||||
import {
|
||||
authenticateResponseInterceptor,
|
||||
errorMessageResponseInterceptor,
|
||||
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);
|
||||
|
||||
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;
|
||||
/**
|
||||
* 重新认证逻辑
|
||||
*/
|
||||
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();
|
||||
}
|
||||
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({
|
||||
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 });
|
||||
|
23
apps/web-antd/src/layouts/auth.vue
Normal file
23
apps/web-antd/src/layouts/auth.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<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>
|
@@ -2,10 +2,9 @@
|
||||
import type { NotificationItem } from '@vben/layouts';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
|
||||
import { LOGIN_PATH, VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
|
||||
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
|
||||
import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
|
||||
import {
|
||||
BasicLayout,
|
||||
@@ -14,17 +13,12 @@ import {
|
||||
UserDropdown,
|
||||
} from '@vben/layouts';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import {
|
||||
resetAllStores,
|
||||
storeToRefs,
|
||||
useAccessStore,
|
||||
useUserStore,
|
||||
} from '@vben/stores';
|
||||
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||
import { openWindow } from '@vben/utils';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
import { resetRoutes } from '#/router';
|
||||
import { useAuthStore } from '#/store';
|
||||
import LoginForm from '#/views/_core/authentication/login.vue';
|
||||
|
||||
const notifications = ref<NotificationItem[]>([
|
||||
{
|
||||
@@ -94,18 +88,12 @@ const menus = computed(() => [
|
||||
},
|
||||
]);
|
||||
|
||||
const { loginLoading } = storeToRefs(authStore);
|
||||
|
||||
const avatar = computed(() => {
|
||||
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
async function handleLogout() {
|
||||
resetAllStores();
|
||||
resetRoutes();
|
||||
await router.replace(LOGIN_PATH);
|
||||
await authStore.logout(false);
|
||||
}
|
||||
|
||||
function handleNoticeClear() {
|
||||
@@ -141,11 +129,9 @@ function handleMakeAll() {
|
||||
<AuthenticationLoginExpiredModal
|
||||
v-model:open="accessStore.loginExpired"
|
||||
:avatar
|
||||
:loading="loginLoading"
|
||||
password-placeholder="123456"
|
||||
username-placeholder="vben"
|
||||
@submit="authStore.authLogin"
|
||||
/>
|
||||
>
|
||||
<LoginForm />
|
||||
</AuthenticationLoginExpiredModal>
|
||||
</template>
|
||||
<template #lock-screen>
|
||||
<LockScreen :avatar @to-login="handleLogout" />
|
||||
|
@@ -1,8 +1,6 @@
|
||||
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 };
|
||||
|
@@ -58,7 +58,11 @@ async function loadDayjsLocale(lang: SupportedLanguagesType) {
|
||||
locale = await import('dayjs/locale/en');
|
||||
}
|
||||
}
|
||||
dayjs.locale(locale);
|
||||
if (locale) {
|
||||
dayjs.locale(locale);
|
||||
} else {
|
||||
console.error(`Failed to load dayjs locale for ${lang}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,4 +91,4 @@ async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
export { $t, antdLocale, loadMessages, setupI18n };
|
||||
export { $t, antdLocale, setupI18n };
|
||||
|
@@ -2,58 +2,7 @@
|
||||
"page": {
|
||||
"demos": {
|
||||
"title": "Demos",
|
||||
"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"
|
||||
}
|
||||
"antd": "Ant Design Vue"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,60 +2,7 @@
|
||||
"page": {
|
||||
"demos": {
|
||||
"title": "演示",
|
||||
"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": "平级模式详情"
|
||||
}
|
||||
"antd": "Ant Design Vue"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -92,10 +92,8 @@ function setupAccessGuard(router: Router) {
|
||||
return to;
|
||||
}
|
||||
|
||||
const accessRoutes = accessStore.accessRoutes;
|
||||
|
||||
// 是否已经生成过动态路由
|
||||
if (accessRoutes && accessRoutes.length > 0) {
|
||||
if (accessStore.isAccessChecked) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -115,10 +113,11 @@ function setupAccessGuard(router: Router) {
|
||||
// 保存菜单信息和路由信息
|
||||
accessStore.setAccessMenus(accessibleMenus);
|
||||
accessStore.setAccessRoutes(accessibleRoutes);
|
||||
const redirectPath = (from.query.redirect ?? to.path) as string;
|
||||
accessStore.setIsAccessChecked(true);
|
||||
const redirectPath = (from.query.redirect ?? to.fullPath) as string;
|
||||
|
||||
return {
|
||||
path: decodeURIComponent(redirectPath),
|
||||
...router.resolve(decodeURIComponent(redirectPath)),
|
||||
replace: true,
|
||||
};
|
||||
});
|
||||
|
@@ -9,19 +9,19 @@ const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
|
||||
});
|
||||
|
||||
// 有需要可以自行打开注释,并创建文件夹
|
||||
// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
|
||||
// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
|
||||
|
||||
/** 动态路由 */
|
||||
const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
|
||||
|
||||
/** 静态路由列表,访问这些页面可以不需要权限 */
|
||||
// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
|
||||
const staticRoutes: RouteRecordRaw[] = [];
|
||||
/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统 */
|
||||
// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
|
||||
const externalRoutes: RouteRecordRaw[] = [];
|
||||
|
||||
/** 路由列表,由基本路由+静态路由组成 */
|
||||
const routes: RouteRecordRaw[] = [
|
||||
...coreRoutes,
|
||||
...staticRoutes,
|
||||
...externalRoutes,
|
||||
fallbackNotFoundRoute,
|
||||
];
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { BasicLayout, IFrameView } from '#/layouts';
|
||||
import { BasicLayout } from '#/layouts';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
@@ -15,477 +15,13 @@ const routes: RouteRecordRaw[] = [
|
||||
name: 'Demos',
|
||||
path: '/demos',
|
||||
children: [
|
||||
// 权限控制
|
||||
{
|
||||
meta: {
|
||||
icon: 'mdi:shield-key-outline',
|
||||
title: $t('page.demos.access.frontendPermissions'),
|
||||
title: $t('page.demos.antd'),
|
||||
},
|
||||
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'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
name: 'AntDesignDemos',
|
||||
path: '/demos/ant-design',
|
||||
component: () => import('#/views/demos/antd/index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@@ -26,7 +26,7 @@ const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
name: 'VbenAbout',
|
||||
path: '/vben-admin/about',
|
||||
component: () => import('#/views/_core/vben/about/index.vue'),
|
||||
component: () => import('#/views/_core/about/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:copyright',
|
||||
title: $t('page.vben.about'),
|
||||
@@ -38,8 +38,7 @@ const routes: RouteRecordRaw[] = [
|
||||
component: IFrameView,
|
||||
meta: {
|
||||
icon: 'lucide:book-open-text',
|
||||
iframeSrc: VBEN_DOC_URL,
|
||||
keepAlive: true,
|
||||
link: VBEN_DOC_URL,
|
||||
title: $t('page.vben.document'),
|
||||
},
|
||||
},
|
||||
|
@@ -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 } from '#/api';
|
||||
import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
@@ -33,13 +33,11 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
let userInfo: null | UserInfo = null;
|
||||
try {
|
||||
loginLoading.value = true;
|
||||
const { accessToken, refreshToken } = await loginApi(params);
|
||||
const { accessToken } = await loginApi(params);
|
||||
|
||||
// 如果成功获取到 accessToken
|
||||
if (accessToken) {
|
||||
// 将 accessToken 存储到 accessStore 中
|
||||
accessStore.setAccessToken(accessToken);
|
||||
accessStore.setRefreshToken(refreshToken);
|
||||
|
||||
// 获取用户信息并存储到 accessStore 中
|
||||
const [fetchUserInfoResult, accessCodes] = await Promise.all([
|
||||
@@ -77,16 +75,19 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
};
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
async function logout(redirect: boolean = true) {
|
||||
await logoutApi();
|
||||
resetAllStores();
|
||||
accessStore.setLoginExpired(false);
|
||||
|
||||
// 回登陆页带上当前路由地址
|
||||
await router.replace({
|
||||
path: LOGIN_PATH,
|
||||
query: {
|
||||
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
|
||||
},
|
||||
query: redirect
|
||||
? {
|
||||
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
|
||||
}
|
||||
: {},
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -1,15 +1,49 @@
|
||||
<script lang="ts" setup>
|
||||
import type { LoginCodeParams } from '@vben/common-ui';
|
||||
import type { LoginCodeParams, VbenFormSchema } from '@vben/common-ui';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { AuthenticationCodeLogin } from '@vben/common-ui';
|
||||
import { LOGIN_PATH } from '@vben/constants';
|
||||
import { AuthenticationCodeLogin, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
defineOptions({ name: 'CodeLogin' });
|
||||
|
||||
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
|
||||
@@ -23,8 +57,8 @@ async function handleLogin(values: LoginCodeParams) {
|
||||
|
||||
<template>
|
||||
<AuthenticationCodeLogin
|
||||
:form-schema="formSchema"
|
||||
:loading="loading"
|
||||
:login-path="LOGIN_PATH"
|
||||
@submit="handleLogin"
|
||||
/>
|
||||
</template>
|
||||
|
@@ -1,13 +1,32 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
|
||||
import { AuthenticationForgetPassword } from '@vben/common-ui';
|
||||
import { LOGIN_PATH } from '@vben/constants';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { AuthenticationForgetPassword, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
defineOptions({ name: 'ForgetPassword' });
|
||||
|
||||
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) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('reset email:', value);
|
||||
@@ -16,8 +35,8 @@ function handleSubmit(value: string) {
|
||||
|
||||
<template>
|
||||
<AuthenticationForgetPassword
|
||||
:form-schema="formSchema"
|
||||
:loading="loading"
|
||||
:login-path="LOGIN_PATH"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
|
@@ -1,18 +1,91 @@
|
||||
<script lang="ts" setup>
|
||||
import { AuthenticationLogin } from '@vben/common-ui';
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
import type { BasicOption } from '@vben/types';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { AuthenticationLogin, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
defineOptions({ name: 'Login' });
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const MOCK_USER_OPTIONS: BasicOption[] = [
|
||||
{
|
||||
label: '超级管理员',
|
||||
value: 'vben',
|
||||
},
|
||||
{
|
||||
label: '管理员',
|
||||
value: 'admin',
|
||||
},
|
||||
{
|
||||
label: '用户',
|
||||
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') }),
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationLogin
|
||||
:form-schema="formSchema"
|
||||
:loading="authStore.loginLoading"
|
||||
password-placeholder="123456"
|
||||
username-placeholder="vben"
|
||||
@submit="authStore.authLogin"
|
||||
/>
|
||||
</template>
|
||||
|
@@ -1,15 +1,91 @@
|
||||
<script lang="ts" setup>
|
||||
import type { LoginAndRegisterParams } from '@vben/common-ui';
|
||||
import type { LoginAndRegisterParams, VbenFormSchema } from '@vben/common-ui';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { computed, h, ref } from 'vue';
|
||||
|
||||
import { AuthenticationRegister } from '@vben/common-ui';
|
||||
import { LOGIN_PATH } from '@vben/constants';
|
||||
import { AuthenticationRegister, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
defineOptions({ name: 'Register' });
|
||||
|
||||
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) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('register submit:', value);
|
||||
@@ -18,8 +94,8 @@ function handleSubmit(value: LoginAndRegisterParams) {
|
||||
|
||||
<template>
|
||||
<AuthenticationRegister
|
||||
:form-schema="formSchema"
|
||||
:loading="loading"
|
||||
:login-path="LOGIN_PATH"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
|
@@ -1,7 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui';
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
@@ -13,7 +17,7 @@ onMounted(() => {
|
||||
containLabel: true,
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
top: '2 %',
|
||||
top: '2 %',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
@@ -51,12 +55,27 @@ 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: [
|
||||
@@ -65,7 +84,10 @@ onMounted(() => {
|
||||
show: false,
|
||||
},
|
||||
max: 80_000,
|
||||
|
||||
splitArea: {
|
||||
show: true,
|
||||
},
|
||||
splitNumber: 4,
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
|
@@ -1,7 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui';
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -1,7 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui';
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -1,7 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui';
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -1,7 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui';
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
@@ -13,7 +17,7 @@ onMounted(() => {
|
||||
containLabel: true,
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
top: '2 %',
|
||||
top: '2 %',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
|
@@ -214,7 +214,11 @@ const trendItems: WorkbenchTrendItem[] = [
|
||||
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" />
|
||||
</div>
|
||||
<div class="w-full lg:w-2/5">
|
||||
<WorkbenchQuickNav :items="quickNavItems" title="快捷导航" />
|
||||
<WorkbenchQuickNav
|
||||
:items="quickNavItems"
|
||||
class="mt-5 lg:mt-0"
|
||||
title="快捷导航"
|
||||
/>
|
||||
<WorkbenchTodo :items="todoItems" class="mt-5" title="待办事项" />
|
||||
<AnalysisChartCard class="mt-5" title="访问来源">
|
||||
<AnalyticsVisitsSource />
|
||||
|
66
apps/web-antd/src/views/demos/antd/index.vue
Normal file
66
apps/web-antd/src/views/demos/antd/index.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<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>
|
@@ -1,5 +1,5 @@
|
||||
# 端口号
|
||||
VITE_PORT=5666
|
||||
VITE_PORT=5777
|
||||
|
||||
VITE_BASE=/
|
||||
|
||||
|
@@ -14,3 +14,6 @@ VITE_ROUTER_HISTORY=hash
|
||||
|
||||
# 是否注入全局loading
|
||||
VITE_INJECT_APP_LOADING=true
|
||||
|
||||
# 打包后是否生成dist.zip
|
||||
VITE_ARCHIVER=true
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/web-ele",
|
||||
"version": "5.0.0",
|
||||
"version": "5.3.0-beta.1",
|
||||
"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": "^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"
|
||||
"@vueuse/core": "^11.0.3",
|
||||
"dayjs": "^1.11.13",
|
||||
"element-plus": "^2.8.2",
|
||||
"pinia": "2.2.2",
|
||||
"vue": "^3.5.4",
|
||||
"vue-router": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"unplugin-element-plus": "^0.8.0"
|
||||
|
89
apps/web-ele/src/adapter/form.ts
Normal file
89
apps/web-ele/src/adapter/form.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import type {
|
||||
BaseFormComponentType,
|
||||
VbenFormSchema as FormSchema,
|
||||
VbenFormProps,
|
||||
} from '@vben/common-ui';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElCheckbox,
|
||||
ElCheckboxGroup,
|
||||
ElDivider,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElRadioGroup,
|
||||
ElSelect,
|
||||
ElSpace,
|
||||
ElSwitch,
|
||||
ElTimePicker,
|
||||
ElTreeSelect,
|
||||
ElUpload,
|
||||
} from 'element-plus';
|
||||
// 业务表单组件适配
|
||||
|
||||
export type FormComponentType =
|
||||
| 'Checkbox'
|
||||
| 'CheckboxGroup'
|
||||
| 'DatePicker'
|
||||
| 'Divider'
|
||||
| 'Input'
|
||||
| 'InputNumber'
|
||||
| 'RadioGroup'
|
||||
| 'Select'
|
||||
| 'Space'
|
||||
| 'Switch'
|
||||
| 'TimePicker'
|
||||
| 'TreeSelect'
|
||||
| 'Upload'
|
||||
| BaseFormComponentType;
|
||||
|
||||
// 初始化表单组件,并注册到form组件内部
|
||||
setupVbenForm<FormComponentType>({
|
||||
components: {
|
||||
Checkbox: ElCheckbox,
|
||||
CheckboxGroup: ElCheckboxGroup,
|
||||
// 自定义默认的重置按钮
|
||||
DefaultResetActionButton: (props, { attrs, slots }) => {
|
||||
return h(ElButton, { ...props, attrs, type: 'info' }, slots);
|
||||
},
|
||||
// 自定义默认的提交按钮
|
||||
DefaultSubmitActionButton: (props, { attrs, slots }) => {
|
||||
return h(ElButton, { ...props, attrs, type: 'primary' }, slots);
|
||||
},
|
||||
Divider: ElDivider,
|
||||
Input: ElInput,
|
||||
InputNumber: ElInputNumber,
|
||||
RadioGroup: ElRadioGroup,
|
||||
Select: ElSelect,
|
||||
Space: ElSpace,
|
||||
Switch: ElSwitch,
|
||||
TimePicker: ElTimePicker,
|
||||
TreeSelect: ElTreeSelect,
|
||||
Upload: ElUpload,
|
||||
},
|
||||
config: {
|
||||
modelPropNameMap: {
|
||||
Upload: 'fileList',
|
||||
},
|
||||
},
|
||||
defineRules: {
|
||||
required: (value, _params, ctx) => {
|
||||
if ((!value && value !== 0) || value.length === 0) {
|
||||
return $t('formRules.required', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const useVbenForm = useForm<FormComponentType>;
|
||||
|
||||
export { useVbenForm, z };
|
||||
|
||||
export type VbenFormSchema = FormSchema<FormComponentType>;
|
||||
export type { VbenFormProps };
|
1
apps/web-ele/src/adapter/index.ts
Normal file
1
apps/web-ele/src/adapter/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './form';
|
@@ -1,4 +1,4 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
import { baseRequestClient, requestClient } from '#/api/request';
|
||||
|
||||
export namespace AuthApi {
|
||||
/** 登录接口参数 */
|
||||
@@ -12,10 +12,14 @@ export namespace AuthApi {
|
||||
accessToken: string;
|
||||
desc: string;
|
||||
realName: string;
|
||||
refreshToken: string;
|
||||
userId: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface RefreshTokenResult {
|
||||
data: string;
|
||||
status: number;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,6 +29,24 @@ 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,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户权限码
|
||||
*/
|
||||
|
@@ -1,67 +1,104 @@
|
||||
/**
|
||||
* 该文件可自行根据业务逻辑进行调整
|
||||
*/
|
||||
import type { HttpResponse } from '@vben/request';
|
||||
|
||||
import { useAppConfig } from '@vben/hooks';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { RequestClient } from '@vben/request';
|
||||
import {
|
||||
authenticateResponseInterceptor,
|
||||
errorMessageResponseInterceptor,
|
||||
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);
|
||||
|
||||
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;
|
||||
/**
|
||||
* 重新认证逻辑
|
||||
*/
|
||||
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();
|
||||
}
|
||||
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({
|
||||
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 });
|
||||
|
@@ -3,6 +3,7 @@ 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';
|
||||
|
||||
|
23
apps/web-ele/src/layouts/auth.vue
Normal file
23
apps/web-ele/src/layouts/auth.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<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>
|
@@ -2,10 +2,9 @@
|
||||
import type { NotificationItem } from '@vben/layouts';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
|
||||
import { LOGIN_PATH, VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
|
||||
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
|
||||
import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
|
||||
import {
|
||||
BasicLayout,
|
||||
@@ -14,17 +13,12 @@ import {
|
||||
UserDropdown,
|
||||
} from '@vben/layouts';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import {
|
||||
resetAllStores,
|
||||
storeToRefs,
|
||||
useAccessStore,
|
||||
useUserStore,
|
||||
} from '@vben/stores';
|
||||
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||
import { openWindow } from '@vben/utils';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
import { resetRoutes } from '#/router';
|
||||
import { useAuthStore } from '#/store';
|
||||
import LoginForm from '#/views/_core/authentication/login.vue';
|
||||
|
||||
const notifications = ref<NotificationItem[]>([
|
||||
{
|
||||
@@ -94,18 +88,12 @@ const menus = computed(() => [
|
||||
},
|
||||
]);
|
||||
|
||||
const { loginLoading } = storeToRefs(authStore);
|
||||
|
||||
const avatar = computed(() => {
|
||||
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
async function handleLogout() {
|
||||
resetAllStores();
|
||||
resetRoutes();
|
||||
await router.replace(LOGIN_PATH);
|
||||
await authStore.logout(false);
|
||||
}
|
||||
|
||||
function handleNoticeClear() {
|
||||
@@ -141,11 +129,9 @@ function handleMakeAll() {
|
||||
<AuthenticationLoginExpiredModal
|
||||
v-model:open="accessStore.loginExpired"
|
||||
:avatar
|
||||
:loading="loginLoading"
|
||||
password-placeholder="123456"
|
||||
username-placeholder="vben"
|
||||
@submit="authStore.authLogin"
|
||||
/>
|
||||
>
|
||||
<LoginForm />
|
||||
</AuthenticationLoginExpiredModal>
|
||||
</template>
|
||||
<template #lock-screen>
|
||||
<LockScreen :avatar @to-login="handleLogout" />
|
||||
|
@@ -1,8 +1,6 @@
|
||||
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 };
|
||||
|
@@ -58,7 +58,11 @@ async function loadDayjsLocale(lang: SupportedLanguagesType) {
|
||||
locale = await import('dayjs/locale/en');
|
||||
}
|
||||
}
|
||||
dayjs.locale(locale);
|
||||
if (locale) {
|
||||
dayjs.locale(locale);
|
||||
} else {
|
||||
console.error(`Failed to load dayjs locale for ${lang}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,4 +91,4 @@ async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
export { $t, elementLocale, loadMessages, setupI18n };
|
||||
export { $t, elementLocale, setupI18n };
|
||||
|
@@ -92,10 +92,8 @@ function setupAccessGuard(router: Router) {
|
||||
return to;
|
||||
}
|
||||
|
||||
const accessRoutes = accessStore.accessRoutes;
|
||||
|
||||
// 是否已经生成过动态路由
|
||||
if (accessRoutes && accessRoutes.length > 0) {
|
||||
if (accessStore.isAccessChecked) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -115,10 +113,11 @@ function setupAccessGuard(router: Router) {
|
||||
// 保存菜单信息和路由信息
|
||||
accessStore.setAccessMenus(accessibleMenus);
|
||||
accessStore.setAccessRoutes(accessibleRoutes);
|
||||
const redirectPath = (from.query.redirect ?? to.path) as string;
|
||||
accessStore.setIsAccessChecked(true);
|
||||
const redirectPath = (from.query.redirect ?? to.fullPath) as string;
|
||||
|
||||
return {
|
||||
path: decodeURIComponent(redirectPath),
|
||||
...router.resolve(decodeURIComponent(redirectPath)),
|
||||
replace: true,
|
||||
};
|
||||
});
|
||||
|
@@ -9,19 +9,19 @@ const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
|
||||
});
|
||||
|
||||
// 有需要可以自行打开注释,并创建文件夹
|
||||
// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
|
||||
// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
|
||||
|
||||
/** 动态路由 */
|
||||
const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
|
||||
|
||||
/** 静态路由列表,访问这些页面可以不需要权限 */
|
||||
// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
|
||||
const staticRoutes: RouteRecordRaw[] = [];
|
||||
/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统 */
|
||||
// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
|
||||
const externalRoutes: RouteRecordRaw[] = [];
|
||||
|
||||
/** 路由列表,由基本路由+静态路由组成 */
|
||||
const routes: RouteRecordRaw[] = [
|
||||
...coreRoutes,
|
||||
...staticRoutes,
|
||||
...externalRoutes,
|
||||
fallbackNotFoundRoute,
|
||||
];
|
||||
|
||||
|
@@ -17,7 +17,6 @@ const routes: RouteRecordRaw[] = [
|
||||
children: [
|
||||
{
|
||||
meta: {
|
||||
icon: 'mdi:shield-key-outline',
|
||||
title: $t('page.demos.element-plus'),
|
||||
},
|
||||
name: 'NaiveDemos',
|
||||
|
@@ -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/vben/about/index.vue'),
|
||||
component: () => import('#/views/_core/about/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:copyright',
|
||||
title: $t('page.vben.about'),
|
||||
@@ -38,8 +38,7 @@ const routes: RouteRecordRaw[] = [
|
||||
component: IFrameView,
|
||||
meta: {
|
||||
icon: 'lucide:book-open-text',
|
||||
iframeSrc: VBEN_DOC_URL,
|
||||
keepAlive: true,
|
||||
link: VBEN_DOC_URL,
|
||||
title: $t('page.vben.document'),
|
||||
},
|
||||
},
|
||||
@@ -69,7 +68,7 @@ const routes: RouteRecordRaw[] = [
|
||||
component: IFrameView,
|
||||
meta: {
|
||||
badgeType: 'dot',
|
||||
link: VBEN_PREVIEW_URL,
|
||||
link: VBEN_ANT_PREVIEW_URL,
|
||||
title: $t('page.vben.antdv'),
|
||||
},
|
||||
},
|
||||
|
@@ -10,7 +10,7 @@ import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
|
||||
import { ElNotification } from 'element-plus';
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import { getAccessCodesApi, getUserInfoApi, loginApi } from '#/api';
|
||||
import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
@@ -33,13 +33,12 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
let userInfo: null | UserInfo = null;
|
||||
try {
|
||||
loginLoading.value = true;
|
||||
const { accessToken, refreshToken } = await loginApi(params);
|
||||
const { accessToken } = await loginApi(params);
|
||||
|
||||
// 如果成功获取到 accessToken
|
||||
if (accessToken) {
|
||||
// 将 accessToken 存储到 accessStore 中
|
||||
accessStore.setAccessToken(accessToken);
|
||||
accessStore.setRefreshToken(refreshToken);
|
||||
|
||||
// 获取用户信息并存储到 accessStore 中
|
||||
const [fetchUserInfoResult, accessCodes] = await Promise.all([
|
||||
@@ -77,16 +76,19 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
};
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
async function logout(redirect: boolean = true) {
|
||||
await logoutApi();
|
||||
resetAllStores();
|
||||
accessStore.setLoginExpired(false);
|
||||
|
||||
// 回登陆页带上当前路由地址
|
||||
await router.replace({
|
||||
path: LOGIN_PATH,
|
||||
query: {
|
||||
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
|
||||
},
|
||||
query: redirect
|
||||
? {
|
||||
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
|
||||
}
|
||||
: {},
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -1,15 +1,49 @@
|
||||
<script lang="ts" setup>
|
||||
import type { LoginCodeParams } from '@vben/common-ui';
|
||||
import type { LoginCodeParams, VbenFormSchema } from '@vben/common-ui';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { AuthenticationCodeLogin } from '@vben/common-ui';
|
||||
import { LOGIN_PATH } from '@vben/constants';
|
||||
import { AuthenticationCodeLogin, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
defineOptions({ name: 'CodeLogin' });
|
||||
|
||||
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
|
||||
@@ -23,8 +57,8 @@ async function handleLogin(values: LoginCodeParams) {
|
||||
|
||||
<template>
|
||||
<AuthenticationCodeLogin
|
||||
:form-schema="formSchema"
|
||||
:loading="loading"
|
||||
:login-path="LOGIN_PATH"
|
||||
@submit="handleLogin"
|
||||
/>
|
||||
</template>
|
||||
|
@@ -1,13 +1,32 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
|
||||
import { AuthenticationForgetPassword } from '@vben/common-ui';
|
||||
import { LOGIN_PATH } from '@vben/constants';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { AuthenticationForgetPassword, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
defineOptions({ name: 'ForgetPassword' });
|
||||
|
||||
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) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('reset email:', value);
|
||||
@@ -16,8 +35,8 @@ function handleSubmit(value: string) {
|
||||
|
||||
<template>
|
||||
<AuthenticationForgetPassword
|
||||
:form-schema="formSchema"
|
||||
:loading="loading"
|
||||
:login-path="LOGIN_PATH"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
|
@@ -1,18 +1,91 @@
|
||||
<script lang="ts" setup>
|
||||
import { AuthenticationLogin } from '@vben/common-ui';
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
import type { BasicOption } from '@vben/types';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { AuthenticationLogin, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
defineOptions({ name: 'Login' });
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const MOCK_USER_OPTIONS: BasicOption[] = [
|
||||
{
|
||||
label: '超级管理员',
|
||||
value: 'vben',
|
||||
},
|
||||
{
|
||||
label: '管理员',
|
||||
value: 'admin',
|
||||
},
|
||||
{
|
||||
label: '用户',
|
||||
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') }),
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationLogin
|
||||
:form-schema="formSchema"
|
||||
:loading="authStore.loginLoading"
|
||||
password-placeholder="123456"
|
||||
username-placeholder="vben"
|
||||
@submit="authStore.authLogin"
|
||||
/>
|
||||
</template>
|
||||
|
@@ -1,15 +1,91 @@
|
||||
<script lang="ts" setup>
|
||||
import type { LoginAndRegisterParams } from '@vben/common-ui';
|
||||
import type { LoginAndRegisterParams, VbenFormSchema } from '@vben/common-ui';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { computed, h, ref } from 'vue';
|
||||
|
||||
import { AuthenticationRegister } from '@vben/common-ui';
|
||||
import { LOGIN_PATH } from '@vben/constants';
|
||||
import { AuthenticationRegister, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
defineOptions({ name: 'Register' });
|
||||
|
||||
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) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('register submit:', value);
|
||||
@@ -18,8 +94,8 @@ function handleSubmit(value: LoginAndRegisterParams) {
|
||||
|
||||
<template>
|
||||
<AuthenticationRegister
|
||||
:form-schema="formSchema"
|
||||
:loading="loading"
|
||||
:login-path="LOGIN_PATH"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
|
@@ -1,7 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui';
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
@@ -13,7 +17,7 @@ onMounted(() => {
|
||||
containLabel: true,
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
top: '2 %',
|
||||
top: '2 %',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
@@ -51,12 +55,27 @@ 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: [
|
||||
@@ -65,7 +84,10 @@ onMounted(() => {
|
||||
show: false,
|
||||
},
|
||||
max: 80_000,
|
||||
|
||||
splitArea: {
|
||||
show: true,
|
||||
},
|
||||
splitNumber: 4,
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
|
@@ -1,7 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui';
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -1,7 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui';
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -1,7 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui';
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -1,7 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui';
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
@@ -13,7 +17,7 @@ onMounted(() => {
|
||||
containLabel: true,
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
top: '2 %',
|
||||
top: '2 %',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
|
@@ -214,7 +214,11 @@ const trendItems: WorkbenchTrendItem[] = [
|
||||
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" />
|
||||
</div>
|
||||
<div class="w-full lg:w-2/5">
|
||||
<WorkbenchQuickNav :items="quickNavItems" title="快捷导航" />
|
||||
<WorkbenchQuickNav
|
||||
:items="quickNavItems"
|
||||
class="mt-5 lg:mt-0"
|
||||
title="快捷导航"
|
||||
/>
|
||||
<WorkbenchTodo :items="todoItems" class="mt-5" title="待办事项" />
|
||||
<AnalysisChartCard class="mt-5" title="访问来源">
|
||||
<AnalyticsVisitsSource />
|
||||
|
@@ -1,4 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElCard,
|
||||
@@ -39,58 +41,38 @@ function notify(type: NotificationType) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-5">
|
||||
<div class="card-box p-5">
|
||||
<h1 class="text-xl font-semibold">Element Plus组件使用演示</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>
|
||||
</div>
|
||||
<div>
|
||||
<ElSpace>
|
||||
<ElButton>Default</ElButton>
|
||||
<ElButton type="primary"> Primary </ElButton>
|
||||
<ElButton type="info"> Info </ElButton>
|
||||
<ElButton type="success"> Success </ElButton>
|
||||
<ElButton type="warning"> Warning </ElButton>
|
||||
<ElButton type="danger"> Error </ElButton>
|
||||
</ElSpace>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-box mt-5 p-5">
|
||||
<div class="mb-3">
|
||||
<span class="text-lg font-semibold">卡片</span>
|
||||
</div>
|
||||
<div>
|
||||
<ElCard title="卡片"> 卡片内容 </ElCard>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-box mt-5 p-5">
|
||||
<div class="mb-3">
|
||||
<span class="text-lg font-semibold">信息 Message </span>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<Page
|
||||
description="支持多语言,主题功能集成切换等"
|
||||
title="Element Plus组件使用演示"
|
||||
>
|
||||
<ElCard class="mb-5">
|
||||
<template #header> 按钮 </template>
|
||||
<ElSpace>
|
||||
<ElButton>Default</ElButton>
|
||||
<ElButton type="primary"> Primary </ElButton>
|
||||
<ElButton type="info"> Info </ElButton>
|
||||
<ElButton type="success"> Success </ElButton>
|
||||
<ElButton type="warning"> Warning </ElButton>
|
||||
<ElButton type="danger"> Error </ElButton>
|
||||
</ElSpace>
|
||||
</ElCard>
|
||||
<ElCard class="mb-5">
|
||||
<template #header> Message </template>
|
||||
<ElSpace>
|
||||
<ElButton type="info" @click="info"> 信息 </ElButton>
|
||||
<ElButton type="danger" @click="error"> 错误 </ElButton>
|
||||
<ElButton type="warning" @click="warning"> 警告 </ElButton>
|
||||
<ElButton type="success" @click="success"> 成功 </ElButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-box mt-5 p-5">
|
||||
<div class="mb-3">
|
||||
<span class="text-lg font-semibold">通知 Notification </span>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
</ElSpace>
|
||||
</ElCard>
|
||||
<ElCard class="mb-5">
|
||||
<template #header> Notification </template>
|
||||
<ElSpace>
|
||||
<ElButton type="info" @click="notify('info')"> 信息 </ElButton>
|
||||
<ElButton type="danger" @click="notify('error')"> 错误 </ElButton>
|
||||
<ElButton type="warning" @click="notify('warning')"> 警告 </ElButton>
|
||||
<ElButton type="success" @click="notify('success')"> 成功 </ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElSpace>
|
||||
</ElCard>
|
||||
</Page>
|
||||
</template>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
# 端口号
|
||||
VITE_PORT=5777
|
||||
VITE_PORT=5888
|
||||
|
||||
VITE_BASE=/
|
||||
|
||||
|
@@ -14,3 +14,6 @@ VITE_ROUTER_HISTORY=hash
|
||||
|
||||
# 是否注入全局loading
|
||||
VITE_INJECT_APP_LOADING=true
|
||||
|
||||
# 打包后是否生成dist.zip
|
||||
VITE_ARCHIVER=true
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/web-naive",
|
||||
"version": "5.0.0",
|
||||
"version": "5.3.0-beta.1",
|
||||
"homepage": "https://vben.pro",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
@@ -27,23 +27,23 @@
|
||||
},
|
||||
"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": "^10.11.0",
|
||||
"@vueuse/core": "^11.0.3",
|
||||
"naive-ui": "^2.39.0",
|
||||
"pinia": "2.2.0",
|
||||
"vue": "^3.4.35",
|
||||
"vue-router": "^4.4.2"
|
||||
"pinia": "2.2.2",
|
||||
"vue": "^3.5.4",
|
||||
"vue-router": "^4.4.3"
|
||||
}
|
||||
}
|
||||
|
98
apps/web-naive/src/adapter/form.ts
Normal file
98
apps/web-naive/src/adapter/form.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import type {
|
||||
BaseFormComponentType,
|
||||
VbenFormSchema as FormSchema,
|
||||
VbenFormProps,
|
||||
} from '@vben/common-ui';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
NButton,
|
||||
NCheckbox,
|
||||
NCheckboxGroup,
|
||||
NDatePicker,
|
||||
NDivider,
|
||||
NInput,
|
||||
NInputNumber,
|
||||
NRadioGroup,
|
||||
NSelect,
|
||||
NSpace,
|
||||
NSwitch,
|
||||
NTimePicker,
|
||||
NTreeSelect,
|
||||
NUpload,
|
||||
} from 'naive-ui';
|
||||
// 业务表单组件适配
|
||||
|
||||
export type FormComponentType =
|
||||
| 'Checkbox'
|
||||
| 'CheckboxGroup'
|
||||
| 'DatePicker'
|
||||
| 'Divider'
|
||||
| 'Input'
|
||||
| 'InputNumber'
|
||||
| 'RadioGroup'
|
||||
| 'Select'
|
||||
| 'Space'
|
||||
| 'Switch'
|
||||
| 'TimePicker'
|
||||
| 'TreeSelect'
|
||||
| 'Upload'
|
||||
| BaseFormComponentType;
|
||||
|
||||
// 初始化表单组件,并注册到form组件内部
|
||||
setupVbenForm<FormComponentType>({
|
||||
components: {
|
||||
Checkbox: NCheckbox,
|
||||
CheckboxGroup: NCheckboxGroup,
|
||||
DatePicker: NDatePicker,
|
||||
// 自定义默认的重置按钮
|
||||
DefaultResetActionButton: (props, { attrs, slots }) => {
|
||||
return h(NButton, { ...props, attrs, text: false, type: 'info' }, slots);
|
||||
},
|
||||
// 自定义默认的提交按钮
|
||||
DefaultSubmitActionButton: (props, { attrs, slots }) => {
|
||||
return h(
|
||||
NButton,
|
||||
{ ...props, attrs, text: false, type: 'primary' },
|
||||
slots,
|
||||
);
|
||||
},
|
||||
Divider: NDivider,
|
||||
Input: NInput,
|
||||
InputNumber: NInputNumber,
|
||||
RadioGroup: NRadioGroup,
|
||||
Select: NSelect,
|
||||
Space: NSpace,
|
||||
Switch: NSwitch,
|
||||
TimePicker: NTimePicker,
|
||||
TreeSelect: NTreeSelect,
|
||||
Upload: NUpload,
|
||||
},
|
||||
config: {
|
||||
baseModelPropName: 'value',
|
||||
modelPropNameMap: {
|
||||
Checkbox: 'checked',
|
||||
Radio: 'checked',
|
||||
Upload: 'fileList',
|
||||
},
|
||||
},
|
||||
defineRules: {
|
||||
required: (value, _params, ctx) => {
|
||||
if ((!value && value !== 0) || value.length === 0) {
|
||||
return $t('formRules.required', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const useVbenForm = useForm<FormComponentType>;
|
||||
|
||||
export { useVbenForm, z };
|
||||
|
||||
export type VbenFormSchema = FormSchema<FormComponentType>;
|
||||
export type { VbenFormProps };
|
1
apps/web-naive/src/adapter/index.ts
Normal file
1
apps/web-naive/src/adapter/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './form';
|
@@ -1,4 +1,4 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
import { baseRequestClient, requestClient } from '#/api/request';
|
||||
|
||||
export namespace AuthApi {
|
||||
/** 登录接口参数 */
|
||||
@@ -12,10 +12,14 @@ export namespace AuthApi {
|
||||
accessToken: string;
|
||||
desc: string;
|
||||
realName: string;
|
||||
refreshToken: string;
|
||||
userId: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface RefreshTokenResult {
|
||||
data: string;
|
||||
status: number;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,6 +29,24 @@ 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,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户权限码
|
||||
*/
|
||||
|
@@ -1,66 +1,103 @@
|
||||
/**
|
||||
* 该文件可自行根据业务逻辑进行调整
|
||||
*/
|
||||
import type { HttpResponse } from '@vben/request';
|
||||
|
||||
import { useAppConfig } from '@vben/hooks';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { RequestClient } from '@vben/request';
|
||||
import {
|
||||
authenticateResponseInterceptor,
|
||||
errorMessageResponseInterceptor,
|
||||
RequestClient,
|
||||
} from '@vben/request';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
import { message } from '#/naive';
|
||||
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);
|
||||
|
||||
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;
|
||||
/**
|
||||
* 重新认证逻辑
|
||||
*/
|
||||
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();
|
||||
}
|
||||
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({
|
||||
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 });
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user