Merge remote-tracking branch 'vben/main'
# Conflicts: # README.md # apps/web-antd/src/adapter/component/index.ts # apps/web-antd/src/layouts/basic.vue # apps/web-antd/src/router/routes/modules/demos.ts # apps/web-antd/src/views/demos/antd/index.vue # package.json # pnpm-lock.yaml
This commit is contained in:
3
.changeset/.prettierrc.json
Normal file
3
.changeset/.prettierrc.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
||||||
7
.changeset/fancy-ears-walk.md
Normal file
7
.changeset/fancy-ears-walk.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
'@vben/styles': patch
|
||||||
|
'@vben-core/form-ui': patch
|
||||||
|
'@vben/web-naive': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(@core/form-ui): 新增 useVbenForm 数组编辑器 VbenFormFieldArray
|
||||||
16
.gitattributes
vendored
16
.gitattributes
vendored
@@ -4,8 +4,18 @@
|
|||||||
* text=auto eol=lf
|
* text=auto eol=lf
|
||||||
|
|
||||||
# Declare files that will always have CRLF line endings on checkout.
|
# Declare files that will always have CRLF line endings on checkout.
|
||||||
*.{cmd,[cC][mM][dD]} text eol=crlf
|
*.cmd text eol=crlf
|
||||||
*.{bat,[bB][aA][tT]} text eol=crlf
|
*.[cC][mM][dD] text eol=crlf
|
||||||
|
*.bat text eol=crlf
|
||||||
|
*.[bB][aA][tT] text eol=crlf
|
||||||
|
|
||||||
# Denote all files that are truly binary and should not be modified.
|
# Denote all files that are truly binary and should not be modified.
|
||||||
*.{ico,png,jpg,jpeg,gif,webp,svg,woff,woff2} binary
|
*.ico binary
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.webp binary
|
||||||
|
*.svg binary
|
||||||
|
*.woff binary
|
||||||
|
*.woff2 binary
|
||||||
4
.github/actions/setup-node/action.yml
vendored
4
.github/actions/setup-node/action.yml
vendored
@@ -6,10 +6,10 @@ runs:
|
|||||||
using: 'composite'
|
using: 'composite'
|
||||||
steps:
|
steps:
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v6
|
||||||
|
|
||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version-file: .node-version
|
node-version-file: .node-version
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|||||||
3
.github/config.yml
vendored
3
.github/config.yml
vendored
@@ -21,12 +21,10 @@ newPRWelcomeComment: |
|
|||||||
firstPRMergeComment: >
|
firstPRMergeComment: >
|
||||||
Thanks for your contribution! 🎉🎉🎉
|
Thanks for your contribution! 🎉🎉🎉
|
||||||
|
|
||||||
|
|
||||||
# Comment to be posted to on first time issues
|
# Comment to be posted to on first time issues
|
||||||
newIssueWelcomeComment: >
|
newIssueWelcomeComment: >
|
||||||
Thanks for opening your first issue! Be sure to follow the issue template and provide every bit of information to help the developers!
|
Thanks for opening your first issue! Be sure to follow the issue template and provide every bit of information to help the developers!
|
||||||
|
|
||||||
|
|
||||||
# *OPTIONAL* default titles to check against for lack of descriptiveness
|
# *OPTIONAL* default titles to check against for lack of descriptiveness
|
||||||
# MUST BE ALL LOWERCASE
|
# MUST BE ALL LOWERCASE
|
||||||
requestInfoDefaultTitles:
|
requestInfoDefaultTitles:
|
||||||
@@ -36,4 +34,3 @@ requestInfoDefaultTitles:
|
|||||||
# *Required* Comment to reply with
|
# *Required* Comment to reply with
|
||||||
requestInfoReplyComment: >
|
requestInfoReplyComment: >
|
||||||
Thanks for filing this issue/PR! It would be much appreciated if you could provide us with more information so we can effectively analyze the situation in context.
|
Thanks for filing this issue/PR! It would be much appreciated if you could provide us with more information so we can effectively analyze the situation in context.
|
||||||
|
|
||||||
|
|||||||
2
.github/contributing.md
vendored
2
.github/contributing.md
vendored
@@ -19,11 +19,9 @@ Project maintainers have the right and responsibility to remove, edit, or reject
|
|||||||
- Checkout a topic branch from the relevant branch, e.g. main, and merge back against that branch.
|
- Checkout a topic branch from the relevant branch, e.g. main, and merge back against that branch.
|
||||||
|
|
||||||
- If adding a new feature:
|
- If adding a new feature:
|
||||||
|
|
||||||
- Provide a convincing reason to add this feature. Ideally, you should open a suggestion issue first and have it approved before working on it.
|
- Provide a convincing reason to add this feature. Ideally, you should open a suggestion issue first and have it approved before working on it.
|
||||||
|
|
||||||
- If fixing bug:
|
- If fixing bug:
|
||||||
|
|
||||||
- Provide a detailed description of the bug in the PR. Live demo preferred.
|
- Provide a detailed description of the bug in the PR. Live demo preferred.
|
||||||
|
|
||||||
- It's OK to have multiple small commits as you work on the PR - GitHub can automatically squash them before merging.
|
- It's OK to have multiple small commits as you work on the PR - GitHub can automatically squash them before merging.
|
||||||
|
|||||||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
|||||||
- windows-latest
|
- windows-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/changeset-version.yml
vendored
2
.github/workflows/changeset-version.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -28,12 +28,12 @@ jobs:
|
|||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v6
|
||||||
with:
|
with:
|
||||||
run_install: false
|
run_install: false
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ jobs:
|
|||||||
- windows-latest
|
- windows-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@@ -57,11 +57,11 @@ jobs:
|
|||||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@v4
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
build-mode: ${{ matrix.build-mode }}
|
build-mode: ${{ matrix.build-mode }}
|
||||||
@@ -89,6 +89,6 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3
|
uses: github/codeql-action/analyze@v4
|
||||||
with:
|
with:
|
||||||
category: '/language:${{matrix.language}}'
|
category: '/language:${{matrix.language}}'
|
||||||
|
|||||||
20
.github/workflows/deploy.yml
vendored
20
.github/workflows/deploy.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
run: pnpm build:play
|
run: pnpm build:play
|
||||||
|
|
||||||
- name: Sync Playground files
|
- name: Sync Playground files
|
||||||
uses: SamKirkland/FTP-Deploy-Action@v4.3.5
|
uses: SamKirkland/FTP-Deploy-Action@v4.4.0
|
||||||
with:
|
with:
|
||||||
server: ${{ secrets.PRO_FTP_HOST }}
|
server: ${{ secrets.PRO_FTP_HOST }}
|
||||||
username: ${{ secrets.WEB_PLAYGROUND_FTP_ACCOUNT }}
|
username: ${{ secrets.WEB_PLAYGROUND_FTP_ACCOUNT }}
|
||||||
@@ -43,7 +43,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
run: pnpm build:docs
|
run: pnpm build:docs
|
||||||
|
|
||||||
- name: Sync Docs files
|
- name: Sync Docs files
|
||||||
uses: SamKirkland/FTP-Deploy-Action@v4.3.5
|
uses: SamKirkland/FTP-Deploy-Action@v4.4.0
|
||||||
with:
|
with:
|
||||||
server: ${{ secrets.PRO_FTP_HOST }}
|
server: ${{ secrets.PRO_FTP_HOST }}
|
||||||
username: ${{ secrets.WEBSITE_FTP_ACCOUNT }}
|
username: ${{ secrets.WEBSITE_FTP_ACCOUNT }}
|
||||||
@@ -67,7 +67,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ jobs:
|
|||||||
run: pnpm run build:antd
|
run: pnpm run build:antd
|
||||||
|
|
||||||
- name: Sync files
|
- name: Sync files
|
||||||
uses: SamKirkland/FTP-Deploy-Action@v4.3.5
|
uses: SamKirkland/FTP-Deploy-Action@v4.4.0
|
||||||
with:
|
with:
|
||||||
server: ${{ secrets.PRO_FTP_HOST }}
|
server: ${{ secrets.PRO_FTP_HOST }}
|
||||||
username: ${{ secrets.WEB_ANTD_FTP_ACCOUNT }}
|
username: ${{ secrets.WEB_ANTD_FTP_ACCOUNT }}
|
||||||
@@ -98,7 +98,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ jobs:
|
|||||||
run: pnpm run build:ele
|
run: pnpm run build:ele
|
||||||
|
|
||||||
- name: Sync files
|
- name: Sync files
|
||||||
uses: SamKirkland/FTP-Deploy-Action@v4.3.5
|
uses: SamKirkland/FTP-Deploy-Action@v4.4.0
|
||||||
with:
|
with:
|
||||||
server: ${{ secrets.PRO_FTP_HOST }}
|
server: ${{ secrets.PRO_FTP_HOST }}
|
||||||
username: ${{ secrets.WEB_ELE_FTP_ACCOUNT }}
|
username: ${{ secrets.WEB_ELE_FTP_ACCOUNT }}
|
||||||
@@ -129,7 +129,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -147,7 +147,7 @@ jobs:
|
|||||||
run: pnpm run build:naive
|
run: pnpm run build:naive
|
||||||
|
|
||||||
- name: Sync files
|
- name: Sync files
|
||||||
uses: SamKirkland/FTP-Deploy-Action@v4.3.5
|
uses: SamKirkland/FTP-Deploy-Action@v4.4.0
|
||||||
with:
|
with:
|
||||||
server: ${{ secrets.PRO_FTP_HOST }}
|
server: ${{ secrets.PRO_FTP_HOST }}
|
||||||
username: ${{ secrets.WEB_NAIVE_FTP_ACCOUNT }}
|
username: ${{ secrets.WEB_NAIVE_FTP_ACCOUNT }}
|
||||||
|
|||||||
2
.github/workflows/draft.yml
vendored
2
.github/workflows/draft.yml
vendored
@@ -20,6 +20,6 @@ jobs:
|
|||||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: release-drafter/release-drafter@v6
|
- uses: release-drafter/release-drafter@v7
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
2
.github/workflows/issue-close-require.yml
vendored
2
.github/workflows/issue-close-require.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# 关闭未活动的 Issues
|
# 关闭未活动的 Issues
|
||||||
- name: Close Inactive Issues
|
- name: Close Inactive Issues
|
||||||
uses: actions/stale@v9
|
uses: actions/stale@v11
|
||||||
with:
|
with:
|
||||||
days-before-stale: -1 # Issues and PR will never be flagged stale automatically.
|
days-before-stale: -1 # Issues and PR will never be flagged stale automatically.
|
||||||
stale-issue-label: needs-reproduction # Label that flags an issue as stale.
|
stale-issue-label: needs-reproduction # Label that flags an issue as stale.
|
||||||
|
|||||||
6
.github/workflows/issue-labeled.yml
vendored
6
.github/workflows/issue-labeled.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: remove enhancement pending
|
- name: remove enhancement pending
|
||||||
if: github.event.label.name == 'enhancement'
|
if: github.event.label.name == 'enhancement'
|
||||||
uses: actions-cool/issues-helper@v3
|
uses: actions-cool/issues-helper-backup@d65454423c6fbbd20026b9b499d403f79422ac69
|
||||||
with:
|
with:
|
||||||
actions: 'remove-labels'
|
actions: 'remove-labels'
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
|
|
||||||
- name: remove bug pending
|
- name: remove bug pending
|
||||||
if: github.event.label.name == 'bug'
|
if: github.event.label.name == 'bug'
|
||||||
uses: actions-cool/issues-helper@v3
|
uses: actions-cool/issues-helper-backup@d65454423c6fbbd20026b9b499d403f79422ac69
|
||||||
with:
|
with:
|
||||||
actions: 'remove-labels'
|
actions: 'remove-labels'
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -36,7 +36,7 @@ jobs:
|
|||||||
|
|
||||||
- name: needs reproduction
|
- name: needs reproduction
|
||||||
if: github.event.label.name == 'needs reproduction'
|
if: github.event.label.name == 'needs reproduction'
|
||||||
uses: actions-cool/issues-helper@v3
|
uses: actions-cool/issues-helper-backup@d65454423c6fbbd20026b9b499d403f79422ac69
|
||||||
with:
|
with:
|
||||||
actions: 'create-comment, remove-labels'
|
actions: 'create-comment, remove-labels'
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@v5
|
- uses: dessant/lock-threads@v6
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
issue-inactive-days: '14'
|
issue-inactive-days: '14'
|
||||||
|
|||||||
78
.github/workflows/release-tag.yml
vendored
78
.github/workflows/release-tag.yml
vendored
@@ -3,78 +3,48 @@ name: Create Release Tag
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*.*.*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
- 'v*.*.*'
|
||||||
|
workflow_dispatch:
|
||||||
env:
|
inputs:
|
||||||
HUSKY: '0'
|
tag:
|
||||||
|
description: 'Tag to create (e.g. v1.2.3)'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
pull-requests: write
|
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
release:
|
||||||
name: Create Release
|
name: Create Release
|
||||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [20]
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
# - name: Checkout code
|
- name: Extract version
|
||||||
# uses: actions/checkout@v4
|
|
||||||
# with:
|
|
||||||
# fetch-depth: 0
|
|
||||||
|
|
||||||
# - name: Install pnpm
|
|
||||||
# uses: pnpm/action-setup@v4
|
|
||||||
|
|
||||||
# - name: Use Node.js ${{ matrix.node-version }}
|
|
||||||
# uses: actions/setup-node@v4
|
|
||||||
# with:
|
|
||||||
# node-version: ${{ matrix.node-version }}
|
|
||||||
# cache: "pnpm"
|
|
||||||
|
|
||||||
# - name: Install dependencies
|
|
||||||
# run: pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
# - name: Test and Build
|
|
||||||
# run: |
|
|
||||||
# pnpm run test
|
|
||||||
# pnpm run build
|
|
||||||
|
|
||||||
- name: version
|
|
||||||
id: version
|
id: version
|
||||||
run: |
|
run: |
|
||||||
tag=${GITHUB_REF/refs\/tags\//}
|
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||||
version=${tag#v}
|
raw_tag="${{ inputs.tag }}"
|
||||||
major=${version%%.*}
|
else
|
||||||
echo "tag=${tag}" >> $GITHUB_OUTPUT
|
raw_tag="${GITHUB_REF_NAME}"
|
||||||
echo "version=${version}" >> $GITHUB_OUTPUT
|
fi
|
||||||
echo "major=${major}" >> $GITHUB_OUTPUT
|
# Normalize: ensure v prefix
|
||||||
|
tag="${raw_tag}"
|
||||||
|
[[ "${tag:0:1}" != "v" ]] && tag="v${tag}"
|
||||||
|
version="${tag#v}"
|
||||||
|
major="${version%%.*}"
|
||||||
|
echo "tag=${tag}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "version=${version}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "major=${major}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
- uses: release-drafter/release-drafter@v6
|
- uses: release-drafter/release-drafter@v7
|
||||||
with:
|
with:
|
||||||
version: ${{ steps.version.outputs.version }}
|
version: ${{ steps.version.outputs.version }}
|
||||||
publish: true
|
publish: true
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
# - name: force update major tag
|
|
||||||
# run: |
|
|
||||||
# git tag v${{ steps.version.outputs.major }} ${{ steps.version.outputs.tag }} -f
|
|
||||||
# git push origin refs/tags/v${{ steps.version.outputs.major }} -f
|
|
||||||
|
|
||||||
# - name: Create Release for Tag
|
|
||||||
# id: release_tag
|
|
||||||
# uses: ncipollo/release-action@v1
|
|
||||||
# with:
|
|
||||||
# token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
# generateReleaseNotes: "true"
|
|
||||||
# body: |
|
|
||||||
# > Please refer to [CHANGELOG.md](https://github.com/vbenjs/vue-vben-admin/blob/main/CHANGELOG.md) for details.
|
|
||||||
|
|||||||
2
.github/workflows/semantic-pull-request.yml
vendored
2
.github/workflows/semantic-pull-request.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Validate PR title
|
- name: Validate PR title
|
||||||
uses: amannn/action-semantic-pull-request@v5
|
uses: amannn/action-semantic-pull-request@v6
|
||||||
with:
|
with:
|
||||||
wip: true
|
wip: true
|
||||||
subjectPattern: ^(?![A-Z]).+$
|
subjectPattern: ^(?![A-Z]).+$
|
||||||
|
|||||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v9
|
- uses: actions/stale@v11
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days'
|
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days'
|
||||||
|
|||||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -22,7 +22,8 @@ yarn.lock
|
|||||||
package-lock.json
|
package-lock.json
|
||||||
.VSCodeCounter
|
.VSCodeCounter
|
||||||
**/backend-mock/data
|
**/backend-mock/data
|
||||||
|
.omx
|
||||||
|
.pnpm-store
|
||||||
# local env files
|
# local env files
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
@@ -49,3 +50,14 @@ vite.config.ts.*
|
|||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
.history
|
.history
|
||||||
|
.cursor
|
||||||
|
|
||||||
|
# AI
|
||||||
|
.agent
|
||||||
|
.agents
|
||||||
|
.claude
|
||||||
|
.codex
|
||||||
|
skills-lock.json
|
||||||
|
.atomcode
|
||||||
|
datalog
|
||||||
|
.playwright-mcp
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
22.1.0
|
24.16.0
|
||||||
|
|||||||
14
.npmrc
14
.npmrc
@@ -1,13 +1 @@
|
|||||||
registry = "https://registry.npmmirror.com"
|
registry=https://registry.npmmirror.com
|
||||||
public-hoist-pattern[]=lefthook
|
|
||||||
public-hoist-pattern[]=eslint
|
|
||||||
public-hoist-pattern[]=prettier
|
|
||||||
public-hoist-pattern[]=prettier-plugin-tailwindcss
|
|
||||||
public-hoist-pattern[]=stylelint
|
|
||||||
public-hoist-pattern[]=*postcss*
|
|
||||||
public-hoist-pattern[]=@commitlint/*
|
|
||||||
public-hoist-pattern[]=czg
|
|
||||||
|
|
||||||
strict-peer-dependencies=false
|
|
||||||
auto-install-peers=true
|
|
||||||
dedupe-peer-dependents=true
|
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
dist
|
|
||||||
dev-dist
|
|
||||||
.local
|
|
||||||
.output.js
|
|
||||||
node_modules
|
|
||||||
.nvmrc
|
|
||||||
coverage
|
|
||||||
CODEOWNERS
|
|
||||||
.nitro
|
|
||||||
.output
|
|
||||||
|
|
||||||
|
|
||||||
**/*.svg
|
|
||||||
**/*.sh
|
|
||||||
|
|
||||||
public
|
|
||||||
.npmrc
|
|
||||||
*-lock.yaml
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from '@vben/prettier-config';
|
|
||||||
@@ -2,3 +2,7 @@ dist
|
|||||||
public
|
public
|
||||||
__tests__
|
__tests__
|
||||||
coverage
|
coverage
|
||||||
|
.codex
|
||||||
|
.claude
|
||||||
|
.agent
|
||||||
|
.agents
|
||||||
|
|||||||
10
.vscode/extensions.json
vendored
10
.vscode/extensions.json
vendored
@@ -2,14 +2,18 @@
|
|||||||
"recommendations": [
|
"recommendations": [
|
||||||
// Vue 3 的语言支持
|
// Vue 3 的语言支持
|
||||||
"Vue.volar",
|
"Vue.volar",
|
||||||
// 将 ESLint JavaScript 集成到 VS Code 中。
|
// 将 eslint 集成到 VS Code 中。
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
|
// 将 oxlint 集成到 VS Code 中。
|
||||||
|
"oxc.oxc-vscode",
|
||||||
// Visual Studio Code 的官方 Stylelint 扩展
|
// Visual Studio Code 的官方 Stylelint 扩展
|
||||||
"stylelint.vscode-stylelint",
|
"stylelint.vscode-stylelint",
|
||||||
// 使用 Prettier 的代码格式化程序
|
// 使用 oxfmt 的代码格式化程序
|
||||||
"esbenp.prettier-vscode",
|
"oxc.oxc-vscode",
|
||||||
// 支持 dotenv 文件语法
|
// 支持 dotenv 文件语法
|
||||||
"mikestead.dotenv",
|
"mikestead.dotenv",
|
||||||
|
// YAML 语言支持,供 ESLint 校验 pnpm-workspace.yaml 等文件
|
||||||
|
"redhat.vscode-yaml",
|
||||||
// 源代码的拼写检查器
|
// 源代码的拼写检查器
|
||||||
"streetsidesoftware.code-spell-checker",
|
"streetsidesoftware.code-spell-checker",
|
||||||
// Tailwind CSS 的官方 VS Code 插件
|
// Tailwind CSS 的官方 VS Code 插件
|
||||||
|
|||||||
70
.vscode/settings.json
vendored
70
.vscode/settings.json
vendored
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"tailwindCSS.experimental.configFile": "internal/tailwind-config/src/index.ts",
|
"tailwindCSS.experimental.configFile": "internal/tailwind-config/src/theme.css",
|
||||||
|
"tailwindCSS.lint.suggestCanonicalClasses": "ignore",
|
||||||
// workbench
|
// workbench
|
||||||
"workbench.list.smoothScrolling": true,
|
"workbench.list.smoothScrolling": true,
|
||||||
"workbench.startupEditor": "newUntitledFile",
|
"workbench.startupEditor": "newUntitledFile",
|
||||||
@@ -31,39 +32,51 @@
|
|||||||
"editor.autoClosingOvertype": "always",
|
"editor.autoClosingOvertype": "always",
|
||||||
"editor.autoClosingQuotes": "beforeWhitespace",
|
"editor.autoClosingQuotes": "beforeWhitespace",
|
||||||
"editor.wordSeparators": "`~!@#%^&*()=+[{]}\\|;:'\",.<>/?",
|
"editor.wordSeparators": "`~!@#%^&*()=+[{]}\\|;:'\",.<>/?",
|
||||||
|
"editor.quickSuggestions": {
|
||||||
|
"strings": "on"
|
||||||
|
},
|
||||||
|
|
||||||
|
// lint && format
|
||||||
|
"oxc.enable": true,
|
||||||
|
"oxc.typeAware": true,
|
||||||
|
"oxc.configPath": "oxlint.config.ts",
|
||||||
|
"oxc.fmt.configPath": "oxfmt.config.ts",
|
||||||
|
"eslint.useFlatConfig": true,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": "explicit",
|
"source.fixAll.eslint": "explicit",
|
||||||
|
"source.fixAll.oxc": "explicit",
|
||||||
"source.fixAll.stylelint": "explicit",
|
"source.fixAll.stylelint": "explicit",
|
||||||
"source.organizeImports": "never"
|
"source.organizeImports": "never"
|
||||||
},
|
},
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "oxc.oxc-vscode",
|
||||||
"[html]": {
|
"[html]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||||
},
|
},
|
||||||
"[css]": {
|
"[css]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||||
},
|
},
|
||||||
"[scss]": {
|
"[scss]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||||
},
|
},
|
||||||
"[javascript]": {
|
"[javascript]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||||
},
|
},
|
||||||
"[typescript]": {
|
"[typescript]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||||
},
|
},
|
||||||
"[json]": {
|
"[json]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||||
},
|
},
|
||||||
"[markdown]": {
|
"[markdown]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||||
},
|
},
|
||||||
"[jsonc]": {
|
"[jsonc]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||||
},
|
},
|
||||||
"[vue]": {
|
"[vue]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||||
},
|
},
|
||||||
|
|
||||||
// extensions
|
// extensions
|
||||||
"extensions.ignoreRecommendations": true,
|
"extensions.ignoreRecommendations": true,
|
||||||
|
|
||||||
@@ -79,6 +92,7 @@
|
|||||||
"files.insertFinalNewline": true,
|
"files.insertFinalNewline": true,
|
||||||
"files.simpleDialog.enable": true,
|
"files.simpleDialog.enable": true,
|
||||||
"files.associations": {
|
"files.associations": {
|
||||||
|
"*.css": "tailwindcss",
|
||||||
"*.ejs": "html",
|
"*.ejs": "html",
|
||||||
"*.art": "html",
|
"*.art": "html",
|
||||||
"**/tsconfig.json": "jsonc",
|
"**/tsconfig.json": "jsonc",
|
||||||
@@ -118,7 +132,7 @@
|
|||||||
// search
|
// search
|
||||||
"search.searchEditor.singleClickBehaviour": "peekDefinition",
|
"search.searchEditor.singleClickBehaviour": "peekDefinition",
|
||||||
"search.followSymlinks": false,
|
"search.followSymlinks": false,
|
||||||
// 在使用搜索功能时,将这些文件夹/文件排除在外
|
// 使用搜索功能时,将这些文件和文件夹排除在外
|
||||||
"search.exclude": {
|
"search.exclude": {
|
||||||
"**/node_modules": true,
|
"**/node_modules": true,
|
||||||
"**/*.log": true,
|
"**/*.log": true,
|
||||||
@@ -158,7 +172,7 @@
|
|||||||
"emmet.triggerExpansionOnTab": false,
|
"emmet.triggerExpansionOnTab": false,
|
||||||
|
|
||||||
"errorLens.enabledDiagnosticLevels": ["warning", "error"],
|
"errorLens.enabledDiagnosticLevels": ["warning", "error"],
|
||||||
"errorLens.excludeBySource": ["cSpell", "Grammarly", "eslint"],
|
"errorLens.excludeBySource": ["cSpell", "Grammarly"],
|
||||||
|
|
||||||
"stylelint.enable": true,
|
"stylelint.enable": true,
|
||||||
"stylelint.packageManager": "pnpm",
|
"stylelint.packageManager": "pnpm",
|
||||||
@@ -166,9 +180,10 @@
|
|||||||
"stylelint.customSyntax": "postcss-html",
|
"stylelint.customSyntax": "postcss-html",
|
||||||
"stylelint.snippet": ["css", "less", "postcss", "scss", "vue"],
|
"stylelint.snippet": ["css", "less", "postcss", "scss", "vue"],
|
||||||
|
|
||||||
"typescript.inlayHints.enumMemberValues.enabled": true,
|
"js/ts.tsdk.path": "node_modules/typescript/lib",
|
||||||
"typescript.preferences.preferTypeOnlyAutoImports": true,
|
"js/ts.inlayHints.enumMemberValues.enabled": true,
|
||||||
"typescript.preferences.includePackageJsonAutoImports": "on",
|
"js/ts.preferences.preferTypeOnlyAutoImports": true,
|
||||||
|
"js/ts.preferences.includePackageJsonAutoImports": "on",
|
||||||
|
|
||||||
"eslint.validate": [
|
"eslint.validate": [
|
||||||
"javascript",
|
"javascript",
|
||||||
@@ -180,7 +195,8 @@
|
|||||||
"markdown",
|
"markdown",
|
||||||
"json",
|
"json",
|
||||||
"jsonc",
|
"jsonc",
|
||||||
"json5"
|
"json5",
|
||||||
|
"yaml"
|
||||||
],
|
],
|
||||||
|
|
||||||
"tailwindCSS.experimental.classRegex": [
|
"tailwindCSS.experimental.classRegex": [
|
||||||
@@ -194,7 +210,7 @@
|
|||||||
"yaml": false
|
"yaml": false
|
||||||
},
|
},
|
||||||
|
|
||||||
"cssVariables.lookupFiles": ["packages/core/base/design/src/**/*.css"],
|
"cssVariables.lookupFiles": ["packages/@core/base/design/src/**/*.css"],
|
||||||
|
|
||||||
"i18n-ally.localesPaths": [
|
"i18n-ally.localesPaths": [
|
||||||
"packages/locales/src/langs",
|
"packages/locales/src/langs",
|
||||||
@@ -219,23 +235,9 @@
|
|||||||
"*.env": "$(capture).env.*",
|
"*.env": "$(capture).env.*",
|
||||||
"README.md": "README*,CHANGELOG*,LICENSE,CNAME",
|
"README.md": "README*,CHANGELOG*,LICENSE,CNAME",
|
||||||
"package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,.gitattributes,.gitignore,.gitpod.yml,.npmrc,.browserslistrc,.node-version,.git*,.tazerc.json",
|
"package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,.gitattributes,.gitignore,.gitpod.yml,.npmrc,.browserslistrc,.node-version,.git*,.tazerc.json",
|
||||||
"eslint.config.mjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json,lefthook.yml",
|
"oxlint.config.ts": ".eslintignore,.stylelintignore,.commitlintrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json,lefthook.yml,oxfmt.config.*,eslint.config.*"
|
||||||
"tailwind.config.mjs": "postcss.*"
|
|
||||||
},
|
},
|
||||||
"commentTranslate.hover.enabled": false,
|
"commentTranslate.hover.enabled": false,
|
||||||
"commentTranslate.multiLineMerge": true,
|
"commentTranslate.multiLineMerge": true,
|
||||||
"vue.server.hybridMode": true,
|
"vue.server.hybridMode": true
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
|
||||||
"oxc.enable": false,
|
|
||||||
"cSpell.words": [
|
|
||||||
"archiver",
|
|
||||||
"axios",
|
|
||||||
"dotenv",
|
|
||||||
"isequal",
|
|
||||||
"jspm",
|
|
||||||
"napi",
|
|
||||||
"nolebase",
|
|
||||||
"rollup",
|
|
||||||
"vitest"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ pnpm build
|
|||||||
|
|
||||||
## ブラウザサポート
|
## ブラウザサポート
|
||||||
|
|
||||||
ローカル開発には `Chrome 80+` ブラウザを推奨します
|
Tailwind CSS v4.0 is designed for Safari 16.4+, Chrome 111+, and Firefox 128+
|
||||||
|
|
||||||
モダンブラウザをサポートし、IEはサポートしません
|
モダンブラウザをサポートし、IEはサポートしません
|
||||||
|
|
||||||
@@ -140,8 +140,12 @@ pnpm build
|
|||||||
|
|
||||||
## 貢献者
|
## 貢献者
|
||||||
|
|
||||||
|
<a href="https://openomy.app/github/vbenjs/vue-vben-admin" target="_blank" style="display: block; width: 100%;" align="center">
|
||||||
|
<img src="https://openomy.app/svg?repo=vbenjs/vue-vben-admin&chart=bubble&latestMonth=3" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
|
||||||
|
</a>
|
||||||
|
|
||||||
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
|
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
|
||||||
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
|
<img alt="Contributors" src="https://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
## Discord
|
## Discord
|
||||||
|
|||||||
158
README.md
158
README.md
@@ -1 +1,157 @@
|
|||||||
# 奶酪云后台管理脚手架
|
<div align="center">
|
||||||
|
<a href="https://github.com/anncwb/vue-vben-admin">
|
||||||
|
<img alt="VbenAdmin Logo" width="215" src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp">
|
||||||
|
</a>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
[](LICENSE)
|
||||||
|
|
||||||
|
<h1>Vue Vben Admin</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
[](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin) [](https://github.com/vbenjs/vue-vben-admin/actions/workflows/codeql.yml) [](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml) [](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml) [](https://github.com/vbenjs/vue-vben-admin/actions/workflows/deploy.yml)
|
||||||
|
|
||||||
|
**English** | [中文](./README.zh-CN.md) | [日本語](./README.ja-JP.md)
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
Vue Vben Admin is a free and open source middle and back-end template. Using the latest `vue3`, `vite`, `TypeScript` and other mainstream technology development, the out-of-the-box middle and back-end front-end solutions can also be used for learning reference.
|
||||||
|
|
||||||
|
## Upgrade Notice
|
||||||
|
|
||||||
|
This is the latest version, 5.0, and it is not compatible with previous versions. If you are starting a new project, it is recommended to use the latest version. If you wish to view the old version, please use the [v2 branch](https://github.com/vbenjs/vue-vben-admin/tree/v2).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Latest Technology Stack**: Developed with cutting-edge front-end technologies like Vue 3 and Vite
|
||||||
|
- **TypeScript**: A language for application-scale JavaScript
|
||||||
|
- **Themes**: Multiple theme colors available with customizable options
|
||||||
|
- **Internationalization**: Comprehensive built-in internationalization support
|
||||||
|
- **Permissions**: Built-in solution for dynamic route-based permission generation
|
||||||
|
|
||||||
|
## Preview
|
||||||
|
|
||||||
|
- [Vben Admin](https://vben.pro/) - Full version Chinese site
|
||||||
|
|
||||||
|
Test Account: vben/123456
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview1.png">
|
||||||
|
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview2.png">
|
||||||
|
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview3.png">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### Use Gitpod
|
||||||
|
|
||||||
|
Open the project in Gitpod (free online dev environment for GitHub) and start coding immediately.
|
||||||
|
|
||||||
|
[](https://gitpod.io/#https://github.com/vbenjs/vue-vben-admin)
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
[Document](https://doc.vben.pro/)
|
||||||
|
|
||||||
|
## Install and Use
|
||||||
|
|
||||||
|
1. Get the project code
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/vbenjs/vue-vben-admin.git
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd vue-vben-admin
|
||||||
|
npm i -g corepack
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Change Log
|
||||||
|
|
||||||
|
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases)
|
||||||
|
|
||||||
|
## How to Contribute
|
||||||
|
|
||||||
|
You are very welcome to join! [Raise an issue](https://github.com/anncwb/vue-vben-admin/issues/new/choose) or submit a Pull Request.
|
||||||
|
|
||||||
|
**Pull Request Process:**
|
||||||
|
|
||||||
|
1. Fork the code
|
||||||
|
2. Create your branch: `git checkout -b feat/xxxx`
|
||||||
|
3. Submit your changes: `git commit -am 'feat(function): add xxxxx'`
|
||||||
|
4. Push your branch: `git push origin feat/xxxx`
|
||||||
|
5. Submit `pull request`
|
||||||
|
|
||||||
|
## Git Contribution Submission Specification
|
||||||
|
|
||||||
|
Reference [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) specification ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
|
||||||
|
|
||||||
|
- `feat` Add new features
|
||||||
|
- `fix` Fix the problem/BUG
|
||||||
|
- `style` The code style is related and does not affect the running result
|
||||||
|
- `perf` Optimization/performance improvement
|
||||||
|
- `refactor` Refactor
|
||||||
|
- `revert` Undo edit
|
||||||
|
- `test` Test related
|
||||||
|
- `docs` Documentation/notes
|
||||||
|
- `chore` Dependency update/scaffolding configuration modification etc.
|
||||||
|
- `ci` Continuous integration
|
||||||
|
- `types` Type definition file changes
|
||||||
|
|
||||||
|
## Browser Support
|
||||||
|
|
||||||
|
Tailwind CSS v4.0 is designed for Safari 16.4+, Chrome 111+, and Firefox 128+
|
||||||
|
|
||||||
|
Support modern browsers, not IE
|
||||||
|
|
||||||
|
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
|
||||||
|
| :-: | :-: | :-: | :-: |
|
||||||
|
| last 2 versions | last 2 versions | last 2 versions | last 2 versions |
|
||||||
|
|
||||||
|
## Maintainer
|
||||||
|
|
||||||
|
[@Vben](https://github.com/anncwb)
|
||||||
|
|
||||||
|
## Star History
|
||||||
|
|
||||||
|
[](https://star-history.com/#vbenjs/vue-vben-admin&Date)
|
||||||
|
|
||||||
|
## Donate
|
||||||
|
|
||||||
|
If you think this project is helpful to you, you can help the author buy a cup of coffee to show your support!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<a style="display: block;width: 100px;height: 50px;line-height: 50px; color: #fff;text-align: center; background: #408aee;border-radius: 4px;" href="https://www.paypal.com/paypalme/cvvben">Paypal Me</a>
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://openomy.app/github/vbenjs/vue-vben-admin" target="_blank" style="display: block; width: 100%;" align="center">
|
||||||
|
<img src="https://openomy.app/svg?repo=vbenjs/vue-vben-admin&chart=bubble&latestMonth=3" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
|
||||||
|
<img alt="Contributors" src="https://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
## Discord
|
||||||
|
|
||||||
|
- [Github Discussions](https://github.com/anncwb/vue-vben-admin/discussions)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT © Vben-2020](./LICENSE)
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ pnpm build
|
|||||||
|
|
||||||
## 浏览器支持
|
## 浏览器支持
|
||||||
|
|
||||||
本地开发推荐使用 `Chrome 80+` 浏览器
|
Tailwind CSS v4.0 is designed for Safari 16.4+, Chrome 111+, and Firefox 128+
|
||||||
|
|
||||||
支持现代浏览器,不支持 IE
|
支持现代浏览器,不支持 IE
|
||||||
|
|
||||||
@@ -140,8 +140,12 @@ pnpm build
|
|||||||
|
|
||||||
## 贡献者
|
## 贡献者
|
||||||
|
|
||||||
|
<a href="https://openomy.app/github/vbenjs/vue-vben-admin" target="_blank" style="display: block; width: 100%;" align="center">
|
||||||
|
<img src="https://openomy.app/svg?repo=vbenjs/vue-vben-admin&chart=bubble&latestMonth=3" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
|
||||||
|
</a>
|
||||||
|
|
||||||
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
|
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
|
||||||
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
|
<img alt="Contributors" src="https://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
## Discord
|
## Discord
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import { eventHandler } from 'h3';
|
||||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||||
import { unAuthorizedResponse } from '~/utils/response';
|
import { MOCK_CODES } from '~/utils/mock-data';
|
||||||
|
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||||
|
|
||||||
export default eventHandler((event) => {
|
export default eventHandler((event) => {
|
||||||
const userinfo = verifyAccessToken(event);
|
const userinfo = verifyAccessToken(event);
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
|
import { defineEventHandler, readBody, setResponseStatus } from 'h3';
|
||||||
import {
|
import {
|
||||||
clearRefreshTokenCookie,
|
clearRefreshTokenCookie,
|
||||||
setRefreshTokenCookie,
|
setRefreshTokenCookie,
|
||||||
} from '~/utils/cookie-utils';
|
} from '~/utils/cookie-utils';
|
||||||
import { generateAccessToken, generateRefreshToken } from '~/utils/jwt-utils';
|
import { generateAccessToken, generateRefreshToken } from '~/utils/jwt-utils';
|
||||||
import { forbiddenResponse } from '~/utils/response';
|
import { MOCK_USERS } from '~/utils/mock-data';
|
||||||
|
import {
|
||||||
|
forbiddenResponse,
|
||||||
|
useResponseError,
|
||||||
|
useResponseSuccess,
|
||||||
|
} from '~/utils/response';
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const { password, username } = await readBody(event);
|
const { password, username } = await readBody(event);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import { defineEventHandler } from 'h3';
|
||||||
import {
|
import {
|
||||||
clearRefreshTokenCookie,
|
clearRefreshTokenCookie,
|
||||||
getRefreshTokenFromCookie,
|
getRefreshTokenFromCookie,
|
||||||
} from '~/utils/cookie-utils';
|
} from '~/utils/cookie-utils';
|
||||||
|
import { useResponseSuccess } from '~/utils/response';
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const refreshToken = getRefreshTokenFromCookie(event);
|
const refreshToken = getRefreshTokenFromCookie(event);
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import { defineEventHandler } from 'h3';
|
||||||
import {
|
import {
|
||||||
clearRefreshTokenCookie,
|
clearRefreshTokenCookie,
|
||||||
getRefreshTokenFromCookie,
|
getRefreshTokenFromCookie,
|
||||||
setRefreshTokenCookie,
|
setRefreshTokenCookie,
|
||||||
} from '~/utils/cookie-utils';
|
} from '~/utils/cookie-utils';
|
||||||
import { verifyRefreshToken } from '~/utils/jwt-utils';
|
import { generateAccessToken, verifyRefreshToken } from '~/utils/jwt-utils';
|
||||||
|
import { MOCK_USERS } from '~/utils/mock-data';
|
||||||
import { forbiddenResponse } from '~/utils/response';
|
import { forbiddenResponse } from '~/utils/response';
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
|
|||||||
32
apps/backend-mock/api/demo/bigint.ts
Normal file
32
apps/backend-mock/api/demo/bigint.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { eventHandler, setHeader } from 'h3';
|
||||||
|
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||||
|
import { unAuthorizedResponse } from '~/utils/response';
|
||||||
|
|
||||||
|
export default eventHandler(async (event) => {
|
||||||
|
const userinfo = verifyAccessToken(event);
|
||||||
|
if (!userinfo) {
|
||||||
|
return unAuthorizedResponse(event);
|
||||||
|
}
|
||||||
|
const data = `
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 123456789012345678901234567890123456789012345678901234567890,
|
||||||
|
"name": "John Doe",
|
||||||
|
"age": 30,
|
||||||
|
"email": "john-doe@demo.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 987654321098765432109876543210987654321098765432109876543210,
|
||||||
|
"name": "Jane Smith",
|
||||||
|
"age": 25,
|
||||||
|
"email": "jane@demo.com"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
setHeader(event, 'Content-Type', 'application/json');
|
||||||
|
return data;
|
||||||
|
});
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import { eventHandler } from 'h3';
|
||||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||||
import { unAuthorizedResponse } from '~/utils/response';
|
import { MOCK_MENUS } from '~/utils/mock-data';
|
||||||
|
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||||
|
|
||||||
export default eventHandler(async (event) => {
|
export default eventHandler(async (event) => {
|
||||||
const userinfo = verifyAccessToken(event);
|
const userinfo = verifyAccessToken(event);
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { eventHandler, getQuery, setResponseStatus } from 'h3';
|
||||||
|
import { useResponseError } from '~/utils/response';
|
||||||
|
|
||||||
export default eventHandler((event) => {
|
export default eventHandler((event) => {
|
||||||
const { status } = getQuery(event);
|
const { status } = getQuery(event);
|
||||||
setResponseStatus(event, Number(status));
|
setResponseStatus(event, Number(status));
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { eventHandler } from 'h3';
|
||||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||||
import {
|
import {
|
||||||
sleep,
|
sleep,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { eventHandler } from 'h3';
|
||||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||||
import {
|
import {
|
||||||
sleep,
|
sleep,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { eventHandler } from 'h3';
|
||||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||||
import {
|
import {
|
||||||
sleep,
|
sleep,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
|
import { eventHandler } from 'h3';
|
||||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { eventHandler } from 'h3';
|
||||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||||
import { MOCK_MENU_LIST } from '~/utils/mock-data';
|
import { MOCK_MENU_LIST } from '~/utils/mock-data';
|
||||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { eventHandler, getQuery } from 'h3';
|
||||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||||
import { MOCK_MENU_LIST } from '~/utils/mock-data';
|
import { MOCK_MENU_LIST } from '~/utils/mock-data';
|
||||||
import { unAuthorizedResponse } from '~/utils/response';
|
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||||
|
|
||||||
const namesMap: Record<string, any> = {};
|
const namesMap: Record<string, any> = {};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { eventHandler, getQuery } from 'h3';
|
||||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||||
import { MOCK_MENU_LIST } from '~/utils/mock-data';
|
import { MOCK_MENU_LIST } from '~/utils/mock-data';
|
||||||
import { unAuthorizedResponse } from '~/utils/response';
|
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||||
|
|
||||||
const pathMap: Record<string, any> = { '/': 0 };
|
const pathMap: Record<string, any> = { '/': 0 };
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
|
import { eventHandler, getQuery } from 'h3';
|
||||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||||
import { getMenuIds, MOCK_MENU_LIST } from '~/utils/mock-data';
|
import { getMenuIds, MOCK_MENU_LIST } from '~/utils/mock-data';
|
||||||
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
|
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
|
||||||
|
|||||||
85
apps/backend-mock/api/system/user/list.ts
Normal file
85
apps/backend-mock/api/system/user/list.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { faker } from '@faker-js/faker';
|
||||||
|
import { eventHandler, getQuery } from 'h3';
|
||||||
|
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||||
|
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
|
||||||
|
|
||||||
|
const formatterCN = new Intl.DateTimeFormat('zh-CN', {
|
||||||
|
timeZone: 'Asia/Shanghai',
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
});
|
||||||
|
|
||||||
|
function generateMockDataList(count: number) {
|
||||||
|
const dataList = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const dataItem: Record<string, any> = {
|
||||||
|
id: faker.string.uuid(),
|
||||||
|
name: faker.commerce.product(),
|
||||||
|
status: faker.helpers.arrayElement([0, 1]),
|
||||||
|
createTime: formatterCN.format(
|
||||||
|
faker.date.between({ from: '2022-01-01', to: '2025-01-01' }),
|
||||||
|
),
|
||||||
|
deptId: faker.string.uuid(),
|
||||||
|
remark: faker.lorem.sentence(),
|
||||||
|
};
|
||||||
|
|
||||||
|
dataList.push(dataItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataList;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockData = generateMockDataList(100);
|
||||||
|
|
||||||
|
export default eventHandler(async (event) => {
|
||||||
|
const userinfo = verifyAccessToken(event);
|
||||||
|
if (!userinfo) {
|
||||||
|
return unAuthorizedResponse(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
page = 1,
|
||||||
|
pageSize = 20,
|
||||||
|
name,
|
||||||
|
id,
|
||||||
|
remark,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
deptId,
|
||||||
|
status,
|
||||||
|
} = getQuery(event);
|
||||||
|
let listData = structuredClone(mockData);
|
||||||
|
if (name) {
|
||||||
|
listData = listData.filter((item) =>
|
||||||
|
item.name.toLowerCase().includes(String(name).toLowerCase()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (id) {
|
||||||
|
listData = listData.filter((item) =>
|
||||||
|
item.id.toLowerCase().includes(String(id).toLowerCase()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (remark) {
|
||||||
|
listData = listData.filter((item) =>
|
||||||
|
item.remark?.toLowerCase()?.includes(String(remark).toLowerCase()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (startTime) {
|
||||||
|
listData = listData.filter((item) => item.createTime >= startTime);
|
||||||
|
}
|
||||||
|
if (endTime) {
|
||||||
|
listData = listData.filter((item) => item.createTime <= endTime);
|
||||||
|
}
|
||||||
|
if (['0', '1'].includes(status as string)) {
|
||||||
|
listData = listData.filter((item) => item.status === Number(status));
|
||||||
|
}
|
||||||
|
if (deptId) {
|
||||||
|
listData = listData.filter((item) => item.deptId === deptId);
|
||||||
|
}
|
||||||
|
return usePageResponseSuccess(page as string, pageSize as string, listData);
|
||||||
|
});
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
|
import { eventHandler, getQuery } from 'h3';
|
||||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||||
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
|
import {
|
||||||
|
sleep,
|
||||||
|
unAuthorizedResponse,
|
||||||
|
usePageResponseSuccess,
|
||||||
|
} from '~/utils/response';
|
||||||
|
|
||||||
function generateMockDataList(count: number) {
|
function generateMockDataList(count: number) {
|
||||||
const dataList = [];
|
const dataList = [];
|
||||||
@@ -44,30 +49,69 @@ export default eventHandler(async (event) => {
|
|||||||
await sleep(600);
|
await sleep(600);
|
||||||
|
|
||||||
const { page, pageSize, sortBy, sortOrder } = getQuery(event);
|
const { page, pageSize, sortBy, sortOrder } = getQuery(event);
|
||||||
|
// 规范化分页参数,处理 string[]
|
||||||
|
const pageRaw = Array.isArray(page) ? page[0] : page;
|
||||||
|
const pageSizeRaw = Array.isArray(pageSize) ? pageSize[0] : pageSize;
|
||||||
|
const pageNumber = Math.max(
|
||||||
|
1,
|
||||||
|
Number.parseInt(String(pageRaw ?? '1'), 10) || 1,
|
||||||
|
);
|
||||||
|
const pageSizeNumber = Math.min(
|
||||||
|
100,
|
||||||
|
Math.max(1, Number.parseInt(String(pageSizeRaw ?? '10'), 10) || 10),
|
||||||
|
);
|
||||||
const listData = structuredClone(mockData);
|
const listData = structuredClone(mockData);
|
||||||
if (sortBy && Reflect.has(listData[0], sortBy as string)) {
|
|
||||||
|
// 规范化 query 入参,兼容 string[]
|
||||||
|
const sortKeyRaw = Array.isArray(sortBy) ? sortBy[0] : sortBy;
|
||||||
|
const sortOrderRaw = Array.isArray(sortOrder) ? sortOrder[0] : sortOrder;
|
||||||
|
// 检查 sortBy 是否是 listData 元素的合法属性键
|
||||||
|
if (
|
||||||
|
typeof sortKeyRaw === 'string' &&
|
||||||
|
listData[0] &&
|
||||||
|
Object.prototype.hasOwnProperty.call(listData[0], sortKeyRaw)
|
||||||
|
) {
|
||||||
|
// 定义数组元素的类型
|
||||||
|
type ItemType = (typeof listData)[0];
|
||||||
|
const sortKey = sortKeyRaw as keyof ItemType; // 将 sortBy 断言为合法键
|
||||||
|
const isDesc = sortOrderRaw === 'desc';
|
||||||
listData.sort((a, b) => {
|
listData.sort((a, b) => {
|
||||||
if (sortOrder === 'asc') {
|
const aValue = a[sortKey] as unknown;
|
||||||
if (sortBy === 'price') {
|
const bValue = b[sortKey] as unknown;
|
||||||
return (
|
|
||||||
Number.parseFloat(a[sortBy as string]) -
|
let result: number;
|
||||||
Number.parseFloat(b[sortBy as string])
|
|
||||||
);
|
if (typeof aValue === 'number' && typeof bValue === 'number') {
|
||||||
|
result = aValue - bValue;
|
||||||
|
} else if (aValue instanceof Date && bValue instanceof Date) {
|
||||||
|
result = aValue.getTime() - bValue.getTime();
|
||||||
|
} else if (typeof aValue === 'boolean' && typeof bValue === 'boolean') {
|
||||||
|
if (aValue === bValue) {
|
||||||
|
result = 0;
|
||||||
} else {
|
} else {
|
||||||
return a[sortBy as string] > b[sortBy as string] ? 1 : -1;
|
result = aValue ? 1 : -1;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (sortBy === 'price') {
|
const aStr = String(aValue);
|
||||||
return (
|
const bStr = String(bValue);
|
||||||
Number.parseFloat(b[sortBy as string]) -
|
const aNum = Number(aStr);
|
||||||
Number.parseFloat(a[sortBy as string])
|
const bNum = Number(bStr);
|
||||||
);
|
result =
|
||||||
} else {
|
Number.isFinite(aNum) && Number.isFinite(bNum)
|
||||||
return a[sortBy as string] < b[sortBy as string] ? 1 : -1;
|
? aNum - bNum
|
||||||
}
|
: aStr.localeCompare(bStr, undefined, {
|
||||||
|
numeric: true,
|
||||||
|
sensitivity: 'base',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return isDesc ? -result : result;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return usePageResponseSuccess(page as string, pageSize as string, listData);
|
return usePageResponseSuccess(
|
||||||
|
String(pageNumber),
|
||||||
|
String(pageSizeNumber),
|
||||||
|
listData,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
|
import { defineEventHandler } from 'h3';
|
||||||
|
|
||||||
export default defineEventHandler(() => 'Test get handler');
|
export default defineEventHandler(() => 'Test get handler');
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
|
import { defineEventHandler } from 'h3';
|
||||||
|
|
||||||
export default defineEventHandler(() => 'Test post handler');
|
export default defineEventHandler(() => 'Test post handler');
|
||||||
|
|||||||
12
apps/backend-mock/api/timezone/getTimezone.ts
Normal file
12
apps/backend-mock/api/timezone/getTimezone.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { eventHandler } from 'h3';
|
||||||
|
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||||
|
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||||
|
import { getTimezone } from '~/utils/timezone-utils';
|
||||||
|
|
||||||
|
export default eventHandler((event) => {
|
||||||
|
const userinfo = verifyAccessToken(event);
|
||||||
|
if (!userinfo) {
|
||||||
|
return unAuthorizedResponse(event);
|
||||||
|
}
|
||||||
|
return useResponseSuccess(getTimezone());
|
||||||
|
});
|
||||||
11
apps/backend-mock/api/timezone/getTimezoneOptions.ts
Normal file
11
apps/backend-mock/api/timezone/getTimezoneOptions.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { eventHandler } from 'h3';
|
||||||
|
import { TIME_ZONE_OPTIONS } from '~/utils/mock-data';
|
||||||
|
import { useResponseSuccess } from '~/utils/response';
|
||||||
|
|
||||||
|
export default eventHandler(() => {
|
||||||
|
const data = TIME_ZONE_OPTIONS.map((o) => ({
|
||||||
|
label: `${o.timezone} (GMT${o.offset >= 0 ? `+${o.offset}` : o.offset})`,
|
||||||
|
value: o.timezone,
|
||||||
|
}));
|
||||||
|
return useResponseSuccess(data);
|
||||||
|
});
|
||||||
26
apps/backend-mock/api/timezone/setTimezone.ts
Normal file
26
apps/backend-mock/api/timezone/setTimezone.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { eventHandler, readBody, setResponseStatus } from 'h3';
|
||||||
|
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||||
|
import { TIME_ZONE_OPTIONS } from '~/utils/mock-data';
|
||||||
|
import {
|
||||||
|
unAuthorizedResponse,
|
||||||
|
useResponseError,
|
||||||
|
useResponseSuccess,
|
||||||
|
} from '~/utils/response';
|
||||||
|
import { setTimezone } from '~/utils/timezone-utils';
|
||||||
|
|
||||||
|
export default eventHandler(async (event) => {
|
||||||
|
const userinfo = verifyAccessToken(event);
|
||||||
|
if (!userinfo) {
|
||||||
|
return unAuthorizedResponse(event);
|
||||||
|
}
|
||||||
|
const body = await readBody<{ timezone?: unknown }>(event);
|
||||||
|
const timezone =
|
||||||
|
typeof body?.timezone === 'string' ? body.timezone : undefined;
|
||||||
|
const allowed = TIME_ZONE_OPTIONS.some((o) => o.timezone === timezone);
|
||||||
|
if (!timezone || !allowed) {
|
||||||
|
setResponseStatus(event, 400);
|
||||||
|
return useResponseError('Bad Request', 'Invalid timezone');
|
||||||
|
}
|
||||||
|
setTimezone(timezone);
|
||||||
|
return useResponseSuccess({});
|
||||||
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { eventHandler } from 'h3';
|
||||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||||
import { unAuthorizedResponse } from '~/utils/response';
|
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||||
|
|
||||||
export default eventHandler((event) => {
|
export default eventHandler((event) => {
|
||||||
const userinfo = verifyAccessToken(event);
|
const userinfo = verifyAccessToken(event);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { eventHandler } from 'h3';
|
||||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||||
import { unAuthorizedResponse } from '~/utils/response';
|
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||||
|
|
||||||
export default eventHandler((event) => {
|
export default eventHandler((event) => {
|
||||||
const userinfo = verifyAccessToken(event);
|
const userinfo = verifyAccessToken(event);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { defineEventHandler } from 'h3';
|
||||||
import { forbiddenResponse, sleep } from '~/utils/response';
|
import { forbiddenResponse, sleep } from '~/utils/response';
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { defineNitroConfig } from 'nitropack/config';
|
||||||
|
|
||||||
import errorHandler from './error';
|
import errorHandler from './error';
|
||||||
|
|
||||||
process.env.COMPATIBILITY_DATE = new Date().toISOString();
|
process.env.COMPATIBILITY_DATE = new Date().toISOString();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben/backend-mock",
|
"name": "@vben/backend-mock",
|
||||||
"version": "0.0.1",
|
"version": "5.7.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
|
import { defineEventHandler } from 'h3';
|
||||||
|
|
||||||
export default defineEventHandler(() => {
|
export default defineEventHandler(() => {
|
||||||
return `
|
return `
|
||||||
<h1>Hello Vben Admin</h1>
|
<h1>Hello Vben Admin</h1>
|
||||||
<h2>Mock service is starting</h2>
|
<h2>Mock service is running</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/api/user">/api/user/info</a></li>
|
|
||||||
<li><a href="/api/menu">/api/menu/all</a></li>
|
|
||||||
<li><a href="/api/auth/codes">/api/auth/codes</a></li>
|
|
||||||
<li><a href="/api/auth/login">/api/auth/login</a></li>
|
<li><a href="/api/auth/login">/api/auth/login</a></li>
|
||||||
<li><a href="/api/upload">/api/upload</a></li>
|
<li><a href="/api/auth/codes">/api/auth/codes</a></li>
|
||||||
|
<li><a href="/api/user/info">/api/user/info</a></li>
|
||||||
|
<li><a href="/api/menu/all">/api/menu/all</a></li>
|
||||||
|
<li><a href="/api/auth/logout">/api/auth/logout</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
{
|
{
|
||||||
"extends": "./.nitro/types/tsconfig.json"
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"extends": "@vben/tsconfig/node.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts", "**/.*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist", ".nitro"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { EventHandlerRequest, H3Event } from 'h3';
|
import type { EventHandlerRequest, H3Event } from 'h3';
|
||||||
|
|
||||||
|
import { deleteCookie, getCookie, setCookie } from 'h3';
|
||||||
|
|
||||||
export function clearRefreshTokenCookie(event: H3Event<EventHandlerRequest>) {
|
export function clearRefreshTokenCookie(event: H3Event<EventHandlerRequest>) {
|
||||||
deleteCookie(event, 'jwt', {
|
deleteCookie(event, 'jwt', {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import type { EventHandlerRequest, H3Event } from 'h3';
|
import type { EventHandlerRequest, H3Event } from 'h3';
|
||||||
|
|
||||||
|
import type { UserInfo } from './mock-data';
|
||||||
|
|
||||||
|
import { getHeader } from 'h3';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
|
|
||||||
import { UserInfo } from './mock-data';
|
import { MOCK_USERS } from './mock-data';
|
||||||
|
|
||||||
// TODO: Replace with your own secret key
|
// TODO: Replace with your own secret key
|
||||||
const ACCESS_TOKEN_SECRET = 'access_token_secret';
|
const ACCESS_TOKEN_SECRET = 'access_token_secret';
|
||||||
@@ -31,12 +34,22 @@ export function verifyAccessToken(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = authHeader.split(' ')[1];
|
const tokenParts = authHeader.split(' ');
|
||||||
|
if (tokenParts.length !== 2) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const token = tokenParts[1] as string;
|
||||||
try {
|
try {
|
||||||
const decoded = jwt.verify(token, ACCESS_TOKEN_SECRET) as UserPayload;
|
const decoded = jwt.verify(
|
||||||
|
token,
|
||||||
|
ACCESS_TOKEN_SECRET,
|
||||||
|
) as unknown as UserPayload;
|
||||||
|
|
||||||
const username = decoded.username;
|
const username = decoded.username;
|
||||||
const user = MOCK_USERS.find((item) => item.username === username);
|
const user = MOCK_USERS.find((item) => item.username === username);
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const { password: _pwd, ...userinfo } = user;
|
const { password: _pwd, ...userinfo } = user;
|
||||||
return userinfo;
|
return userinfo;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -50,7 +63,12 @@ export function verifyRefreshToken(
|
|||||||
try {
|
try {
|
||||||
const decoded = jwt.verify(token, REFRESH_TOKEN_SECRET) as UserPayload;
|
const decoded = jwt.verify(token, REFRESH_TOKEN_SECRET) as UserPayload;
|
||||||
const username = decoded.username;
|
const username = decoded.username;
|
||||||
const user = MOCK_USERS.find((item) => item.username === username);
|
const user = MOCK_USERS.find(
|
||||||
|
(item) => item.username === username,
|
||||||
|
) as UserInfo;
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const { password: _pwd, ...userinfo } = user;
|
const { password: _pwd, ...userinfo } = user;
|
||||||
return userinfo;
|
return userinfo;
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ export interface UserInfo {
|
|||||||
homePath?: string;
|
homePath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TimezoneOption {
|
||||||
|
offset: number;
|
||||||
|
timezone: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const MOCK_USERS: UserInfo[] = [
|
export const MOCK_USERS: UserInfo[] = [
|
||||||
{
|
{
|
||||||
id: 0,
|
id: 0,
|
||||||
@@ -14,6 +19,7 @@ export const MOCK_USERS: UserInfo[] = [
|
|||||||
realName: 'Vben',
|
realName: 'Vben',
|
||||||
roles: ['super'],
|
roles: ['super'],
|
||||||
username: 'vben',
|
username: 'vben',
|
||||||
|
homePath: '/dashboard/workspace',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -21,7 +27,7 @@ export const MOCK_USERS: UserInfo[] = [
|
|||||||
realName: 'Admin',
|
realName: 'Admin',
|
||||||
roles: ['admin'],
|
roles: ['admin'],
|
||||||
username: 'admin',
|
username: 'admin',
|
||||||
homePath: '/workspace',
|
homePath: '/dashboard/workspace',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
@@ -29,7 +35,7 @@ export const MOCK_USERS: UserInfo[] = [
|
|||||||
realName: 'Jack',
|
realName: 'Jack',
|
||||||
roles: ['user'],
|
roles: ['user'],
|
||||||
username: 'jack',
|
username: 'jack',
|
||||||
homePath: '/analytics',
|
homePath: '/dashboard/analytics',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -59,11 +65,11 @@ const dashboardMenus = [
|
|||||||
},
|
},
|
||||||
name: 'Dashboard',
|
name: 'Dashboard',
|
||||||
path: '/dashboard',
|
path: '/dashboard',
|
||||||
redirect: '/analytics',
|
redirect: '/dashboard/analytics',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
name: 'Analytics',
|
name: 'Analytics',
|
||||||
path: '/analytics',
|
path: 'analytics',
|
||||||
component: '/dashboard/analytics/index',
|
component: '/dashboard/analytics/index',
|
||||||
meta: {
|
meta: {
|
||||||
affixTab: true,
|
affixTab: true,
|
||||||
@@ -72,7 +78,7 @@ const dashboardMenus = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Workspace',
|
name: 'Workspace',
|
||||||
path: '/workspace',
|
path: 'workspace',
|
||||||
component: '/dashboard/workspace/index',
|
component: '/dashboard/workspace/index',
|
||||||
meta: {
|
meta: {
|
||||||
title: 'page.dashboard.workspace',
|
title: 'page.dashboard.workspace',
|
||||||
@@ -91,7 +97,7 @@ const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
|
|||||||
title: 'demos.access.adminVisible',
|
title: 'demos.access.adminVisible',
|
||||||
},
|
},
|
||||||
name: 'AccessAdminVisibleDemo',
|
name: 'AccessAdminVisibleDemo',
|
||||||
path: '/demos/access/admin-visible',
|
path: 'admin-visible',
|
||||||
},
|
},
|
||||||
super: {
|
super: {
|
||||||
component: '/demos/access/super-visible',
|
component: '/demos/access/super-visible',
|
||||||
@@ -100,7 +106,7 @@ const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
|
|||||||
title: 'demos.access.superVisible',
|
title: 'demos.access.superVisible',
|
||||||
},
|
},
|
||||||
name: 'AccessSuperVisibleDemo',
|
name: 'AccessSuperVisibleDemo',
|
||||||
path: '/demos/access/super-visible',
|
path: 'super-visible',
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
component: '/demos/access/user-visible',
|
component: '/demos/access/user-visible',
|
||||||
@@ -109,7 +115,7 @@ const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
|
|||||||
title: 'demos.access.userVisible',
|
title: 'demos.access.userVisible',
|
||||||
},
|
},
|
||||||
name: 'AccessUserVisibleDemo',
|
name: 'AccessUserVisibleDemo',
|
||||||
path: '/demos/access/user-visible',
|
path: 'user-visible',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -127,7 +133,7 @@ const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
name: 'AccessDemos',
|
name: 'AccessDemos',
|
||||||
path: '/demosaccess',
|
path: 'access',
|
||||||
meta: {
|
meta: {
|
||||||
icon: 'mdi:cloud-key-outline',
|
icon: 'mdi:cloud-key-outline',
|
||||||
title: 'demos.access.backendPermissions',
|
title: 'demos.access.backendPermissions',
|
||||||
@@ -136,7 +142,7 @@ const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
name: 'AccessPageControlDemo',
|
name: 'AccessPageControlDemo',
|
||||||
path: '/demos/access/page-control',
|
path: 'page-control',
|
||||||
component: '/demos/access/index',
|
component: '/demos/access/index',
|
||||||
meta: {
|
meta: {
|
||||||
icon: 'mdi:page-previous-outline',
|
icon: 'mdi:page-previous-outline',
|
||||||
@@ -145,7 +151,7 @@ const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'AccessButtonControlDemo',
|
name: 'AccessButtonControlDemo',
|
||||||
path: '/demos/access/button-control',
|
path: 'button-control',
|
||||||
component: '/demos/access/button-control',
|
component: '/demos/access/button-control',
|
||||||
meta: {
|
meta: {
|
||||||
icon: 'mdi:button-cursor',
|
icon: 'mdi:button-cursor',
|
||||||
@@ -154,7 +160,7 @@ const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'AccessMenuVisible403Demo',
|
name: 'AccessMenuVisible403Demo',
|
||||||
path: '/demos/access/menu-visible-403',
|
path: 'menu-visible-403',
|
||||||
component: '/demos/access/menu-visible-403',
|
component: '/demos/access/menu-visible-403',
|
||||||
meta: {
|
meta: {
|
||||||
authority: ['no-body'],
|
authority: ['no-body'],
|
||||||
@@ -189,18 +195,46 @@ export const MOCK_MENUS = [
|
|||||||
export const MOCK_MENU_LIST = [
|
export const MOCK_MENU_LIST = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Workspace',
|
name: 'Dashboard',
|
||||||
status: 1,
|
status: 1,
|
||||||
type: 'menu',
|
type: 'catalog',
|
||||||
icon: 'mdi:dashboard',
|
icon: 'lucide:layout-dashboard',
|
||||||
path: '/workspace',
|
path: '/dashboard',
|
||||||
component: '/dashboard/workspace/index',
|
|
||||||
meta: {
|
meta: {
|
||||||
icon: 'carbon:workspace',
|
icon: 'lucide:layout-dashboard',
|
||||||
title: 'page.dashboard.workspace',
|
order: -1,
|
||||||
affixTab: true,
|
title: 'page.dashboard.title',
|
||||||
order: 0,
|
|
||||||
},
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 101,
|
||||||
|
pid: 1,
|
||||||
|
status: 1,
|
||||||
|
type: 'menu',
|
||||||
|
name: 'Analytics',
|
||||||
|
path: 'analytics',
|
||||||
|
component: '/dashboard/analytics/index',
|
||||||
|
meta: {
|
||||||
|
affixTab: true,
|
||||||
|
icon: 'lucide:area-chart',
|
||||||
|
title: 'page.dashboard.analytics',
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 102,
|
||||||
|
pid: 1,
|
||||||
|
status: 1,
|
||||||
|
type: 'menu',
|
||||||
|
name: 'Workspace',
|
||||||
|
path: 'workspace',
|
||||||
|
component: '/views/dashboard/workspace/index',
|
||||||
|
meta: {
|
||||||
|
icon: 'carbon:workspace',
|
||||||
|
title: 'page.dashboard.workspace',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
@@ -276,7 +310,7 @@ export const MOCK_MENU_LIST = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: 20_401,
|
id: 20_401,
|
||||||
pid: 201,
|
pid: 202,
|
||||||
name: 'SystemDeptCreate',
|
name: 'SystemDeptCreate',
|
||||||
status: 1,
|
status: 1,
|
||||||
type: 'button',
|
type: 'button',
|
||||||
@@ -285,7 +319,7 @@ export const MOCK_MENU_LIST = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 20_402,
|
id: 20_402,
|
||||||
pid: 201,
|
pid: 202,
|
||||||
name: 'SystemDeptEdit',
|
name: 'SystemDeptEdit',
|
||||||
status: 1,
|
status: 1,
|
||||||
type: 'button',
|
type: 'button',
|
||||||
@@ -294,7 +328,7 @@ export const MOCK_MENU_LIST = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 20_403,
|
id: 20_403,
|
||||||
pid: 201,
|
pid: 202,
|
||||||
name: 'SystemDeptDelete',
|
name: 'SystemDeptDelete',
|
||||||
status: 1,
|
status: 1,
|
||||||
type: 'button',
|
type: 'button',
|
||||||
@@ -388,3 +422,29 @@ export function getMenuIds(menus: any[]) {
|
|||||||
});
|
});
|
||||||
return ids;
|
return ids;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 时区选项
|
||||||
|
*/
|
||||||
|
export const TIME_ZONE_OPTIONS: TimezoneOption[] = [
|
||||||
|
{
|
||||||
|
offset: -5,
|
||||||
|
timezone: 'America/New_York',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offset: 0,
|
||||||
|
timezone: 'Europe/London',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offset: 8,
|
||||||
|
timezone: 'Asia/Shanghai',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offset: 9,
|
||||||
|
timezone: 'Asia/Tokyo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offset: 9,
|
||||||
|
timezone: 'Asia/Seoul',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { EventHandlerRequest, H3Event } from 'h3';
|
import type { EventHandlerRequest, H3Event } from 'h3';
|
||||||
|
|
||||||
|
import { setResponseStatus } from 'h3';
|
||||||
|
|
||||||
export function useResponseSuccess<T = any>(data: T) {
|
export function useResponseSuccess<T = any>(data: T) {
|
||||||
return {
|
return {
|
||||||
code: 0,
|
code: 0,
|
||||||
|
|||||||
9
apps/backend-mock/utils/timezone-utils.ts
Normal file
9
apps/backend-mock/utils/timezone-utils.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
let mockTimeZone: null | string = null;
|
||||||
|
|
||||||
|
export const setTimezone = (timeZone: string) => {
|
||||||
|
mockTimeZone = timeZone;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTimezone = () => {
|
||||||
|
return mockTimeZone;
|
||||||
|
};
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
|
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
|
||||||
/>
|
/>
|
||||||
<!-- 由 vite 注入 VITE_APP_TITLE 变量,在 .env 文件内配置 -->
|
<!-- 由 vite 注入 VITE_APP_TITLE 变量,在 .env 文件内配置 -->
|
||||||
<title><%= VITE_APP_TITLE %></title>
|
<title>%VITE_APP_TITLE%</title>
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<script>
|
<script>
|
||||||
// 生产环境下注入百度统计
|
// 生产环境下注入百度统计
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben/web-antd",
|
"name": "@vben/web-antd",
|
||||||
"version": "5.5.6",
|
"version": "5.7.0",
|
||||||
"homepage": "https://vben.pro",
|
"homepage": "https://vben.pro",
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from '@vben/tailwind-config/postcss';
|
|
||||||
@@ -3,23 +3,80 @@
|
|||||||
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
|
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Component } from 'vue';
|
/* eslint-disable vue/one-component-per-file */
|
||||||
|
|
||||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
import type {
|
||||||
|
AutoCompleteProps,
|
||||||
|
ButtonProps,
|
||||||
|
CascaderProps,
|
||||||
|
CheckboxGroupProps,
|
||||||
|
CheckboxProps,
|
||||||
|
DatePickerProps,
|
||||||
|
DividerProps,
|
||||||
|
InputNumberProps,
|
||||||
|
InputProps,
|
||||||
|
MentionsProps,
|
||||||
|
RadioGroupProps,
|
||||||
|
RadioProps,
|
||||||
|
RateProps,
|
||||||
|
SelectProps,
|
||||||
|
SpaceProps,
|
||||||
|
SwitchProps,
|
||||||
|
TextAreaProps,
|
||||||
|
TimePickerProps,
|
||||||
|
TreeSelectProps,
|
||||||
|
UploadChangeParam,
|
||||||
|
UploadFile,
|
||||||
|
UploadProps,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
import type { RangePickerProps } from 'ant-design-vue/es/date-picker';
|
||||||
|
|
||||||
|
import type { Component, Ref } from 'vue';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ApiComponentSharedProps,
|
||||||
|
BaseFormComponentType,
|
||||||
|
IconPickerProps,
|
||||||
|
} from '@vben/common-ui';
|
||||||
|
import type { Sortable } from '@vben/hooks';
|
||||||
import type { Recordable } from '@vben/types';
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
computed,
|
||||||
defineAsyncComponent,
|
defineAsyncComponent,
|
||||||
defineComponent,
|
defineComponent,
|
||||||
getCurrentInstance,
|
|
||||||
h,
|
h,
|
||||||
|
nextTick,
|
||||||
|
onMounted,
|
||||||
|
onUnmounted,
|
||||||
ref,
|
ref,
|
||||||
|
render,
|
||||||
|
unref,
|
||||||
|
watch,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
|
|
||||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
import {
|
||||||
|
ApiComponent,
|
||||||
|
globalShareState,
|
||||||
|
IconPicker,
|
||||||
|
VCropper,
|
||||||
|
} from '@vben/common-ui';
|
||||||
|
import { useSortable } from '@vben/hooks';
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
import { notification } from 'ant-design-vue';
|
import { message, Modal, notification } from 'ant-design-vue';
|
||||||
|
|
||||||
|
type AdapterUploadProps = UploadProps & {
|
||||||
|
aspectRatio?: string;
|
||||||
|
crop?: boolean;
|
||||||
|
draggable?: boolean;
|
||||||
|
handleChange?: (event: UploadChangeParam) => void;
|
||||||
|
maxSize?: number;
|
||||||
|
onDragSort?: (oldIndex: number, newIndex: number) => void;
|
||||||
|
onHandleChange?: (event: UploadChangeParam) => void;
|
||||||
|
};
|
||||||
|
|
||||||
import { registerComponent } from '#/components/form/component-map';
|
import { registerComponent } from '#/components/form/component-map';
|
||||||
|
|
||||||
@@ -67,10 +124,17 @@ const TimePicker = defineAsyncComponent(
|
|||||||
const TreeSelect = defineAsyncComponent(
|
const TreeSelect = defineAsyncComponent(
|
||||||
() => import('ant-design-vue/es/tree-select'),
|
() => import('ant-design-vue/es/tree-select'),
|
||||||
);
|
);
|
||||||
|
const Cascader = defineAsyncComponent(
|
||||||
|
() => import('ant-design-vue/es/cascader'),
|
||||||
|
);
|
||||||
const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));
|
const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));
|
||||||
|
const Image = defineAsyncComponent(() => import('ant-design-vue/es/image'));
|
||||||
|
const PreviewGroup = defineAsyncComponent(() =>
|
||||||
|
import('ant-design-vue/es/image').then((res) => res.ImagePreviewGroup),
|
||||||
|
);
|
||||||
|
|
||||||
const withDefaultPlaceholder = <T extends Component>(
|
const withDefaultPlaceholder = (
|
||||||
component: T,
|
component: Component,
|
||||||
type: 'input' | 'select',
|
type: 'input' | 'select',
|
||||||
componentProps: Recordable<any> = {},
|
componentProps: Recordable<any> = {},
|
||||||
) => {
|
) => {
|
||||||
@@ -84,16 +148,15 @@ const withDefaultPlaceholder = <T extends Component>(
|
|||||||
$t(`ui.placeholder.${type}`);
|
$t(`ui.placeholder.${type}`);
|
||||||
// 透传组件暴露的方法
|
// 透传组件暴露的方法
|
||||||
const innerRef = ref();
|
const innerRef = ref();
|
||||||
const publicApi: Recordable<any> = {};
|
expose(
|
||||||
expose(publicApi);
|
new Proxy(
|
||||||
const instance = getCurrentInstance();
|
{},
|
||||||
instance?.proxy?.$nextTick(() => {
|
{
|
||||||
for (const key in innerRef.value) {
|
get: (_target, key) => innerRef.value?.[key],
|
||||||
if (typeof innerRef.value[key] === 'function') {
|
has: (_target, key) => key in (innerRef.value || {}),
|
||||||
publicApi[key] = innerRef.value[key];
|
},
|
||||||
}
|
),
|
||||||
}
|
);
|
||||||
});
|
|
||||||
return () =>
|
return () =>
|
||||||
h(
|
h(
|
||||||
component,
|
component,
|
||||||
@@ -104,12 +167,445 @@ const withDefaultPlaceholder = <T extends Component>(
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const IMAGE_EXTENSIONS = new Set([
|
||||||
|
'bmp',
|
||||||
|
'gif',
|
||||||
|
'jpeg',
|
||||||
|
'jpg',
|
||||||
|
'png',
|
||||||
|
'svg',
|
||||||
|
'webp',
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否为图片文件
|
||||||
|
*/
|
||||||
|
function isImageFile(file: UploadFile): boolean {
|
||||||
|
if (file.url) {
|
||||||
|
try {
|
||||||
|
const pathname = new URL(file.url, 'http://localhost').pathname;
|
||||||
|
const ext = pathname.split('.').pop()?.toLowerCase();
|
||||||
|
return ext ? IMAGE_EXTENSIONS.has(ext) : false;
|
||||||
|
} catch {
|
||||||
|
const ext = file.url?.split('.').pop()?.toLowerCase();
|
||||||
|
return ext ? IMAGE_EXTENSIONS.has(ext) : false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!file.type) {
|
||||||
|
const ext = file.name?.split('.').pop()?.toLowerCase();
|
||||||
|
return ext ? IMAGE_EXTENSIONS.has(ext) : false;
|
||||||
|
}
|
||||||
|
return file.type.startsWith('image/');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建默认的上传按钮插槽
|
||||||
|
*/
|
||||||
|
function createDefaultUploadSlots(listType: string, placeholder: string) {
|
||||||
|
if (listType === 'picture-card') {
|
||||||
|
return { default: () => placeholder };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
default: () =>
|
||||||
|
h(
|
||||||
|
Button,
|
||||||
|
{
|
||||||
|
icon: h(IconifyIcon, {
|
||||||
|
icon: 'ant-design:upload-outlined',
|
||||||
|
class: 'mb-1 size-4',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
() => placeholder,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件的 Base64
|
||||||
|
*/
|
||||||
|
function getBase64(file: File): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
reader.addEventListener('load', () => resolve(reader.result as string));
|
||||||
|
reader.addEventListener('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预览图片
|
||||||
|
*/
|
||||||
|
async function previewImage(
|
||||||
|
file: UploadFile,
|
||||||
|
visible: Ref<boolean>,
|
||||||
|
fileList: Ref<UploadProps['fileList']>,
|
||||||
|
) {
|
||||||
|
// 非图片文件直接打开链接
|
||||||
|
if (!isImageFile(file)) {
|
||||||
|
const url = file.url || file.preview;
|
||||||
|
if (url) {
|
||||||
|
window.open(url, '_blank');
|
||||||
|
} else {
|
||||||
|
message.error($t('ui.formRules.previewWarning'));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [ImageComponent, PreviewGroupComponent] = await Promise.all([
|
||||||
|
Image,
|
||||||
|
PreviewGroup,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 过滤图片文件并生成预览
|
||||||
|
const imageFiles = (unref(fileList) || []).filter((f) => isImageFile(f));
|
||||||
|
|
||||||
|
for (const imgFile of imageFiles) {
|
||||||
|
if (!imgFile.url && !imgFile.preview && imgFile.originFileObj) {
|
||||||
|
imgFile.preview = await getBase64(imgFile.originFileObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
document.body.append(container);
|
||||||
|
let isUnmounted = false;
|
||||||
|
|
||||||
|
const currentIndex = imageFiles.findIndex((f) => f.uid === file.uid);
|
||||||
|
|
||||||
|
const PreviewWrapper = {
|
||||||
|
setup() {
|
||||||
|
return () => {
|
||||||
|
if (isUnmounted) return null;
|
||||||
|
return h(
|
||||||
|
PreviewGroupComponent,
|
||||||
|
{
|
||||||
|
class: 'hidden',
|
||||||
|
preview: {
|
||||||
|
visible: visible.value,
|
||||||
|
current: currentIndex,
|
||||||
|
onVisibleChange: (value: boolean) => {
|
||||||
|
visible.value = value;
|
||||||
|
if (!value) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!isUnmounted && container) {
|
||||||
|
isUnmounted = true;
|
||||||
|
render(null, container);
|
||||||
|
container.remove();
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
() =>
|
||||||
|
imageFiles.map((imgFile) =>
|
||||||
|
h(ImageComponent, {
|
||||||
|
key: imgFile.uid,
|
||||||
|
src: imgFile.url || imgFile.preview,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
render(h(PreviewWrapper), container);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片裁剪操作
|
||||||
|
*/
|
||||||
|
function cropImage(file: File, aspectRatio: string | undefined) {
|
||||||
|
return new Promise<Blob | string | undefined>((resolve, reject) => {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
document.body.append(container);
|
||||||
|
|
||||||
|
let isUnmounted = false;
|
||||||
|
let objectUrl: null | string = null;
|
||||||
|
|
||||||
|
const open = ref<boolean>(true);
|
||||||
|
const cropperRef = ref<InstanceType<typeof VCropper> | null>(null);
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
open.value = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!isUnmounted && container) {
|
||||||
|
if (objectUrl) {
|
||||||
|
URL.revokeObjectURL(objectUrl);
|
||||||
|
}
|
||||||
|
isUnmounted = true;
|
||||||
|
render(null, container);
|
||||||
|
container.remove();
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CropperWrapper = {
|
||||||
|
setup() {
|
||||||
|
return () => {
|
||||||
|
if (isUnmounted) return null;
|
||||||
|
if (!objectUrl) {
|
||||||
|
objectUrl = URL.createObjectURL(file);
|
||||||
|
}
|
||||||
|
return h(
|
||||||
|
Modal,
|
||||||
|
{
|
||||||
|
open: open.value,
|
||||||
|
title: h('div', {}, [
|
||||||
|
$t('ui.crop.title'),
|
||||||
|
h(
|
||||||
|
'span',
|
||||||
|
{
|
||||||
|
class: `${aspectRatio ? '' : 'hidden'} ml-2 text-sm text-gray-400 font-normal`,
|
||||||
|
},
|
||||||
|
$t('ui.crop.titleTip', [aspectRatio]),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
centered: true,
|
||||||
|
width: 548,
|
||||||
|
zIndex: 9999,
|
||||||
|
keyboard: false,
|
||||||
|
maskClosable: false,
|
||||||
|
closable: false,
|
||||||
|
cancelText: $t('common.cancel'),
|
||||||
|
okText: $t('ui.crop.confirm'),
|
||||||
|
destroyOnClose: true,
|
||||||
|
onOk: async () => {
|
||||||
|
const cropper = cropperRef.value;
|
||||||
|
if (!cropper) {
|
||||||
|
reject(new Error('Cropper not found'));
|
||||||
|
closeModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const dataUrl = await cropper.getCropImage();
|
||||||
|
if (dataUrl) {
|
||||||
|
resolve(dataUrl);
|
||||||
|
} else {
|
||||||
|
reject(new Error($t('ui.crop.errorTip')));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
reject(new Error($t('ui.crop.errorTip')));
|
||||||
|
} finally {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
resolve('');
|
||||||
|
closeModal();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
() =>
|
||||||
|
h(VCropper, {
|
||||||
|
ref: (ref: any) => (cropperRef.value = ref),
|
||||||
|
img: objectUrl as string,
|
||||||
|
aspectRatio,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
render(h(CropperWrapper), container);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 带预览功能的上传组件
|
||||||
|
*/
|
||||||
|
const withPreviewUpload = () => {
|
||||||
|
return defineComponent({
|
||||||
|
name: Upload.name,
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
setup(
|
||||||
|
props: any,
|
||||||
|
{ attrs, slots, emit }: { attrs: any; emit: any; slots: any },
|
||||||
|
) {
|
||||||
|
const previewVisible = ref<boolean>(false);
|
||||||
|
const placeholder = attrs?.placeholder || $t('ui.placeholder.upload');
|
||||||
|
const listType = attrs?.listType || attrs?.['list-type'] || 'text';
|
||||||
|
const fileList = ref<UploadProps['fileList']>(
|
||||||
|
attrs?.fileList || attrs?.['file-list'] || [],
|
||||||
|
);
|
||||||
|
|
||||||
|
const maxSize = computed(() => attrs?.maxSize ?? attrs?.['max-size']);
|
||||||
|
const aspectRatio = computed(
|
||||||
|
() => attrs?.aspectRatio ?? attrs?.['aspect-ratio'],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleBeforeUpload = async (
|
||||||
|
file: UploadFile,
|
||||||
|
originFileList: Array<File>,
|
||||||
|
) => {
|
||||||
|
// 文件大小限制
|
||||||
|
if (maxSize.value && (file.size || 0) / 1024 / 1024 > maxSize.value) {
|
||||||
|
message.error($t('ui.formRules.sizeLimit', [maxSize.value]));
|
||||||
|
file.status = 'removed';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片裁剪处理
|
||||||
|
if (
|
||||||
|
attrs.crop &&
|
||||||
|
!attrs.multiple &&
|
||||||
|
originFileList[0] &&
|
||||||
|
isImageFile(file)
|
||||||
|
) {
|
||||||
|
file.status = 'removed';
|
||||||
|
const blob = await cropImage(originFileList[0], aspectRatio.value);
|
||||||
|
if (!blob) {
|
||||||
|
throw new Error($t('ui.crop.errorTip'));
|
||||||
|
}
|
||||||
|
return blob;
|
||||||
|
}
|
||||||
|
|
||||||
|
return attrs.beforeUpload?.(file) ?? true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (event: UploadChangeParam) => {
|
||||||
|
try {
|
||||||
|
attrs.handleChange?.(event);
|
||||||
|
attrs.onHandleChange?.(event);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
fileList.value = event.fileList.filter(
|
||||||
|
(file) => file.status !== 'removed',
|
||||||
|
);
|
||||||
|
emit(
|
||||||
|
'update:modelValue',
|
||||||
|
event.fileList?.length ? fileList.value : undefined,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePreview = async (file: UploadFile) => {
|
||||||
|
previewVisible.value = true;
|
||||||
|
await previewImage(file, previewVisible, fileList);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderUploadButton = () => {
|
||||||
|
if (attrs.disabled) return null;
|
||||||
|
return isEmpty(slots)
|
||||||
|
? createDefaultUploadSlots(listType, placeholder)
|
||||||
|
: slots;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 拖拽排序
|
||||||
|
const draggable = computed(
|
||||||
|
() => (attrs.draggable ?? false) && !attrs.disabled,
|
||||||
|
);
|
||||||
|
const uploadId = `upload-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
||||||
|
const sortableInstance = ref<null | Sortable>(null);
|
||||||
|
|
||||||
|
const styleId = `upload-drag-style-${uploadId}`;
|
||||||
|
|
||||||
|
function injectDragStyle() {
|
||||||
|
if (!document.querySelector(`[id="${styleId}"]`)) {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = styleId;
|
||||||
|
style.textContent = `
|
||||||
|
[data-upload-id="${uploadId}"] .ant-upload-list-item { cursor: move; }
|
||||||
|
[data-upload-id="${uploadId}"] .ant-upload-list-item:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.15); }
|
||||||
|
`;
|
||||||
|
document.head.append(style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeDragStyle() {
|
||||||
|
document.querySelector(`[id="${styleId}"]`)?.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initSortable(retryCount = 0) {
|
||||||
|
if (!draggable.value) return;
|
||||||
|
|
||||||
|
injectDragStyle();
|
||||||
|
await nextTick();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
const container = document.querySelector(
|
||||||
|
`[data-upload-id="${uploadId}"] .ant-upload-list`,
|
||||||
|
) as HTMLElement;
|
||||||
|
|
||||||
|
if (!container) {
|
||||||
|
if (retryCount < 5) {
|
||||||
|
setTimeout(() => initSortable(retryCount + 1), 200);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { initializeSortable } = useSortable(container, {
|
||||||
|
animation: 300,
|
||||||
|
delay: 400,
|
||||||
|
delayOnTouchOnly: true,
|
||||||
|
filter:
|
||||||
|
'.ant-upload-select, .ant-upload-list-item-error, .ant-upload-list-item-uploading',
|
||||||
|
onEnd: (evt) => {
|
||||||
|
const { oldIndex, newIndex } = evt;
|
||||||
|
if (
|
||||||
|
oldIndex === undefined ||
|
||||||
|
newIndex === undefined ||
|
||||||
|
oldIndex === newIndex
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = [...(fileList.value || [])];
|
||||||
|
const [movedItem] = list.splice(oldIndex, 1);
|
||||||
|
if (movedItem) {
|
||||||
|
list.splice(newIndex, 0, movedItem);
|
||||||
|
fileList.value = list;
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs.onDragSort?.(oldIndex, newIndex);
|
||||||
|
emit('update:modelValue', fileList.value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
sortableInstance.value = await initializeSortable();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听表单值变化
|
||||||
|
watch(
|
||||||
|
() => attrs.modelValue,
|
||||||
|
(res) => {
|
||||||
|
fileList.value = res;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(initSortable);
|
||||||
|
onUnmounted(() => {
|
||||||
|
sortableInstance.value?.destroy();
|
||||||
|
removeDragStyle();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () =>
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ 'data-upload-id': uploadId, class: 'w-full' },
|
||||||
|
h(
|
||||||
|
Upload,
|
||||||
|
{
|
||||||
|
...props,
|
||||||
|
...attrs,
|
||||||
|
fileList: fileList.value,
|
||||||
|
beforeUpload: handleBeforeUpload,
|
||||||
|
onChange: handleChange,
|
||||||
|
onPreview: handlePreview,
|
||||||
|
},
|
||||||
|
renderUploadButton() as any,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
||||||
export type ComponentType =
|
export type ComponentType =
|
||||||
|
| 'ApiCascader'
|
||||||
| 'ApiSelect'
|
| 'ApiSelect'
|
||||||
| 'ApiTreeSelect'
|
| 'ApiTreeSelect'
|
||||||
| 'AutoComplete'
|
| 'AutoComplete'
|
||||||
| 'Avatar'
|
| 'Avatar'
|
||||||
|
| 'Cascader'
|
||||||
| 'Checkbox'
|
| 'Checkbox'
|
||||||
| 'CheckboxGroup'
|
| 'CheckboxGroup'
|
||||||
| 'DatePicker'
|
| 'DatePicker'
|
||||||
@@ -134,40 +630,68 @@ export type ComponentType =
|
|||||||
| 'Upload'
|
| 'Upload'
|
||||||
| BaseFormComponentType;
|
| BaseFormComponentType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 与 {@link ComponentType} 中注册的组件名一一对应,便于 Schema 上 `component` + `componentProps` 联动提示
|
||||||
|
*/
|
||||||
|
export interface ComponentPropsMap {
|
||||||
|
ApiCascader: ApiComponentSharedProps & CascaderProps;
|
||||||
|
ApiSelect: ApiComponentSharedProps & SelectProps;
|
||||||
|
ApiTreeSelect: ApiComponentSharedProps & TreeSelectProps;
|
||||||
|
AutoComplete: AutoCompleteProps;
|
||||||
|
Cascader: CascaderProps;
|
||||||
|
Checkbox: CheckboxProps;
|
||||||
|
CheckboxGroup: CheckboxGroupProps;
|
||||||
|
DatePicker: DatePickerProps;
|
||||||
|
DefaultButton: ButtonProps;
|
||||||
|
Divider: DividerProps;
|
||||||
|
IconPicker: IconPickerProps;
|
||||||
|
Input: InputProps;
|
||||||
|
InputNumber: InputNumberProps;
|
||||||
|
InputPassword: InputProps;
|
||||||
|
Mentions: MentionsProps;
|
||||||
|
PrimaryButton: ButtonProps;
|
||||||
|
Radio: RadioProps;
|
||||||
|
RadioGroup: RadioGroupProps;
|
||||||
|
RangePicker: RangePickerProps;
|
||||||
|
Rate: RateProps;
|
||||||
|
Select: SelectProps;
|
||||||
|
Space: SpaceProps;
|
||||||
|
Switch: SwitchProps;
|
||||||
|
Textarea: TextAreaProps;
|
||||||
|
TimePicker: TimePickerProps;
|
||||||
|
TreeSelect: TreeSelectProps;
|
||||||
|
Upload: AdapterUploadProps;
|
||||||
|
}
|
||||||
|
|
||||||
async function initComponentAdapter() {
|
async function initComponentAdapter() {
|
||||||
const components: Partial<Record<ComponentType, Component>> = {
|
const components: Partial<Record<ComponentType, Component>> = {
|
||||||
// 如果你的组件体积比较大,可以使用异步加载
|
// 如果你的组件体积比较大,可以使用异步加载
|
||||||
// Button: () =>
|
// Button: () =>
|
||||||
// import('xxx').then((res) => res.Button),
|
// import('xxx').then((res) => res.Button),
|
||||||
ApiSelect: withDefaultPlaceholder(
|
|
||||||
{
|
ApiCascader: withDefaultPlaceholder(ApiComponent, 'select', {
|
||||||
...ApiComponent,
|
component: Cascader,
|
||||||
name: 'ApiSelect',
|
fieldNames: { label: 'label', value: 'value', children: 'children' },
|
||||||
},
|
loadingSlot: 'suffixIcon',
|
||||||
'select',
|
modelPropName: 'value',
|
||||||
{
|
visibleEvent: 'onVisibleChange',
|
||||||
component: Select,
|
}),
|
||||||
loadingSlot: 'suffixIcon',
|
ApiSelect: withDefaultPlaceholder(ApiComponent, 'select', {
|
||||||
visibleEvent: 'onDropdownVisibleChange',
|
component: Select,
|
||||||
modelPropName: 'value',
|
loadingSlot: 'suffixIcon',
|
||||||
},
|
modelPropName: 'value',
|
||||||
),
|
visibleEvent: 'onVisibleChange',
|
||||||
ApiTreeSelect: withDefaultPlaceholder(
|
}),
|
||||||
{
|
ApiTreeSelect: withDefaultPlaceholder(ApiComponent, 'select', {
|
||||||
...ApiComponent,
|
component: TreeSelect,
|
||||||
name: 'ApiTreeSelect',
|
fieldNames: { label: 'label', value: 'value', children: 'children' },
|
||||||
},
|
loadingSlot: 'suffixIcon',
|
||||||
'select',
|
modelPropName: 'value',
|
||||||
{
|
optionsPropName: 'treeData',
|
||||||
component: TreeSelect,
|
visibleEvent: 'onVisibleChange',
|
||||||
fieldNames: { label: 'label', value: 'value', children: 'children' },
|
}),
|
||||||
loadingSlot: 'suffixIcon',
|
|
||||||
modelPropName: 'value',
|
|
||||||
optionsPropName: 'treeData',
|
|
||||||
visibleEvent: 'onVisibleChange',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
AutoComplete,
|
AutoComplete,
|
||||||
|
Cascader,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
CheckboxGroup,
|
CheckboxGroup,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
@@ -182,7 +706,9 @@ async function initComponentAdapter() {
|
|||||||
modelValueProp: 'value',
|
modelValueProp: 'value',
|
||||||
}),
|
}),
|
||||||
Input: withDefaultPlaceholder(Input, 'input'),
|
Input: withDefaultPlaceholder(Input, 'input'),
|
||||||
InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
|
InputNumber: withDefaultPlaceholder(InputNumber, 'input', {
|
||||||
|
style: { width: '100%' },
|
||||||
|
}),
|
||||||
InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
|
InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
|
||||||
Mentions: withDefaultPlaceholder(Mentions, 'input'),
|
Mentions: withDefaultPlaceholder(Mentions, 'input'),
|
||||||
// 自定义主要按钮
|
// 自定义主要按钮
|
||||||
@@ -199,7 +725,7 @@ async function initComponentAdapter() {
|
|||||||
Textarea: withDefaultPlaceholder(Textarea, 'input'),
|
Textarea: withDefaultPlaceholder(Textarea, 'input'),
|
||||||
TimePicker,
|
TimePicker,
|
||||||
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
|
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
|
||||||
Upload,
|
Upload: withPreviewUpload(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 自动注册自定义组件
|
// 自动注册自定义组件
|
||||||
|
|||||||
@@ -1,47 +1,68 @@
|
|||||||
import type {
|
import type {
|
||||||
|
VbenFormProps as FormProps,
|
||||||
VbenFormSchema as FormSchema,
|
VbenFormSchema as FormSchema,
|
||||||
VbenFormProps,
|
FormValues,
|
||||||
} from '@vben/common-ui';
|
} from '@vben/common-ui';
|
||||||
|
|
||||||
import type { ComponentType } from './component';
|
import type { ComponentPropsMap, ComponentType } from './component';
|
||||||
|
|
||||||
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
|
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
setupVbenForm<ComponentType>({
|
async function initSetupVbenForm() {
|
||||||
config: {
|
setupVbenForm<ComponentType>({
|
||||||
// ant design vue组件库默认都是 v-model:value
|
config: {
|
||||||
baseModelPropName: 'value',
|
// ant design vue组件库默认都是 v-model:value
|
||||||
|
baseModelPropName: 'value',
|
||||||
|
|
||||||
// 一些组件是 v-model:checked 或者 v-model:fileList
|
// 一些组件是 v-model:checked 或者 v-model:fileList
|
||||||
modelPropNameMap: {
|
modelPropNameMap: {
|
||||||
Checkbox: 'checked',
|
Checkbox: 'checked',
|
||||||
Radio: 'checked',
|
Radio: 'checked',
|
||||||
Switch: 'checked',
|
Switch: 'checked',
|
||||||
Upload: 'fileList',
|
Upload: 'fileList',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
rules: {
|
||||||
defineRules: {
|
// 输入项目必填国际化适配
|
||||||
// 输入项目必填国际化适配
|
required: (value, _params, ctx) => {
|
||||||
required: (value, _params, ctx) => {
|
if (value === undefined || value === null || value.length === 0) {
|
||||||
if (value === undefined || value === null || value.length === 0) {
|
return $t('ui.formRules.required', [ctx.label]);
|
||||||
return $t('ui.formRules.required', [ctx.label]);
|
}
|
||||||
}
|
return true;
|
||||||
return true;
|
},
|
||||||
|
// 选择项目必填国际化适配
|
||||||
|
selectRequired: (value, _params, ctx) => {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return $t('ui.formRules.selectRequired', [ctx.label]);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
// 选择项目必填国际化适配
|
});
|
||||||
selectRequired: (value, _params, ctx) => {
|
}
|
||||||
if (value === undefined || value === null) {
|
|
||||||
return $t('ui.formRules.selectRequired', [ctx.label]);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const useVbenForm = useForm<ComponentType>;
|
function useVbenForm<
|
||||||
|
TFormValues extends FormValues = FormValues,
|
||||||
|
TSubmitValues extends FormValues = TFormValues,
|
||||||
|
>(
|
||||||
|
options: FormProps<
|
||||||
|
ComponentType,
|
||||||
|
ComponentPropsMap,
|
||||||
|
TFormValues,
|
||||||
|
TSubmitValues
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return useForm<TFormValues, ComponentType, ComponentPropsMap, TSubmitValues>(
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export { useVbenForm, z };
|
export { initSetupVbenForm, useVbenForm, z };
|
||||||
|
|
||||||
export type VbenFormSchema = FormSchema<ComponentType>;
|
export type VbenFormSchema<TValues extends FormValues = FormValues> =
|
||||||
export type { VbenFormProps };
|
FormSchema<ComponentType, ComponentPropsMap, TValues>;
|
||||||
|
export type VbenFormProps<
|
||||||
|
TFormValues extends FormValues = FormValues,
|
||||||
|
TSubmitValues extends FormValues = TFormValues,
|
||||||
|
> = FormProps<ComponentType, ComponentPropsMap, TFormValues, TSubmitValues>;
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
|
import type { FormValues } from '@vben/common-ui';
|
||||||
|
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
|
||||||
|
|
||||||
|
import type { ComponentPropsMap, ComponentType } from './component';
|
||||||
|
|
||||||
import { h } from 'vue';
|
import { h } from 'vue';
|
||||||
|
|
||||||
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
|
import {
|
||||||
|
setupVbenVxeTable,
|
||||||
|
useVbenVxeGrid as useGrid,
|
||||||
|
} from '@vben/plugins/vxe-table';
|
||||||
|
|
||||||
import { Button, Image } from 'ant-design-vue';
|
import { Button, Image } from 'ant-design-vue';
|
||||||
|
|
||||||
@@ -33,14 +41,15 @@ setupVbenVxeTable({
|
|||||||
round: true,
|
round: true,
|
||||||
showOverflow: true,
|
showOverflow: true,
|
||||||
size: 'small',
|
size: 'small',
|
||||||
},
|
} as VxeTableGridOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
||||||
vxeUI.renderer.add('CellImage', {
|
vxeUI.renderer.add('CellImage', {
|
||||||
renderTableDefault(_renderOpts, params) {
|
renderTableDefault(renderOpts, params) {
|
||||||
|
const { props } = renderOpts;
|
||||||
const { column, row } = params;
|
const { column, row } = params;
|
||||||
return h(Image, { src: row[column.field] });
|
return h(Image, { src: row[column.field], ...props });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -62,6 +71,23 @@ setupVbenVxeTable({
|
|||||||
useVbenForm,
|
useVbenForm,
|
||||||
});
|
});
|
||||||
|
|
||||||
export { useVbenVxeGrid };
|
export const useVbenVxeGrid = <
|
||||||
|
T extends Record<string, any>,
|
||||||
|
TFormValues extends FormValues = FormValues,
|
||||||
|
TSubmitValues extends FormValues = TFormValues,
|
||||||
|
>(
|
||||||
|
...rest: Parameters<
|
||||||
|
typeof useGrid<
|
||||||
|
T,
|
||||||
|
ComponentType,
|
||||||
|
ComponentPropsMap,
|
||||||
|
TFormValues,
|
||||||
|
TSubmitValues
|
||||||
|
>
|
||||||
|
>
|
||||||
|
) =>
|
||||||
|
useGrid<T, ComponentType, ComponentPropsMap, TFormValues, TSubmitValues>(
|
||||||
|
...rest,
|
||||||
|
);
|
||||||
|
|
||||||
export type * from '@vben/plugins/vxe-table';
|
export type * from '@vben/plugins/vxe-table';
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { useTitle } from '@vueuse/core';
|
|||||||
import { $t, setupI18n } from '#/locales';
|
import { $t, setupI18n } from '#/locales';
|
||||||
|
|
||||||
import { initComponentAdapter } from './adapter/component';
|
import { initComponentAdapter } from './adapter/component';
|
||||||
|
import { initSetupVbenForm } from './adapter/form';
|
||||||
import App from './app.vue';
|
import App from './app.vue';
|
||||||
import { router } from './router';
|
import { router } from './router';
|
||||||
|
|
||||||
@@ -19,6 +20,9 @@ async function bootstrap(namespace: string) {
|
|||||||
// 初始化组件适配器
|
// 初始化组件适配器
|
||||||
await initComponentAdapter();
|
await initComponentAdapter();
|
||||||
|
|
||||||
|
// 初始化表单组件
|
||||||
|
await initSetupVbenForm();
|
||||||
|
|
||||||
// // 设置弹窗的默认配置
|
// // 设置弹窗的默认配置
|
||||||
// setDefaultModalProps({
|
// setDefaultModalProps({
|
||||||
// fullscreenButton: false,
|
// fullscreenButton: false,
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ import { $t } from '#/locales';
|
|||||||
|
|
||||||
const appName = computed(() => preferences.app.name);
|
const appName = computed(() => preferences.app.name);
|
||||||
const logo = computed(() => preferences.logo.source);
|
const logo = computed(() => preferences.logo.source);
|
||||||
|
const logoDark = computed(() => preferences.logo.sourceDark);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AuthPageLayout
|
<AuthPageLayout
|
||||||
:app-name="appName"
|
:app-name="appName"
|
||||||
:logo="logo"
|
:logo="logo"
|
||||||
|
:logo-dark="logoDark"
|
||||||
:page-description="$t('authentication.pageDesc')"
|
:page-description="$t('authentication.pageDesc')"
|
||||||
:page-title="$t('authentication.pageTitle')"
|
:page-title="$t('authentication.pageTitle')"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,18 +2,19 @@
|
|||||||
import type { NotificationItem } from '@vben/layouts';
|
import type { NotificationItem } from '@vben/layouts';
|
||||||
|
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
|
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
|
||||||
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
|
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
|
||||||
import { useWatermark } from '@vben/hooks';
|
import { useWatermark } from '@vben/hooks';
|
||||||
import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
|
import { BookOpenText, CircleHelp, SvgGithubIcon } from '@vben/icons';
|
||||||
import {
|
import {
|
||||||
BasicLayout,
|
BasicLayout,
|
||||||
LockScreen,
|
LockScreen,
|
||||||
Notification,
|
Notification,
|
||||||
UserDropdown,
|
UserDropdown,
|
||||||
} from '@vben/layouts';
|
} from '@vben/layouts';
|
||||||
import { preferences } from '@vben/preferences';
|
import { preferences, usePreferences } from '@vben/preferences';
|
||||||
import { useAccessStore, useUserStore } from '@vben/stores';
|
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||||
import { openWindow } from '@vben/utils';
|
import { openWindow } from '@vben/utils';
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ import LoginForm from '#/views/_core/authentication/login.vue';
|
|||||||
|
|
||||||
const notifications = ref<NotificationItem[]>([
|
const notifications = ref<NotificationItem[]>([
|
||||||
{
|
{
|
||||||
|
id: 1,
|
||||||
avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
|
avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
|
||||||
date: '3小时前',
|
date: '3小时前',
|
||||||
isRead: true,
|
isRead: true,
|
||||||
@@ -30,6 +32,7 @@ const notifications = ref<NotificationItem[]>([
|
|||||||
title: '收到了 14 份新周报',
|
title: '收到了 14 份新周报',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 2,
|
||||||
avatar: 'https://avatar.vercel.sh/1',
|
avatar: 'https://avatar.vercel.sh/1',
|
||||||
date: '刚刚',
|
date: '刚刚',
|
||||||
isRead: false,
|
isRead: false,
|
||||||
@@ -37,6 +40,7 @@ const notifications = ref<NotificationItem[]>([
|
|||||||
title: '朱偏右 回复了你',
|
title: '朱偏右 回复了你',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 3,
|
||||||
avatar: 'https://avatar.vercel.sh/1',
|
avatar: 'https://avatar.vercel.sh/1',
|
||||||
date: '2024-01-01',
|
date: '2024-01-01',
|
||||||
isRead: false,
|
isRead: false,
|
||||||
@@ -44,23 +48,51 @@ const notifications = ref<NotificationItem[]>([
|
|||||||
title: '曲丽丽 评论了你',
|
title: '曲丽丽 评论了你',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 4,
|
||||||
avatar: 'https://avatar.vercel.sh/satori',
|
avatar: 'https://avatar.vercel.sh/satori',
|
||||||
date: '1天前',
|
date: '1天前',
|
||||||
isRead: false,
|
isRead: false,
|
||||||
message: '描述信息描述信息描述信息',
|
message: '描述信息描述信息描述信息',
|
||||||
title: '代办提醒',
|
title: '代办提醒',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
avatar: 'https://avatar.vercel.sh/satori',
|
||||||
|
date: '1天前',
|
||||||
|
isRead: false,
|
||||||
|
message: '描述信息描述信息描述信息',
|
||||||
|
title: '跳转Workspace示例',
|
||||||
|
link: '/workspace',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
avatar: 'https://avatar.vercel.sh/satori',
|
||||||
|
date: '1天前',
|
||||||
|
isRead: false,
|
||||||
|
message: '描述信息描述信息描述信息',
|
||||||
|
title: '跳转外部链接示例',
|
||||||
|
link: 'https://doc.vben.pro',
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const accessStore = useAccessStore();
|
const accessStore = useAccessStore();
|
||||||
const { destroyWatermark, updateWatermark } = useWatermark();
|
const { destroyWatermark, updateWatermark } = useWatermark();
|
||||||
|
const { isDark } = usePreferences();
|
||||||
const showDot = computed(() =>
|
const showDot = computed(() =>
|
||||||
notifications.value.some((item) => !item.isRead),
|
notifications.value.some((item) => !item.isRead),
|
||||||
);
|
);
|
||||||
|
|
||||||
const menus = computed(() => [
|
const menus = computed(() => [
|
||||||
|
{
|
||||||
|
handler: () => {
|
||||||
|
router.push({ name: 'Profile' });
|
||||||
|
},
|
||||||
|
icon: 'lucide:user',
|
||||||
|
text: $t('page.auth.profile'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
handler: () => {
|
handler: () => {
|
||||||
openWindow(VBEN_DOC_URL, {
|
openWindow(VBEN_DOC_URL, {
|
||||||
@@ -76,7 +108,7 @@ const menus = computed(() => [
|
|||||||
target: '_blank',
|
target: '_blank',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
icon: MdiGithub,
|
icon: SvgGithubIcon,
|
||||||
text: 'GitHub',
|
text: 'GitHub',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -102,15 +134,77 @@ function handleNoticeClear() {
|
|||||||
notifications.value = [];
|
notifications.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function markRead(id: number | string) {
|
||||||
|
const item = notifications.value.find((item) => item.id === id);
|
||||||
|
if (item) {
|
||||||
|
item.isRead = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(id: number | string) {
|
||||||
|
notifications.value = notifications.value.filter((item) => item.id !== id);
|
||||||
|
}
|
||||||
|
|
||||||
function handleMakeAll() {
|
function handleMakeAll() {
|
||||||
notifications.value.forEach((item) => (item.isRead = true));
|
notifications.value.forEach((item) => (item.isRead = true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const viewAll = () => {};
|
||||||
|
|
||||||
|
const handleClick = (item: NotificationItem) => {
|
||||||
|
// 如果通知项有链接,点击时跳转
|
||||||
|
if (item.link) {
|
||||||
|
navigateTo(item.link, item.query, item.state);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function navigateTo(
|
||||||
|
link: string,
|
||||||
|
query?: Record<string, any>,
|
||||||
|
state?: Record<string, any>,
|
||||||
|
) {
|
||||||
|
if (link.startsWith('http://') || link.startsWith('https://')) {
|
||||||
|
// 外部链接,在新标签页打开
|
||||||
|
window.open(link, '_blank');
|
||||||
|
} else {
|
||||||
|
// 内部路由链接,支持 query 参数和 state
|
||||||
|
router.push({
|
||||||
|
path: link,
|
||||||
|
query: query || {},
|
||||||
|
state,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => preferences.app.watermark,
|
() => ({
|
||||||
async (enable) => {
|
enable: preferences.app.watermark,
|
||||||
|
content: preferences.app.watermarkContent,
|
||||||
|
isDark: isDark.value,
|
||||||
|
}),
|
||||||
|
async ({ enable, content, isDark: isDarkValue }) => {
|
||||||
if (enable) {
|
if (enable) {
|
||||||
|
const watermarkColor = isDarkValue
|
||||||
|
? 'rgba(255, 255, 255, 0.12)'
|
||||||
|
: 'rgba(0, 0, 0, 0.12)';
|
||||||
|
|
||||||
await updateWatermark({
|
await updateWatermark({
|
||||||
content: `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
|
advancedStyle: {
|
||||||
|
colorStops: [
|
||||||
|
{
|
||||||
|
color: watermarkColor,
|
||||||
|
offset: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: watermarkColor,
|
||||||
|
offset: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'linear',
|
||||||
|
},
|
||||||
|
content:
|
||||||
|
content ||
|
||||||
|
`${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
destroyWatermark();
|
destroyWatermark();
|
||||||
@@ -123,7 +217,12 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<BasicLayout @clear-preferences-and-logout="handleLogout">
|
<BasicLayout
|
||||||
|
:avatar
|
||||||
|
:text="userStore.userInfo?.realName"
|
||||||
|
@clear-preferences-and-logout="handleLogout"
|
||||||
|
@logout="handleLogout"
|
||||||
|
>
|
||||||
<template #user-dropdown>
|
<template #user-dropdown>
|
||||||
<UserDropdown
|
<UserDropdown
|
||||||
:avatar
|
:avatar
|
||||||
@@ -139,7 +238,11 @@ watch(
|
|||||||
:dot="showDot"
|
:dot="showDot"
|
||||||
:notifications="notifications"
|
:notifications="notifications"
|
||||||
@clear="handleNoticeClear"
|
@clear="handleNoticeClear"
|
||||||
|
@read="(item) => item.id && markRead(item.id)"
|
||||||
|
@remove="(item) => item.id && remove(item.id)"
|
||||||
@make-all="handleMakeAll"
|
@make-all="handleMakeAll"
|
||||||
|
@on-click="handleClick"
|
||||||
|
@view-all="viewAll"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #extra>
|
<template #extra>
|
||||||
|
|||||||
@@ -6,7 +6,9 @@
|
|||||||
"about": "About",
|
"about": "About",
|
||||||
"document": "Document",
|
"document": "Document",
|
||||||
"antdv": "Ant Design Vue Version",
|
"antdv": "Ant Design Vue Version",
|
||||||
|
"antdv-next": "Antdv Next Version",
|
||||||
"naive-ui": "Naive UI Version",
|
"naive-ui": "Naive UI Version",
|
||||||
"element-plus": "Element Plus Version"
|
"element-plus": "Element Plus Version",
|
||||||
|
"tdesign": "TDesign Vue Version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
"register": "Register",
|
"register": "Register",
|
||||||
"codeLogin": "Code Login",
|
"codeLogin": "Code Login",
|
||||||
"qrcodeLogin": "Qr Code Login",
|
"qrcodeLogin": "Qr Code Login",
|
||||||
"forgetPassword": "Forget Password"
|
"forgetPassword": "Forget Password",
|
||||||
|
"profile": "Profile"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "Dashboard",
|
"title": "Dashboard",
|
||||||
|
|||||||
@@ -6,7 +6,9 @@
|
|||||||
"about": "关于",
|
"about": "关于",
|
||||||
"document": "文档",
|
"document": "文档",
|
||||||
"antdv": "Ant Design Vue 版本",
|
"antdv": "Ant Design Vue 版本",
|
||||||
|
"antdv-next": "Antdv Next 版本",
|
||||||
"naive-ui": "Naive UI 版本",
|
"naive-ui": "Naive UI 版本",
|
||||||
"element-plus": "Element Plus 版本"
|
"element-plus": "Element Plus 版本",
|
||||||
|
"tdesign": "TDesign Vue 版本"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
"register": "注册",
|
"register": "注册",
|
||||||
"codeLogin": "验证码登录",
|
"codeLogin": "验证码登录",
|
||||||
"qrcodeLogin": "二维码登录",
|
"qrcodeLogin": "二维码登录",
|
||||||
"forgetPassword": "忘记密码"
|
"forgetPassword": "忘记密码",
|
||||||
|
"profile": "个人中心"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "概览",
|
"title": "概览",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { initPreferences } from '@vben/preferences';
|
import { initPreferences } from '@vben/preferences';
|
||||||
import { unmountGlobalLoading } from '@vben/utils';
|
import { unmountGlobalLoading } from '@vben/utils';
|
||||||
|
|
||||||
import { overridesPreferences } from './preferences';
|
import { overridesPreferences, preferencesExtension } from './preferences';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 应用初始化完成之后再进行页面加载渲染
|
* 应用初始化完成之后再进行页面加载渲染
|
||||||
@@ -15,6 +15,7 @@ async function initApplication() {
|
|||||||
|
|
||||||
// app偏好设置初始化
|
// app偏好设置初始化
|
||||||
await initPreferences({
|
await initPreferences({
|
||||||
|
extension: preferencesExtension,
|
||||||
namespace,
|
namespace,
|
||||||
overrides: overridesPreferences,
|
overrides: overridesPreferences,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,15 @@
|
|||||||
import { defineOverridesPreferences } from '@vben/preferences';
|
import {
|
||||||
|
appCopyrightPreferences,
|
||||||
|
defineOverridesPreferences,
|
||||||
|
definePreferencesExtension,
|
||||||
|
} from '@vben/preferences';
|
||||||
|
|
||||||
|
interface WebAntdPreferencesExtension {
|
||||||
|
defaultTableSize: number;
|
||||||
|
enableFormFullscreen: boolean;
|
||||||
|
reportTitle: string;
|
||||||
|
tenantMode: 'multi' | 'single';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 项目配置文件
|
* @description 项目配置文件
|
||||||
@@ -32,4 +43,54 @@ export const overridesPreferences = defineOverridesPreferences({
|
|||||||
colorPrimary: 'hsl(245 82% 67%)',
|
colorPrimary: 'hsl(245 82% 67%)',
|
||||||
mode: 'auto',
|
mode: 'auto',
|
||||||
},
|
},
|
||||||
|
copyright: appCopyrightPreferences,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const preferencesExtension =
|
||||||
|
definePreferencesExtension<WebAntdPreferencesExtension>({
|
||||||
|
tabLabel: 'preferences.antd.tabLabel',
|
||||||
|
title: 'preferences.antd.title',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
component: 'switch',
|
||||||
|
defaultValue: true,
|
||||||
|
key: 'enableFormFullscreen',
|
||||||
|
label: 'preferences.antd.fields.enableFormFullscreen.label',
|
||||||
|
tip: 'preferences.antd.fields.enableFormFullscreen.tip',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'select',
|
||||||
|
defaultValue: 'single',
|
||||||
|
key: 'tenantMode',
|
||||||
|
label: 'preferences.antd.fields.tenantMode.label',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'preferences.antd.fields.tenantMode.options.single.label',
|
||||||
|
value: 'single',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'preferences.antd.fields.tenantMode.options.multi.label',
|
||||||
|
value: 'multi',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'number',
|
||||||
|
componentProps: {
|
||||||
|
max: 200,
|
||||||
|
min: 10,
|
||||||
|
step: 10,
|
||||||
|
},
|
||||||
|
defaultValue: 20,
|
||||||
|
key: 'defaultTableSize',
|
||||||
|
label: 'preferences.antd.fields.defaultTableSize.label',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'input',
|
||||||
|
defaultValue: '',
|
||||||
|
key: 'reportTitle',
|
||||||
|
label: 'preferences.antd.fields.reportTitle.label',
|
||||||
|
placeholder: 'preferences.antd.fields.reportTitle.placeholder',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
name: 'Analytics',
|
name: 'Analytics',
|
||||||
path: '/analytics',
|
path: 'analytics',
|
||||||
component: () => import('#/views/dashboard/analytics/index.vue'),
|
component: () => import('#/views/dashboard/analytics/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
affixTab: true,
|
affixTab: true,
|
||||||
@@ -24,7 +24,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Workspace',
|
name: 'Workspace',
|
||||||
path: '/workspace',
|
path: 'workspace',
|
||||||
component: () => import('#/views/dashboard/workspace/index.vue'),
|
component: () => import('#/views/dashboard/workspace/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
icon: 'carbon:workspace',
|
icon: 'carbon:workspace',
|
||||||
|
|||||||
28
apps/web-antd/src/router/routes/modules/demos.ts
Normal file
28
apps/web-antd/src/router/routes/modules/demos.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
meta: {
|
||||||
|
icon: 'ic:baseline-view-in-ar',
|
||||||
|
keepAlive: true,
|
||||||
|
order: 1000,
|
||||||
|
title: $t('demos.title'),
|
||||||
|
},
|
||||||
|
name: 'Demos',
|
||||||
|
path: '/demos',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
meta: {
|
||||||
|
title: $t('demos.antd'),
|
||||||
|
},
|
||||||
|
name: 'AntDesignDemos',
|
||||||
|
path: 'ant-design',
|
||||||
|
component: () => import('#/views/demos/antd/index.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
import type { RouteRecordRaw } from 'vue-router';
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
VBEN_ANTDV_NEXT_PREVIEW_URL,
|
||||||
VBEN_DOC_URL,
|
VBEN_DOC_URL,
|
||||||
VBEN_ELE_PREVIEW_URL,
|
VBEN_ELE_PREVIEW_URL,
|
||||||
VBEN_GITHUB_URL,
|
VBEN_GITHUB_URL,
|
||||||
VBEN_LOGO_URL,
|
VBEN_LOGO_URL,
|
||||||
VBEN_NAIVE_PREVIEW_URL,
|
VBEN_NAIVE_PREVIEW_URL,
|
||||||
|
VBEN_TD_PREVIEW_URL,
|
||||||
} from '@vben/constants';
|
} from '@vben/constants';
|
||||||
|
import { SvgAntdvNextLogoIcon, SvgTDesignIcon } from '@vben/icons';
|
||||||
|
|
||||||
import { IFrameView } from '#/layouts';
|
import { IFrameView } from '#/layouts';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
@@ -24,7 +27,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
name: 'VbenDocument',
|
name: 'VbenDocument',
|
||||||
path: '/vben-admin/document',
|
path: 'document',
|
||||||
component: IFrameView,
|
component: IFrameView,
|
||||||
meta: {
|
meta: {
|
||||||
icon: 'lucide:book-open-text',
|
icon: 'lucide:book-open-text',
|
||||||
@@ -34,7 +37,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'VbenGithub',
|
name: 'VbenGithub',
|
||||||
path: '/vben-admin/github',
|
path: 'github',
|
||||||
component: IFrameView,
|
component: IFrameView,
|
||||||
meta: {
|
meta: {
|
||||||
icon: 'mdi:github',
|
icon: 'mdi:github',
|
||||||
@@ -42,9 +45,20 @@ const routes: RouteRecordRaw[] = [
|
|||||||
title: 'Github',
|
title: 'Github',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'VbenAntdVNext',
|
||||||
|
path: 'antdv-next',
|
||||||
|
component: IFrameView,
|
||||||
|
meta: {
|
||||||
|
badgeType: 'dot',
|
||||||
|
icon: SvgAntdvNextLogoIcon,
|
||||||
|
link: VBEN_ANTDV_NEXT_PREVIEW_URL,
|
||||||
|
title: $t('demos.vben.antdv-next'),
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'VbenNaive',
|
name: 'VbenNaive',
|
||||||
path: '/vben-admin/naive',
|
path: 'naive',
|
||||||
component: IFrameView,
|
component: IFrameView,
|
||||||
meta: {
|
meta: {
|
||||||
badgeType: 'dot',
|
badgeType: 'dot',
|
||||||
@@ -53,9 +67,20 @@ const routes: RouteRecordRaw[] = [
|
|||||||
title: $t('demos.vben.naive-ui'),
|
title: $t('demos.vben.naive-ui'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'VbenTDesign',
|
||||||
|
path: 'tdesign',
|
||||||
|
component: IFrameView,
|
||||||
|
meta: {
|
||||||
|
badgeType: 'dot',
|
||||||
|
icon: SvgTDesignIcon,
|
||||||
|
link: VBEN_TD_PREVIEW_URL,
|
||||||
|
title: $t('demos.vben.tdesign'),
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'VbenElementPlus',
|
name: 'VbenElementPlus',
|
||||||
path: '/vben-admin/ele',
|
path: 'ele',
|
||||||
component: IFrameView,
|
component: IFrameView,
|
||||||
meta: {
|
meta: {
|
||||||
badgeType: 'dot',
|
badgeType: 'dot',
|
||||||
@@ -76,6 +101,16 @@ const routes: RouteRecordRaw[] = [
|
|||||||
order: 9999,
|
order: 9999,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Profile',
|
||||||
|
path: '/profile',
|
||||||
|
component: () => import('#/views/_core/profile/index.vue'),
|
||||||
|
meta: {
|
||||||
|
icon: 'lucide:user',
|
||||||
|
hideInMenu: true,
|
||||||
|
title: $t('page.auth.profile'),
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default routes;
|
export default routes;
|
||||||
|
|||||||
@@ -98,8 +98,7 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchUserInfo() {
|
async function fetchUserInfo() {
|
||||||
let userInfo: null | UserInfo = null;
|
const userInfo = await getUserInfoApi();
|
||||||
userInfo = await getUserInfoApi();
|
|
||||||
userStore.setUserInfo(userInfo);
|
userStore.setUserInfo(userInfo);
|
||||||
return userInfo;
|
return userInfo;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,8 +55,7 @@ const formSchema = computed((): VbenFormSchema[] => {
|
|||||||
* @param values 登录表单数据
|
* @param values 登录表单数据
|
||||||
*/
|
*/
|
||||||
async function handleLogin(values: Recordable<any>) {
|
async function handleLogin(values: Recordable<any>) {
|
||||||
// eslint-disable-next-line no-console
|
void values;
|
||||||
console.log(values);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -29,8 +29,7 @@ const formSchema = computed((): VbenFormSchema[] => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function handleSubmit(value: Recordable<any>) {
|
function handleSubmit(value: Recordable<any>) {
|
||||||
// eslint-disable-next-line no-console
|
void value;
|
||||||
console.log('reset email:', value);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const formSchema = computed((): VbenFormSchema[] => {
|
|||||||
rules(values) {
|
rules(values) {
|
||||||
const { password } = values;
|
const { password } = values;
|
||||||
return z
|
return z
|
||||||
.string({ required_error: $t('authentication.passwordTip') })
|
.string({ error: $t('authentication.passwordTip') })
|
||||||
.min(1, { message: $t('authentication.passwordTip') })
|
.min(1, { message: $t('authentication.passwordTip') })
|
||||||
.refine((value) => value === password, {
|
.refine((value) => value === password, {
|
||||||
message: $t('authentication.confirmPasswordTip'),
|
message: $t('authentication.confirmPasswordTip'),
|
||||||
@@ -82,8 +82,7 @@ const formSchema = computed((): VbenFormSchema[] => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function handleSubmit(value: Recordable<any>) {
|
function handleSubmit(value: Recordable<any>) {
|
||||||
// eslint-disable-next-line no-console
|
void value;
|
||||||
console.log('register submit:', value);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
65
apps/web-antd/src/views/_core/profile/base-setting.vue
Normal file
65
apps/web-antd/src/views/_core/profile/base-setting.vue
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { BasicOption } from '@vben/types';
|
||||||
|
|
||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { ProfileBaseSetting } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { getUserInfoApi } from '#/api';
|
||||||
|
|
||||||
|
const profileBaseSettingRef = ref();
|
||||||
|
|
||||||
|
const MOCK_ROLES_OPTIONS: BasicOption[] = [
|
||||||
|
{
|
||||||
|
label: '管理员',
|
||||||
|
value: 'super',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '用户',
|
||||||
|
value: 'user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '测试',
|
||||||
|
value: 'test',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const formSchema = computed((): VbenFormSchema[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'realName',
|
||||||
|
component: 'Input',
|
||||||
|
label: '姓名',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'username',
|
||||||
|
component: 'Input',
|
||||||
|
label: '用户名',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'roles',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
mode: 'tags',
|
||||||
|
options: MOCK_ROLES_OPTIONS,
|
||||||
|
},
|
||||||
|
label: '角色',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'introduction',
|
||||||
|
component: 'Textarea',
|
||||||
|
label: '个人简介',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const data = await getUserInfoApi();
|
||||||
|
profileBaseSettingRef.value.getFormApi().setValues(data);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<ProfileBaseSetting ref="profileBaseSettingRef" :form-schema="formSchema" />
|
||||||
|
</template>
|
||||||
49
apps/web-antd/src/views/_core/profile/index.vue
Normal file
49
apps/web-antd/src/views/_core/profile/index.vue
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { Profile } from '@vben/common-ui';
|
||||||
|
import { useUserStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import ProfileBase from './base-setting.vue';
|
||||||
|
import ProfileNotificationSetting from './notification-setting.vue';
|
||||||
|
import ProfilePasswordSetting from './password-setting.vue';
|
||||||
|
import ProfileSecuritySetting from './security-setting.vue';
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
const tabsValue = ref<string>('basic');
|
||||||
|
|
||||||
|
const tabs = ref([
|
||||||
|
{
|
||||||
|
label: '基本设置',
|
||||||
|
value: 'basic',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '安全设置',
|
||||||
|
value: 'security',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '修改密码',
|
||||||
|
value: 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '新消息提醒',
|
||||||
|
value: 'notice',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Profile
|
||||||
|
v-model:model-value="tabsValue"
|
||||||
|
title="个人中心"
|
||||||
|
:user-info="userStore.userInfo"
|
||||||
|
:tabs="tabs"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<ProfileBase v-if="tabsValue === 'basic'" />
|
||||||
|
<ProfileSecuritySetting v-if="tabsValue === 'security'" />
|
||||||
|
<ProfilePasswordSetting v-if="tabsValue === 'password'" />
|
||||||
|
<ProfileNotificationSetting v-if="tabsValue === 'notice'" />
|
||||||
|
</template>
|
||||||
|
</Profile>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { ProfileNotificationSetting } from '@vben/common-ui';
|
||||||
|
|
||||||
|
const formSchema = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
fieldName: 'accountPassword',
|
||||||
|
label: '账户密码',
|
||||||
|
description: '其他用户的消息将以站内信的形式通知',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
fieldName: 'systemMessage',
|
||||||
|
label: '系统消息',
|
||||||
|
description: '系统消息将以站内信的形式通知',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
fieldName: 'todoTask',
|
||||||
|
label: '待办任务',
|
||||||
|
description: '待办任务将以站内信的形式通知',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<ProfileNotificationSetting :form-schema="formSchema" />
|
||||||
|
</template>
|
||||||
63
apps/web-antd/src/views/_core/profile/password-setting.vue
Normal file
63
apps/web-antd/src/views/_core/profile/password-setting.vue
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { ProfilePasswordSetting, z } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
const formSchema = computed((): VbenFormSchema[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'oldPassword',
|
||||||
|
label: '旧密码',
|
||||||
|
component: 'VbenInputPassword',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入旧密码',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'newPassword',
|
||||||
|
label: '新密码',
|
||||||
|
component: 'VbenInputPassword',
|
||||||
|
componentProps: {
|
||||||
|
passwordStrength: true,
|
||||||
|
placeholder: '请输入新密码',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'confirmPassword',
|
||||||
|
label: '确认密码',
|
||||||
|
component: 'VbenInputPassword',
|
||||||
|
componentProps: {
|
||||||
|
passwordStrength: true,
|
||||||
|
placeholder: '请再次输入新密码',
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
rules(values) {
|
||||||
|
const { newPassword } = values;
|
||||||
|
return z
|
||||||
|
.string({ error: '请再次输入新密码' })
|
||||||
|
.min(1, { message: '请再次输入新密码' })
|
||||||
|
.refine((value) => value === newPassword, {
|
||||||
|
message: '两次输入的密码不一致',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
triggerFields: ['newPassword'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
message.success('密码修改成功');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<ProfilePasswordSetting
|
||||||
|
class="w-1/3"
|
||||||
|
:form-schema="formSchema"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
43
apps/web-antd/src/views/_core/profile/security-setting.vue
Normal file
43
apps/web-antd/src/views/_core/profile/security-setting.vue
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { ProfileSecuritySetting } from '@vben/common-ui';
|
||||||
|
|
||||||
|
const formSchema = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
fieldName: 'accountPassword',
|
||||||
|
label: '账户密码',
|
||||||
|
description: '当前密码强度:强',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
fieldName: 'securityPhone',
|
||||||
|
label: '密保手机',
|
||||||
|
description: '已绑定手机:138****8293',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
fieldName: 'securityQuestion',
|
||||||
|
label: '密保问题',
|
||||||
|
description: '未设置密保问题,密保问题可有效保护账户安全',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
fieldName: 'securityEmail',
|
||||||
|
label: '备用邮箱',
|
||||||
|
description: '已绑定邮箱:ant***sign.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: false,
|
||||||
|
fieldName: 'securityMfa',
|
||||||
|
label: 'MFA 设备',
|
||||||
|
description: '未绑定 MFA 设备,绑定后,可以进行二次确认',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<ProfileSecuritySetting :form-schema="formSchema" />
|
||||||
|
</template>
|
||||||
@@ -24,7 +24,7 @@ onMounted(() => {
|
|||||||
{ name: '定制', value: 310 },
|
{ name: '定制', value: 310 },
|
||||||
{ name: '技术支持', value: 274 },
|
{ name: '技术支持', value: 274 },
|
||||||
{ name: '远程', value: 400 },
|
{ name: '远程', value: 400 },
|
||||||
].sort((a, b) => {
|
].toSorted((a, b) => {
|
||||||
return a.value - b.value;
|
return a.value - b.value;
|
||||||
}),
|
}),
|
||||||
name: '商业占比',
|
name: '商业占比',
|
||||||
|
|||||||
@@ -76,10 +76,10 @@ const chartTabs: TabOption[] = [
|
|||||||
</AnalysisChartsTabs>
|
</AnalysisChartsTabs>
|
||||||
|
|
||||||
<div class="mt-5 w-full md:flex">
|
<div class="mt-5 w-full md:flex">
|
||||||
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问数量">
|
<AnalysisChartCard class="mt-5 md:mt-0 md:mr-4 md:w-1/3" title="访问数量">
|
||||||
<AnalyticsVisitsData />
|
<AnalyticsVisitsData />
|
||||||
</AnalysisChartCard>
|
</AnalysisChartCard>
|
||||||
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问来源">
|
<AnalysisChartCard class="mt-5 md:mt-0 md:mr-4 md:w-1/3" title="访问来源">
|
||||||
<AnalyticsVisitsSource />
|
<AnalyticsVisitsSource />
|
||||||
</AnalysisChartCard>
|
</AnalysisChartCard>
|
||||||
<AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="访问来源">
|
<AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="访问来源">
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
|||||||
<template #description> 今日晴,20℃ - 32℃! </template>
|
<template #description> 今日晴,20℃ - 32℃! </template>
|
||||||
</WorkbenchHeader>
|
</WorkbenchHeader>
|
||||||
|
|
||||||
<div class="mt-5 flex flex-col lg:flex-row">
|
<div class="flex flex-col lg:flex-row">
|
||||||
<div class="mr-4 w-full lg:w-3/5">
|
<div class="mr-4 w-full lg:w-3/5">
|
||||||
<WorkbenchProject :items="projectItems" title="项目" @click="navTo" />
|
<WorkbenchProject :items="projectItems" title="项目" @click="navTo" />
|
||||||
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" />
|
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" />
|
||||||
@@ -252,7 +252,7 @@ function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
|||||||
<div class="w-full lg:w-2/5">
|
<div class="w-full lg:w-2/5">
|
||||||
<WorkbenchQuickNav
|
<WorkbenchQuickNav
|
||||||
:items="quickNavItems"
|
:items="quickNavItems"
|
||||||
class="mt-5 lg:mt-0"
|
class="lg:mt-0"
|
||||||
title="快捷导航"
|
title="快捷导航"
|
||||||
@click="navTo"
|
@click="navTo"
|
||||||
/>
|
/>
|
||||||
|
|||||||
68
apps/web-antd/src/views/demos/antd/index.vue
Normal file
68
apps/web-antd/src/views/demos/antd/index.vue
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Button, Card, message, notification, Space } from 'ant-design-vue';
|
||||||
|
|
||||||
|
type NotificationType = 'error' | 'info' | 'success' | 'warning';
|
||||||
|
|
||||||
|
function info() {
|
||||||
|
message.info('How many roads must a man walk down');
|
||||||
|
}
|
||||||
|
|
||||||
|
function error() {
|
||||||
|
message.error({
|
||||||
|
content: 'Once upon a time you dressed so fine',
|
||||||
|
// ant-design-vue 的 message.duration 单位是「秒」, 不是毫秒
|
||||||
|
duration: 2.5,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function warning() {
|
||||||
|
message.warning('How many roads must a man walk down');
|
||||||
|
}
|
||||||
|
function success() {
|
||||||
|
message.success('Cause you walked hand in hand With another man in my place');
|
||||||
|
}
|
||||||
|
|
||||||
|
function notify(type: NotificationType) {
|
||||||
|
notification[type]({
|
||||||
|
// ant-design-vue 的 notification.duration 单位是「秒」, 不是毫秒
|
||||||
|
duration: 2.5,
|
||||||
|
message: '说点啥呢',
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page
|
||||||
|
description="支持多语言,主题功能集成切换等"
|
||||||
|
title="Ant Design Vue组件使用演示"
|
||||||
|
>
|
||||||
|
<Card class="mb-5" title="按钮">
|
||||||
|
<Space>
|
||||||
|
<Button>Default</Button>
|
||||||
|
<Button type="primary"> Primary </Button>
|
||||||
|
<Button> Info </Button>
|
||||||
|
<Button danger> Error </Button>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
<Card class="mb-5" title="Message">
|
||||||
|
<Space>
|
||||||
|
<Button @click="info"> 信息 </Button>
|
||||||
|
<Button danger @click="error"> 错误 </Button>
|
||||||
|
<Button @click="warning"> 警告 </Button>
|
||||||
|
<Button @click="success"> 成功 </Button>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card class="mb-5" title="Notification">
|
||||||
|
<Space>
|
||||||
|
<Button @click="notify('info')"> 信息 </Button>
|
||||||
|
<Button danger @click="notify('error')"> 错误 </Button>
|
||||||
|
<Button @click="notify('warning')"> 警告 </Button>
|
||||||
|
<Button @click="notify('success')"> 成功 </Button>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from '@vben/tailwind-config';
|
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
"$schema": "https://json.schemastore.org/tsconfig",
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
"extends": "@vben/tsconfig/web-app.json",
|
"extends": "@vben/tsconfig/web-app.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"#/*": ["./src/*"]
|
"#/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,5 +6,5 @@
|
|||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
"noEmit": false
|
"noEmit": false
|
||||||
},
|
},
|
||||||
"include": ["vite.config.mts"]
|
"include": ["vite.config.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
8
apps/web-antdv-next/.env
Normal file
8
apps/web-antdv-next/.env
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# 应用标题
|
||||||
|
VITE_APP_TITLE=Vben Admin Antdv Next
|
||||||
|
|
||||||
|
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离
|
||||||
|
VITE_APP_NAMESPACE=vben-web-antdv-next
|
||||||
|
|
||||||
|
# 对store进行加密的密钥,在将store持久化到localStorage时会使用该密钥进行加密
|
||||||
|
VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key
|
||||||
7
apps/web-antdv-next/.env.analyze
Normal file
7
apps/web-antdv-next/.env.analyze
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# public path
|
||||||
|
VITE_BASE=/
|
||||||
|
|
||||||
|
# Basic interface address SPA
|
||||||
|
VITE_GLOB_API_URL=/api
|
||||||
|
|
||||||
|
VITE_VISUALIZER=true
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user