1
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Deploy Website on push / Rerun on failure (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled

This commit is contained in:
杨志
2025-12-05 13:39:40 +08:00
parent 21107f02fd
commit 51a72f1f0c
1239 changed files with 107262 additions and 1 deletions

4
.browserslistrc Normal file
View File

@@ -0,0 +1,4 @@
> 1%
last 2 versions
not dead
not ie 11

5
.changeset/README.md Normal file
View File

@@ -0,0 +1,5 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with multi-package repos, or single-package repos to help you version and publish your code. You can find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)

18
.changeset/config.json Normal file
View File

@@ -0,0 +1,18 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
"changelog": [
"@changesets/changelog-github",
{ "repo": "vbenjs/vue-vben-admin" }
],
"commit": false,
"fixed": [["@vben-core/*", "@vben/*"]],
"snapshot": {
"prereleaseTemplate": "{tag}-{datetime}"
},
"privatePackages": { "version": true, "tag": true },
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}

1
.commitlintrc.js Normal file
View File

@@ -0,0 +1 @@
export { default } from '@vben/commitlint-config';

7
.dockerignore Normal file
View File

@@ -0,0 +1,7 @@
node_modules
.git
.gitignore
*.md
dist
.turbo
dist.zip

18
.editorconfig Normal file
View File

@@ -0,0 +1,18 @@
root = true
[*]
charset=utf-8
end_of_line=lf
insert_final_newline=true
indent_style=space
indent_size=2
max_line_length = 100
trim_trailing_whitespace = true
quote_type = single
[*.{yml,yaml,json}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false

11
.gitattributes vendored Normal file
View File

@@ -0,0 +1,11 @@
# https://docs.github.com/cn/get-started/getting-started-with-git/configuring-git-to-handle-line-endings
# Automatically normalize line endings (to LF) for all text-based files.
* text=auto eol=lf
# Declare files that will always have CRLF line endings on checkout.
*.{cmd,[cC][mM][dD]} text eol=crlf
*.{bat,[bB][aA][tT]} text eol=crlf
# Denote all files that are truly binary and should not be modified.
*.{ico,png,jpg,jpeg,gif,webp,svg,woff,woff2} binary

2
.gitconfig Normal file
View File

@@ -0,0 +1,2 @@
[core]
ignorecase = false

14
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,14 @@
# default onwer
* anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
# vben core onwer
/.github/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
/.vscode/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
/packages/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
/packages/@core/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
/internal/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
/scripts/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
# vben team onwer
apps/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com @vbenjs/team-v5 jinmao88@qq.com
docs/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com @vbenjs/team-v5 jinmao88@qq.com

74
.github/ISSUE_TEMPLATE/bug-report.yml vendored Normal file
View File

@@ -0,0 +1,74 @@
name: 🐞 Bug Report
description: Report an issue with Vben Admin to help us make it better.
title: 'Bug: '
labels: ['bug: pending triage']
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: dropdown
id: version
attributes:
label: Version
description: What version of our software are you running?
options:
- Vben Admin V5
- Vben Admin V2
default: 0
validations:
required: true
- type: textarea
id: bug-desc
attributes:
label: Describe the bug?
description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, tell us in the description. Thanks!
placeholder: Bug Description
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Reproduction
description: Please provide a link to [StackBlitz](https://stackblitz.com/fork/github/vitest-dev/vitest/tree/main/examples/basic?initialPath=__vitest__/) (you can also use [examples](https://github.com/vitest-dev/vitest/tree/main/examples)) or a github repo that can reproduce the problem you ran into. A [minimal reproduction](https://stackoverflow.com/help/minimal-reproducible-example) is required unless you are absolutely sure that the issue is obvious and the provided information is enough to understand the problem. If a report is vague (e.g. just a generic error message) and has no reproduction, it will receive a "needs reproduction" label. If no reproduction is provided after 3 days, it will be auto-closed.
placeholder: Reproduction
validations:
required: true
- type: textarea
id: system-info
attributes:
label: System Info
description: Output of `npx envinfo --system --npmPackages '{vue}' --binaries --browsers`
render: shell
placeholder: System, Binaries, Browsers
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell
- type: checkboxes
id: terms
attributes:
label: Validations
description: Before submitting the issue, please make sure you do the following
# description: By submitting this issue, you agree to follow our [Code of Conduct](https://example.com).
options:
- label: Read the [docs](https://doc.vben.pro/)
required: true
- label: Ensure the code is up to date. (Some issues have been fixed in the latest version)
required: true
- label: I have searched the [existing issues](https://github.com/vbenjs/vue-vben-admin/issues) and checked that my issue does not duplicate any existing issues.
required: true
- label: Check that this is a concrete bug. For Q&A open a [GitHub Discussion](https://github.com/vbenjs/vue-vben-admin/discussions) or join our [Discord Chat Server](https://discord.gg/8GuAdwDhj6).
required: true
- label: The provided reproduction is a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) of the bug.
required: true

38
.github/ISSUE_TEMPLATE/docs.yml vendored Normal file
View File

@@ -0,0 +1,38 @@
name: 📚 Documentation
description: Report an issue with Vben Admin Website to help us make it better.
title: 'Docs: '
labels: [documentation]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this issue!
- type: checkboxes
id: documentation_is
attributes:
label: Documentation is
options:
- label: Missing
- label: Outdated
- label: Confusing
- label: Not sure?
- type: textarea
id: description
attributes:
label: Explain in Detail
description: A clear and concise description of your suggestion. If you intend to submit a PR for this issue, tell us in the description. Thanks!
placeholder: The description of ... page is not clear. I thought it meant ... but it wasn't.
validations:
required: true
- type: textarea
id: suggestion
attributes:
label: Your Suggestion for Changes
validations:
required: true
- type: textarea
id: reproduction-steps
attributes:
label: Steps to reproduce
description: Please provide any reproduction steps that may need to be described. E.g. if it happens only when running the dev or build script make sure it's clear which one to use.
placeholder: Run `pnpm install` followed by `pnpm run docs:dev`

View File

@@ -0,0 +1,70 @@
name: ✨ New Feature Proposal
description: Propose a new feature to be added to Vben Admin
title: 'FEATURE: '
labels: ['enhancement: pending triage']
body:
- type: markdown
attributes:
value: |
Thank you for suggesting a feature for our project! Please fill out the information below to help us understand and implement your request!
- type: dropdown
id: version
attributes:
label: Version
description: What version of our software are you running?
options:
- Vben Admin V5
- Vben Admin V2
default: 0
validations:
required: true
- type: textarea
id: description
attributes:
label: Description
description: A detailed description of the feature request.
placeholder: Please describe the feature you would like to see, and why it would be useful.
validations:
required: true
- type: textarea
id: proposed-solution
attributes:
label: Proposed Solution
description: A clear and concise description of what you want to happen.
placeholder: Describe the solution you'd like to see
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives Considered
description: |
A clear and concise description of any alternative solutions or features you've considered.
placeholder: Describe any alternative solutions or features you've considered
validations:
required: false
- type: input
id: additional-context
attributes:
label: Additional Context
description: Add any other context or screenshots about the feature request here.
placeholder: Any additional information
validations:
required: false
- type: checkboxes
id: checkboxes
attributes:
label: Validations
description: Before submitting the issue, please make sure you do the following
options:
- label: Read the [docs](https://doc.vben.pro/)
required: true
- label: Ensure the code is up to date. (Some issues have been fixed in the latest version)
required: true
- label: I have searched the [existing issues](https://github.com/vbenjs/vue-vben-admin/issues) and checked that my issue does not duplicate any existing issues.
required: true

40
.github/actions/setup-node/action.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: 'Setup Node'
description: 'Setup node and pnpm'
runs:
using: 'composite'
steps:
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version-file: .node-version
cache: 'pnpm'
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v4
name: Setup pnpm cache
if: ${{ github.ref_name == 'main' }}
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- uses: actions/cache/restore@v4
if: ${{ github.ref_name != 'main' }}
with:
path: ${{ env.STORE_PATH }}
key: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
shell: bash
run: pnpm install --frozen-lockfile

89
.github/commit-convention.md vendored Normal file
View File

@@ -0,0 +1,89 @@
## Git Commit Message Convention
> This is adapted from [Angular's commit convention](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular).
#### TL;DR:
Messages must be matched by the following regex:
```js
/^(revert: )?(feat|fix|docs|style|refactor|perf|test|workflow|build|ci|chore|types|wip): .{1,50}/;
```
#### Examples
Appears under "Features" header, `dev` subheader:
```
feat(dev): add 'comments' option
```
Appears under "Bug Fixes" header, `dev` subheader, with a link to issue #28:
```
fix(dev): fix dev error
close #28
```
Appears under "Performance Improvements" header, and under "Breaking Changes" with the breaking change explanation:
```
perf(build): remove 'foo' option
BREAKING CHANGE: The 'foo' option has been removed.
```
The following commit and commit `667ecc1` do not appear in the changelog if they are under the same release. If not, the revert commit appears under the "Reverts" header.
```
revert: feat(compiler): add 'comments' option
This reverts commit 667ecc1654a317a13331b17617d973392f415f02.
```
### Full Message Format
A commit message consists of a **header**, **body** and **footer**. The header has a **type**, **scope** and **subject**:
```
<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>
```
The **header** is mandatory and the **scope** of the header is optional.
### Revert
If the commit reverts a previous commit, it should begin with `revert: `, followed by the header of the reverted commit. In the body, it should say: `This reverts commit <hash>.`, where the hash is the SHA of the commit being reverted.
### Type
If the prefix is `feat`, `fix` or `perf`, it will appear in the changelog. However, if there is any [BREAKING CHANGE](#footer), the commit will always appear in the changelog.
Other prefixes are up to your discretion. Suggested prefixes are `docs`, `chore`, `style`, `refactor`, and `test` for non-changelog related tasks.
### Scope
The scope could be anything specifying the place of the commit change. For example `dev`, `build`, `workflow`, `cli` etc...
### Subject
The subject contains a succinct description of the change:
- use the imperative, present tense: "change" not "changed" nor "changes"
- don't capitalize the first letter
- no dot (.) at the end
### Body
Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes". The body should include the motivation for the change and contrast this with previous behavior.
### Footer
The footer should contain any information about **Breaking Changes** and is also the place to reference GitHub issues that this commit **Closes**.
**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this.

39
.github/config.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
# Prevent issues being created without using the template
blank_issues_enabled: false
checkIssueTemplate: true
checkPullRequestTemplate: true
contact_links:
- name: 💬 Discord Chat
url: https://discord.gg/8GuAdwDhj6
about: Ask questions and discuss with other Vben users in real time.
- name: ❓ Questions & Discussions
url: https://github.com/@vbenjs/vue-vben-admin/discussions
about: Use GitHub discussions for message-board style questions and discussions.
# Comment to be posted to on PRs from first time contributors in your repository
newPRWelcomeComment: |
💖 Thanks for opening this pull request! 💖
Please be patient and we will get back to you as soon as we can.
# Comment to be posted to on pull requests merged by a first time user
firstPRMergeComment: >
Thanks for your contribution! 🎉🎉🎉
# Comment to be posted to on first time issues
newIssueWelcomeComment: >
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
# MUST BE ALL LOWERCASE
requestInfoDefaultTitles:
- update readme.md
- updates
# *Required* Comment to reply with
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.

40
.github/contributing.md vendored Normal file
View File

@@ -0,0 +1,40 @@
# Vben Admin Contributing Guide
Hi! We're really excited that you are interested in contributing to Vben Admin. Before submitting your contribution, please make sure to take a moment and read through the following guidelines:
- [Pull Request Guidelines](#pull-request-guidelines)
## Contributor Code of Conduct
As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
We are committed to making participation in this project a harassment-free experience for everyone, regardless of the level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion.
Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
## Pull Request Guidelines
- Checkout a topic branch from the relevant branch, e.g. main, and merge back against that branch.
- If adding a new feature:
- Provide a convincing reason to add this feature. Ideally, you should open a suggestion issue first and have it approved before working on it.
- If fixing bug:
- Provide a detailed description of the bug in the PR. Live demo preferred.
- It's OK to have multiple small commits as you work on the PR - GitHub can automatically squash them before merging.
## Development Setup
You will need [pnpm](https://pnpm.io/)
After cloning the repo, run:
```bash
# install the dependencies of the project
$ pnpm install
# start the project
$ pnpm run dev
```

17
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
version: 2
updates:
- package-ecosystem: npm
directory: '/'
schedule:
interval: daily
groups:
non-breaking-changes:
update-types: [minor, patch]
- package-ecosystem: github-actions
directory: '/'
schedule:
interval: weekly
groups:
non-breaking-changes:
update-types: [minor, patch]

33
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,33 @@
## Description
<!-- Please describe the change as necessary. If it's a feature or enhancement please be as detailed as possible. If it's a bug fix, please link the issue that it fixes or describe the bug in as much detail.
-->
<!-- You can also add additional context here -->
## Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
- [ ] Please, don't make changes to `pnpm-lock.yaml` unless you introduce a new test example.
## Checklist
> Check all checkboxes - this will indicate that you have done everything in accordance with the rules in [CONTRIBUTING](contributing.md).
- [ ] If you introduce new functionality, document it. You can run documentation with `pnpm run docs:dev` command.
- [ ] Run the tests with `pnpm test`.
- [ ] Changes in changelog are generated from PR name. Please, make sure that it explains your changes in an understandable manner. Please, prefix changeset messages with `feat:`, `fix:`, `perf:`, `docs:`, or `chore:`.
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream modules

61
.github/release-drafter.yml vendored Normal file
View File

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

13
.github/semantic.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
titleAndCommits: true
types:
- feat
- fix
- docs
- chore
- style
- refactor
- perf
- test
- build
- ci
- revert

48
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,48 @@
# name: Dependabot post-update
name: Build detection
on:
pull_request_target:
types: [opened, synchronize, reopened]
branches:
- main
env:
HUSKY: '0'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: write
jobs:
post-update:
if: github.repository == 'vbenjs/vue-vben-admin'
# if: ${{ github.actor == 'dependabot[bot]' }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os:
- ubuntu-latest
# - macos-latest
- windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Checkout out pull request
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr checkout ${{ github.event.pull_request.number }}
- name: Setup Node
uses: ./.github/actions/setup-node
- name: Build
run: |
pnpm run build

42
.github/workflows/changeset-version.yml vendored Normal file
View File

@@ -0,0 +1,42 @@
# https://github.com/changesets/action
name: Changeset version
on:
workflow_dispatch:
pull_request:
types:
- closed
branches:
- main
permissions:
pull-requests: write
contents: write
env:
CI: true
jobs:
version:
if: (github.event.pull_request.merged || github.event_name == 'workflow_dispatch') && github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]') && github.repository == 'vbenjs/vue-vben-admin'
# if: github.repository == 'vbenjs/vue-vben-admin'
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node
uses: ./.github/actions/setup-node
- name: Create Release Pull Request
uses: changesets/action@v1
with:
version: pnpm run version
commit: 'chore: bump versions'
title: 'chore: bump versions'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

125
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,125 @@
name: CI
on:
pull_request:
push:
branches:
- main
- 'releases/*'
permissions:
contents: read
env:
CI: true
TZ: Asia/Shanghai
jobs:
test:
name: Test
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ${{ matrix.os }}
strategy:
matrix:
os:
- ubuntu-latest
# - macos-latest
- windows-latest
timeout-minutes: 20
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- name: Setup Node
uses: ./.github/actions/setup-node
# - name: Check Git version
# run: git --version
# - name: Setup mock Git user
# run: git config --global user.email "you@example.com" && git config --global user.name "Your Name"
- name: Vitest tests
run: pnpm run test:unit
# - name: Upload coverage
# uses: codecov/codecov-action@v4
# with:
# token: ${{ secrets.CODECOV_TOKEN }}
lint:
name: Lint
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ${{ matrix.os }}
strategy:
matrix:
os:
- ubuntu-latest
# - macos-latest
- windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node
uses: ./.github/actions/setup-node
- name: Lint
run: pnpm run lint
check:
name: Check
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ${{ matrix.os }}
timeout-minutes: 20
strategy:
matrix:
os:
- ubuntu-latest
# - macos-latest
- windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node
uses: ./.github/actions/setup-node
- name: Typecheck
run: pnpm check:type
# From https://github.com/rhysd/actionlint/blob/main/docs/usage.md#use-actionlint-on-github-actions
- name: Check workflow files
if: runner.os == 'Linux'
run: |
bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
./actionlint -color -shellcheck=""
ci-ok:
name: CI OK
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest
needs: [test, check, lint]
env:
FAILURE: ${{ contains(join(needs.*.result, ','), 'failure') }}
steps:
- name: Check for failure
run: |
echo $FAILURE
if [ "$FAILURE" = "false" ]; then
exit 0
else
exit 1
fi

94
.github/workflows/codeql.yml vendored Normal file
View File

@@ -0,0 +1,94 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: 'CodeQL'
on:
push:
branches: ['main']
pull_request:
branches: ['main']
schedule:
- cron: '35 0 * * 0'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
if: github.repository == 'vbenjs/vue-vben-admin'
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: javascript-typescript
build-mode: none
# CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# 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:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: '/language:${{matrix.language}}'

172
.github/workflows/deploy.yml vendored Normal file
View File

@@ -0,0 +1,172 @@
name: Deploy Website on push
on:
push:
branches:
- main
jobs:
deploy-playground-ftp:
name: Deploy Push Playground Ftp
if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]') && github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Sed Config Base
shell: bash
run: |
sed -i "s#VITE_COMPRESS\s*=.*#VITE_COMPRESS = gzip#g" ./playground/.env.production
sed -i "s#VITE_PWA\s*=.*#VITE_PWA = true#g" ./playground/.env.production
cat ./playground/.env.production
- name: Setup Node
uses: ./.github/actions/setup-node
- name: Build
run: pnpm build:play
- name: Sync Playground files
uses: SamKirkland/FTP-Deploy-Action@v4.3.6
with:
server: ${{ secrets.PRO_FTP_HOST }}
username: ${{ secrets.WEB_PLAYGROUND_FTP_ACCOUNT }}
password: ${{ secrets.WEB_PLAYGROUND_FTP_PWSSWORD }}
local-dir: ./playground/dist/
deploy-docs-ftp:
name: Deploy Push Docs Ftp
if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]') && github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node
uses: ./.github/actions/setup-node
- name: Build
run: pnpm build:docs
- name: Sync Docs files
uses: SamKirkland/FTP-Deploy-Action@v4.3.6
with:
server: ${{ secrets.PRO_FTP_HOST }}
username: ${{ secrets.WEBSITE_FTP_ACCOUNT }}
password: ${{ secrets.WEBSITE_FTP_PASSWORD }}
local-dir: ./docs/.vitepress/dist/
deploy-antd-ftp:
name: Deploy Push Antd Ftp
if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]') && github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Sed Config Base
shell: bash
run: |
sed -i "s#VITE_COMPRESS\s*=.*#VITE_COMPRESS = gzip#g" ./apps/web-antd/.env.production
sed -i "s#VITE_PWA\s*=.*#VITE_PWA = true#g" ./apps/web-antd/.env.production
cat ./apps/web-antd/.env.production
- name: Setup Node
uses: ./.github/actions/setup-node
- name: Build
run: pnpm run build:antd
- name: Sync files
uses: SamKirkland/FTP-Deploy-Action@v4.3.6
with:
server: ${{ secrets.PRO_FTP_HOST }}
username: ${{ secrets.WEB_ANTD_FTP_ACCOUNT }}
password: ${{ secrets.WEB_ANTD_FTP_PASSWORD }}
local-dir: ./apps/web-antd/dist/
deploy-ele-ftp:
name: Deploy Push Element Ftp
if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]') && github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Sed Config Base
shell: bash
run: |
sed -i "s#VITE_COMPRESS\s*=.*#VITE_COMPRESS = gzip#g" ./apps/web-ele/.env.production
sed -i "s#VITE_PWA\s*=.*#VITE_PWA = true#g" ./apps/web-ele/.env.production
cat ./apps/web-ele/.env.production
- name: Setup Node
uses: ./.github/actions/setup-node
- name: Build
run: pnpm run build:ele
- name: Sync files
uses: SamKirkland/FTP-Deploy-Action@v4.3.6
with:
server: ${{ secrets.PRO_FTP_HOST }}
username: ${{ secrets.WEB_ELE_FTP_ACCOUNT }}
password: ${{ secrets.WEB_ELE_FTP_PASSWORD }}
local-dir: ./apps/web-ele/dist/
deploy-naive-ftp:
name: Deploy Push Naive Ftp
if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]') && github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Sed Config Base
shell: bash
run: |
sed -i "s#VITE_COMPRESS\s*=.*#VITE_COMPRESS = gzip#g" ./apps/web-naive/.env.production
sed -i "s#VITE_PWA\s*=.*#VITE_PWA = true#g" ./apps/web-naive/.env.production
cat ./apps/web-naive/.env.production
- name: Setup Node
uses: ./.github/actions/setup-node
- name: Build
run: pnpm run build:naive
- name: Sync files
uses: SamKirkland/FTP-Deploy-Action@v4.3.6
with:
server: ${{ secrets.PRO_FTP_HOST }}
username: ${{ secrets.WEB_NAIVE_FTP_ACCOUNT }}
password: ${{ secrets.WEB_NAIVE_FTP_PASSWORD }}
local-dir: ./apps/web-naive/dist/
rerun-on-failure:
name: Rerun on failure
needs:
- deploy-playground-ftp
- deploy-docs-ftp
- deploy-antd-ftp
- deploy-ele-ftp
- deploy-naive-ftp
if: failure() && fromJSON(github.run_attempt) < 10
runs-on: ubuntu-latest
steps:
- name: Retry ${{ fromJSON(github.run_attempt) }} of 10
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
run: gh workflow run rerun.yml -F run_id=${{ github.run_id }}

25
.github/workflows/draft.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: Release Drafter
on:
push:
branches:
- main
permissions:
contents: read
pull-requests: write
jobs:
update_release_draft:
permissions:
# write permission is required to create a github release
contents: write
# write permission is required for autolabeler
# otherwise, read permission is required at least
pull-requests: write
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -0,0 +1,31 @@
# 每天零点运行一次,它会检查所有带有 "need reproduction" 标签的 Issues。如果这些 Issues 在过去的 3 天内没有任何活动,它们将会被自动关闭。这有助于保持 Issue 列表的整洁,并且提醒用户在必要时提供更多的信息。
name: Issue Close Require
# 触发条件:每天零点
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * *'
permissions:
pull-requests: write
contents: write
issues: write
jobs:
close-issues:
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest
steps:
# 关闭未活动的 Issues
- name: Close Inactive Issues
uses: actions/stale@v9
with:
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.
only-labels: needs-reproduction # Only process these issues
days-before-issue-close: 3
ignore-updates: true
remove-stale-when-updated: false
close-issue-message: This issue was closed because it was open for 3 days without a valid reproduction.
close-issue-label: closed-by-action

46
.github/workflows/issue-labeled.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: Label Based Actions
on:
issues:
types: [labeled]
# pull_request:
# types: [labeled]
permissions:
issues: write
pull-requests: write
contents: write
jobs:
reply-labeled:
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest
steps:
- name: remove enhancement pending
if: github.event.label.name == 'enhancement'
uses: actions-cool/issues-helper@v3
with:
actions: 'remove-labels'
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }}
labels: 'enhancement: pending triage'
- name: remove bug pending
if: github.event.label.name == 'bug'
uses: actions-cool/issues-helper@v3
with:
actions: 'remove-labels'
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }}
labels: 'bug: pending triage'
- name: needs reproduction
if: github.event.label.name == 'needs reproduction'
uses: actions-cool/issues-helper@v3
with:
actions: 'create-comment, remove-labels'
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }}
body: |
Hello @${{ github.event.issue.user.login }}. Please provide the complete reproduction steps and code. Issues labeled by `needs reproduction` will be closed if no activities in 3 days.
labels: 'bug: pending triage'

24
.github/workflows/lock.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Lock Threads
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
jobs:
action:
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v5
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
issue-inactive-days: '14'
issue-lock-reason: ''
pr-inactive-days: '30'
pr-lock-reason: ''
process-only: 'issues, prs'

80
.github/workflows/release-tag.yml vendored Normal file
View File

@@ -0,0 +1,80 @@
name: Create Release Tag
on:
push:
tags:
- 'v*.*.*' # Push events to matching v*, i.e. v1.0, v20.15.10
env:
HUSKY: '0'
permissions:
pull-requests: write
contents: write
jobs:
build:
name: Create Release
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20]
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
# - name: Checkout code
# 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
run: |
tag=${GITHUB_REF/refs\/tags\//}
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
with:
version: ${{ steps.version.outputs.version }}
publish: true
env:
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.

19
.github/workflows/rerun.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
name: Rerun workflow
on:
workflow_dispatch:
inputs:
run_id:
description: The workflow id to relanch
required: true
jobs:
rerun:
runs-on: ubuntu-latest
steps:
- name: rerun ${{ inputs.run_id }}
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
run: |
gh run watch ${{ inputs.run_id }} > /dev/null 2>&1
gh run rerun ${{ inputs.run_id }} --failed

View File

@@ -0,0 +1,41 @@
name: Semantic Pull Request
on:
pull_request_target:
types:
- opened
- edited
- synchronize
jobs:
main:
name: Semantic Pull Request
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest
steps:
- name: Validate PR title
uses: amannn/action-semantic-pull-request@v5
with:
wip: true
subjectPattern: ^(?![A-Z]).+$
subjectPatternError: |
The subject "{subject}" found in the pull request title "{title}"
didn't match the configured pattern. Please ensure that the subject
doesn't start with an uppercase character.
requireScope: false
types: |
fix
feat
docs
style
refactor
perf
test
build
ci
chore
revert
types
release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

19
.github/workflows/stale.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
name: 'Close stale issues'
on:
schedule:
- cron: '0 1 * * *'
jobs:
stale:
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
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-pr-message: 'This PR is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days'
exempt-issue-labels: 'bug,enhancement'
days-before-stale: 60
days-before-close: 7

52
.gitignore vendored Normal file
View File

@@ -0,0 +1,52 @@
node_modules
.DS_Store
dist
dist-ssr
dist.zip
dist.tar
dist.war
.nitro
.output
*-dist.zip
*-dist.tar
*-dist.war
coverage
*.local
**/.vitepress/cache
.cache
.turbo
.temp
dev-dist
.stylelintcache
yarn.lock
package-lock.json
.VSCodeCounter
**/backend-mock/data
# local env files
.env.local
.env.*.local
.eslintcache
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
vite.config.mts.*
vite.config.mjs.*
vite.config.js.*
vite.config.ts.*
# Editor directories and files
.idea
# .vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.history
.cursor

6
.gitpod.yml Normal file
View File

@@ -0,0 +1,6 @@
ports:
- port: 5555
onOpen: open-preview
tasks:
- init: npm i -g corepack && pnpm install
command: pnpm run dev:play

1
.node-version Normal file
View File

@@ -0,0 +1 @@
22.1.0

13
.npmrc Normal file
View File

@@ -0,0 +1,13 @@
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

18
.prettierignore Normal file
View File

@@ -0,0 +1,18 @@
dist
dev-dist
.local
.output.js
node_modules
.nvmrc
coverage
CODEOWNERS
.nitro
.output
**/*.svg
**/*.sh
public
.npmrc
*-lock.yaml

1
.prettierrc.mjs Normal file
View File

@@ -0,0 +1 @@
export { default } from '@vben/prettier-config';

4
.stylelintignore Normal file
View File

@@ -0,0 +1,4 @@
dist
public
__tests__
coverage

30
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,30 @@
{
"recommendations": [
// Vue 3 的语言支持
"Vue.volar",
// 将 ESLint JavaScript 集成到 VS Code 中。
"dbaeumer.vscode-eslint",
// Visual Studio Code 的官方 Stylelint 扩展
"stylelint.vscode-stylelint",
// 使用 Prettier 的代码格式化程序
"esbenp.prettier-vscode",
// 支持 dotenv 文件语法
"mikestead.dotenv",
// 源代码的拼写检查器
"streetsidesoftware.code-spell-checker",
// Tailwind CSS 的官方 VS Code 插件
"bradlc.vscode-tailwindcss",
// iconify 图标插件
"antfu.iconify",
// i18n 插件
"Lokalise.i18n-ally",
// CSS 变量提示
"vunguyentuan.vscode-css-variables",
// 在 package.json 中显示 PNPM catalog 的版本
"antfu.pnpm-catalog-lens"
],
"unwantedRecommendations": [
// 和 volar 冲突
"octref.vetur"
]
}

37
.vscode/global.code-snippets vendored Normal file
View File

@@ -0,0 +1,37 @@
{
"import": {
"scope": "javascript,typescript",
"prefix": "im",
"body": ["import { $2 } from '$1';"],
"description": "Import a module",
},
"export-all": {
"scope": "javascript,typescript",
"prefix": "ex",
"body": ["export * from '$1';"],
"description": "Export a module",
},
"vue-script-setup": {
"scope": "vue",
"prefix": "<sc",
"body": [
"<script setup lang=\"ts\">",
"const props = defineProps<{",
" modelValue?: boolean,",
"}>()",
"$1",
"</script>",
"",
"<template>",
" <div>",
" <slot/>",
" </div>",
"</template>",
],
},
"vue-computed": {
"scope": "javascript,typescript,vue",
"prefix": "com",
"body": ["computed(() => { $1 })"],
},
}

42
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,42 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"name": "vben admin playground dev",
"request": "launch",
"url": "http://localhost:5555",
"env": { "NODE_ENV": "development" },
"sourceMaps": true,
"webRoot": "${workspaceFolder}/playground"
},
{
"type": "chrome",
"name": "vben admin antd dev",
"request": "launch",
"url": "http://localhost:5666",
"env": { "NODE_ENV": "development" },
"sourceMaps": true,
"webRoot": "${workspaceFolder}/apps/web-antd"
},
{
"type": "chrome",
"name": "vben admin ele dev",
"request": "launch",
"url": "http://localhost:5777",
"env": { "NODE_ENV": "development" },
"sourceMaps": true,
"webRoot": "${workspaceFolder}/apps/web-ele"
},
{
"type": "chrome",
"name": "vben admin naive dev",
"request": "launch",
"url": "http://localhost:5888",
"env": { "NODE_ENV": "development" },
"sourceMaps": true,
"webRoot": "${workspaceFolder}/apps/web-naive"
}
]
}

241
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,241 @@
{
"tailwindCSS.experimental.configFile": "internal/tailwind-config/src/index.ts",
// workbench
"workbench.list.smoothScrolling": true,
"workbench.startupEditor": "newUntitledFile",
"workbench.tree.indent": 10,
"workbench.editor.highlightModifiedTabs": true,
"workbench.editor.closeOnFileDelete": true,
"workbench.editor.limit.enabled": true,
"workbench.editor.limit.perEditorGroup": true,
"workbench.editor.limit.value": 5,
// editor
"editor.tabSize": 2,
"editor.detectIndentation": false,
"editor.cursorBlinking": "expand",
"editor.largeFileOptimizations": true,
"editor.accessibilitySupport": "off",
"editor.cursorSmoothCaretAnimation": "on",
"editor.guides.bracketPairs": "active",
"editor.inlineSuggest.enabled": true,
"editor.suggestSelection": "recentlyUsedByPrefix",
"editor.acceptSuggestionOnEnter": "smart",
"editor.suggest.snippetsPreventQuickSuggestions": false,
"editor.stickyScroll.enabled": true,
"editor.hover.sticky": true,
"editor.suggest.insertMode": "replace",
"editor.bracketPairColorization.enabled": true,
"editor.autoClosingBrackets": "beforeWhitespace",
"editor.autoClosingDelete": "always",
"editor.autoClosingOvertype": "always",
"editor.autoClosingQuotes": "beforeWhitespace",
"editor.wordSeparators": "`~!@#%^&*()=+[{]}\\|;:'\",.<>/?",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.fixAll.stylelint": "explicit",
"source.organizeImports": "never"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
// extensions
"extensions.ignoreRecommendations": true,
// terminal
"terminal.integrated.cursorBlinking": true,
"terminal.integrated.persistentSessionReviveProcess": "never",
"terminal.integrated.tabs.enabled": true,
"terminal.integrated.scrollback": 10000,
"terminal.integrated.stickyScroll.enabled": true,
// files
"files.eol": "\n",
"files.insertFinalNewline": true,
"files.simpleDialog.enable": true,
"files.associations": {
"*.ejs": "html",
"*.art": "html",
"**/tsconfig.json": "jsonc",
"*.json": "jsonc",
"package.json": "json"
},
"files.exclude": {
"**/.eslintcache": true,
"**/bower_components": true,
"**/.turbo": true,
"**/.idea": true,
"**/.vitepress": true,
"**/tmp": true,
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/CVS": true,
"**/.stylelintcache": true,
"**/.DS_Store": true,
"**/vite.config.mts.*": true,
"**/tea.yaml": true
},
"files.watcherExclude": {
"**/.git/objects/**": true,
"**/.git/subtree-cache/**": true,
"**/.vscode/**": true,
"**/node_modules/**": true,
"**/tmp/**": true,
"**/bower_components/**": true,
"**/dist/**": true,
"**/yarn.lock": true
},
"typescript.tsserver.exclude": ["**/node_modules", "**/dist", "**/.turbo"],
// search
"search.searchEditor.singleClickBehaviour": "peekDefinition",
"search.followSymlinks": false,
// 在使用搜索功能时,将这些文件夹/文件排除在外
"search.exclude": {
"**/node_modules": true,
"**/*.log": true,
"**/*.log*": true,
"**/bower_components": true,
"**/dist": true,
"**/elehukouben": true,
"**/.git": true,
"**/.github": true,
"**/.gitignore": true,
"**/.svn": true,
"**/.DS_Store": true,
"**/.vitepress/cache": true,
"**/.idea": true,
"**/.vscode": false,
"**/.yarn": true,
"**/tmp": true,
"*.xml": true,
"out": true,
"dist": true,
"node_modules": true,
"CHANGELOG.md": true,
"**/pnpm-lock.yaml": true,
"**/yarn.lock": true
},
"debug.onTaskErrors": "debugAnyway",
"diffEditor.ignoreTrimWhitespace": false,
"npm.packageManager": "pnpm",
"css.validate": false,
"less.validate": false,
"scss.validate": false,
// extension
"emmet.showSuggestionsAsSnippets": true,
"emmet.triggerExpansionOnTab": false,
"errorLens.enabledDiagnosticLevels": ["warning", "error"],
"errorLens.excludeBySource": ["cSpell", "Grammarly", "eslint"],
"stylelint.enable": true,
"stylelint.packageManager": "pnpm",
"stylelint.validate": ["css", "less", "postcss", "scss", "vue"],
"stylelint.customSyntax": "postcss-html",
"stylelint.snippet": ["css", "less", "postcss", "scss", "vue"],
"typescript.inlayHints.enumMemberValues.enabled": true,
"typescript.preferences.preferTypeOnlyAutoImports": true,
"typescript.preferences.includePackageJsonAutoImports": "on",
"eslint.validate": [
"javascript",
"typescript",
"javascriptreact",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"json5"
],
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
],
"github.copilot.enable": {
"*": true,
"markdown": true,
"plaintext": false,
"yaml": false
},
"cssVariables.lookupFiles": ["packages/core/base/design/src/**/*.css"],
"i18n-ally.localesPaths": [
"packages/locales/src/langs",
"playground/src/locales/langs",
"apps/*/src/locales/langs"
],
"i18n-ally.pathMatcher": "{locale}/{namespace}.{ext}",
"i18n-ally.enabledParsers": ["json"],
"i18n-ally.sourceLanguage": "en",
"i18n-ally.displayLanguage": "zh-CN",
"i18n-ally.enabledFrameworks": ["vue", "react"],
"i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": true,
"i18n-ally.namespace": true,
// 控制相关文件嵌套展示
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.expand": false,
"explorer.fileNesting.patterns": {
"*.ts": "$(capture).test.ts, $(capture).test.tsx, $(capture).spec.ts, $(capture).spec.tsx, $(capture).d.ts",
"*.tsx": "$(capture).test.ts, $(capture).test.tsx, $(capture).spec.ts, $(capture).spec.tsx,$(capture).d.ts",
"*.env": "$(capture).env.*",
"README.md": "README*,CHANGELOG*,LICENSE,CNAME",
"package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,.gitattributes,.gitignore,.gitpod.yml,.npmrc,.browserslistrc,.node-version,.git*,.tazerc.json",
"eslint.config.mjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json,lefthook.yml",
"tailwind.config.mjs": "postcss.*"
},
"commentTranslate.hover.enabled": false,
"commentTranslate.multiLineMerge": 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"
]
}

489
API.md Normal file
View File

@@ -0,0 +1,489 @@
# 后台管理API文档
## 基础说明
- **基础URL**: `/api/admin`
- **认证方式**: Session与现有后台保持一致
- **数据格式**: JSON
- **字符编码**: UTF-8
## 响应格式
### 成功响应
```json
{
"code": 200,
"message": "操作成功",
"data": {}
}
```
### 错误响应
```json
{
"code": 400,
"message": "错误信息",
"data": null
}
```
## 接口列表
### 1. 登录相关
#### 1.1 管理员登录
- **URL**: `POST /api/admin/login`
- **说明**: 管理员登录
- **参数**:
- `username` (string, 必填): 用户名
- `password` (string, 必填): 密码
- **返回**: 管理员信息
#### 1.2 获取当前管理员信息
- **URL**: `GET /api/admin/login/info`
- **说明**: 获取当前登录的管理员信息
- **需要登录**: 是
#### 1.3 退出登录
- **URL**: `POST /api/admin/login/logout`
- **说明**: 退出登录
- **需要登录**: 是
### 2. 首页相关
#### 2.1 获取系统信息
- **URL**: `GET /api/admin/index/systemInfo`
- **说明**: 获取系统信息PHP版本、MySQL版本、磁盘空间等
- **需要登录**: 是
### 3. 个人信息
#### 3.1 获取个人信息
- **URL**: `GET /api/admin/profile/info`
- **说明**: 获取当前管理员个人信息
- **需要登录**: 是
#### 3.2 更新个人信息
- **URL**: `POST /api/admin/profile/update`
- **说明**: 更新个人信息
- **需要登录**: 是
- **参数**:
- `nickname` (string): 昵称
- `email` (string): 邮箱
- `avatar` (file): 头像文件(可选)
#### 3.3 修改密码
- **URL**: `POST /api/admin/profile/updatePassword`
- **说明**: 修改密码
- **需要登录**: 是
- **参数**:
- `old_password` (string, 必填): 当前密码
- `new_password` (string, 必填): 新密码
- `confirm_password` (string, 必填): 确认新密码
### 4. 教室管理
#### 4.1 获取教室列表
- **URL**: `GET /api/admin/classroom/list`
- **说明**: 获取教室列表(支持分页和筛选)
- **需要登录**: 是
- **参数**:
- `page` (int): 页码默认1
- `limit` (int): 每页数量默认10
- `name` (string): 教室名称(模糊搜索)
- `building` (string): 楼栋
- `type` (string): 教室类型normal/multimedia/lab
- `school_id` (int): 分校ID仅超级管理员可用
#### 4.2 获取教室详情
- **URL**: `GET /api/admin/classroom/detail`
- **说明**: 获取教室详细信息
- **需要登录**: 是
- **参数**:
- `id` (int, 必填): 教室ID
#### 4.3 保存教室(新增/编辑)
- **URL**: `POST /api/admin/classroom/save`
- **说明**: 新增或编辑教室
- **需要登录**: 是
- **参数**: 教室信息包含id则为编辑不包含则为新增
#### 4.4 删除教室
- **URL**: `POST /api/admin/classroom/delete`
- **说明**: 删除教室
- **需要登录**: 是
- **参数**:
- `id` (int, 必填): 教室ID
#### 4.5 批量删除教室
- **URL**: `POST /api/admin/classroom/batchDelete`
- **说明**: 批量删除教室
- **需要登录**: 是
- **参数**:
- `ids` (array, 必填): 教室ID数组
#### 4.6 保存座位布局
- **URL**: `POST /api/admin/classroom/saveLayout`
- **说明**: 保存教室座位布局
- **需要登录**: 是
- **参数**:
- `id` (int, 必填): 教室ID
- `layout` (json, 必填): 布局数据
#### 4.7 获取教室座位状态
- **URL**: `GET /api/admin/classroom/status`
- **说明**: 获取教室座位状态(包含预订信息)
- **需要登录**: 是
- **参数**:
- `id` (int, 必填): 教室ID
#### 4.8 分配座位
- **URL**: `POST /api/admin/classroom/assignSeat`
- **说明**: 为学生分配座位
- **需要登录**: 是
- **参数**:
- `classroomId` (int, 必填): 教室ID
- `studentId` (int, 必填): 学生ID
- `row` (int, 必填): 行号
- `col` (int, 必填): 列号
- `number` (string, 必填): 座位号
#### 4.9 随机分配座位
- **URL**: `POST /api/admin/classroom/randomAssignSeats`
- **说明**: 为未分配座位的学生随机分配座位
- **需要登录**: 是
- **参数**:
- `classroomId` (int, 必填): 教室ID
#### 4.10 获取未分配座位的学生列表
- **URL**: `GET /api/admin/classroom/getUnassignedStudents`
- **说明**: 获取未分配座位的学生列表
- **需要登录**: 是
- **参数**:
- `classroomId` (int, 必填): 教室ID
#### 4.11 取消选座
- **URL**: `POST /api/admin/classroom/cancelBooking`
- **说明**: 取消选座
- **需要登录**: 是
- **参数**:
- `id` (int, 必填): 预订ID
#### 4.12 批量取消选座
- **URL**: `POST /api/admin/classroom/cancelBookingAuto`
- **说明**: 批量取消选座
- **需要登录**: 是
- **参数**:
- `ids` (array, 必填): 预订ID数组
#### 4.13 获取分校列表
- **URL**: `GET /api/admin/classroom/getSchoolList`
- **说明**: 获取分校列表(根据权限过滤)
- **需要登录**: 是
#### 4.14 上传图片
- **URL**: `POST /api/admin/classroom/upload`
- **说明**: 上传教室相关图片
- **需要登录**: 是
- **参数**:
- `file` (file, 必填): 图片文件
### 5. 预订管理
#### 5.1 获取预订列表
- **URL**: `GET /api/admin/booking/list`
- **说明**: 获取预订记录列表
- **需要登录**: 是
- **参数**:
- `page` (int): 页码
- `limit` (int): 每页数量
- `classroom_id` (int): 教室ID
- `booking_date` (string): 预订日期
- `type` (string): 类型
#### 5.2 同意座位变更申请
- **URL**: `POST /api/admin/booking/approve`
- **说明**: 同意座位变更申请
- **需要登录**: 是
- **参数**:
- `id` (int, 必填): 申请ID
#### 5.3 拒绝座位变更申请
- **URL**: `POST /api/admin/booking/reject`
- **说明**: 拒绝座位变更申请
- **需要登录**: 是
- **参数**:
- `id` (int, 必填): 申请ID
- `reason` (string, 必填): 拒绝原因
### 6. 系统设置
#### 6.1 获取系统设置
- **URL**: `GET /api/admin/setting/get`
- **说明**: 获取系统设置
- **需要登录**: 是(仅超级管理员)
#### 6.2 保存系统设置
- **URL**: `POST /api/admin/setting/save`
- **说明**: 保存系统设置
- **需要登录**: 是(仅超级管理员)
- **参数**: 设置项键值对
#### 6.3 上传图片
- **URL**: `POST /api/admin/setting/upload`
- **说明**: 上传系统设置相关图片(如轮播图)
- **需要登录**: 是(仅超级管理员)
- **参数**:
- `file` (file, 必填): 图片文件
### 7. 班级管理
#### 7.1 获取班级列表
- **URL**: `GET /api/admin/classmanager/list`
- **说明**: 获取班级列表
- **需要登录**: 是
- **参数**:
- `page` (int): 页码
- `limit` (int): 每页数量
- `name` (string): 班级名称
- `teacher_name` (string): 班主任姓名
- `school_id` (int): 分校ID
#### 7.2 获取班级详情
- **URL**: `GET /api/admin/classmanager/detail`
- **说明**: 获取班级详细信息
- **需要登录**: 是
- **参数**:
- `id` (int, 必填): 班级ID
#### 7.3 获取班主任列表
- **URL**: `GET /api/admin/classmanager/getTeachers`
- **说明**: 获取班主任列表
- **需要登录**: 是
#### 7.4 获取教室列表
- **URL**: `GET /api/admin/classmanager/getClassrooms`
- **说明**: 获取可用教室列表
- **需要登录**: 是
#### 7.5 保存班级(新增/编辑)
- **URL**: `POST /api/admin/classmanager/save`
- **说明**: 新增或编辑班级
- **需要登录**: 是
- **参数**: 班级信息
#### 7.6 删除班级
- **URL**: `POST /api/admin/classmanager/deleteClass`
- **说明**: 删除班级
- **需要登录**: 是
- **参数**:
- `id` (int, 必填): 班级ID
### 8. 学生管理
#### 8.1 获取学生列表
- **URL**: `GET /api/admin/student/list`
- **说明**: 获取学生列表
- **需要登录**: 是
- **参数**:
- `page` (int): 页码
- `limit` (int): 每页数量
- `student_id` (string): 学号
- `name` (string): 姓名
- `class_id` (int): 班级ID
- `school_id` (int): 分校ID
#### 8.2 获取学生详情
- **URL**: `GET /api/admin/student/detail`
- **说明**: 获取学生详细信息
- **需要登录**: 是
- **参数**:
- `id` (int, 必填): 学生ID
#### 8.3 保存学生(新增/编辑)
- **URL**: `POST /api/admin/student/save`
- **说明**: 新增或编辑学生
- **需要登录**: 是
- **参数**: 学生信息
#### 8.4 删除学生
- **URL**: `POST /api/admin/student/delete`
- **说明**: 删除学生
- **需要登录**: 是
- **参数**:
- `id` (int, 必填): 学生ID
#### 8.5 更新学生状态
- **URL**: `POST /api/admin/student/status`
- **说明**: 更新学生状态
- **需要登录**: 是
- **参数**:
- `id` (int, 必填): 学生ID
- `status` (int, 必填): 状态0/1
#### 8.6 解绑学生
- **URL**: `POST /api/admin/student/unbind`
- **说明**: 解绑学生与用户的关联
- **需要登录**: 是
- **参数**:
- `id` (int, 必填): 学生ID
#### 8.7 上传学生导入文件
- **URL**: `POST /api/admin/student/upload`
- **说明**: 上传学生导入Excel文件
- **需要登录**: 是
- **参数**:
- `file` (file, 必填): Excel文件
#### 8.8 导入学生数据
- **URL**: `POST /api/admin/student/import`
- **说明**: 导入学生数据
- **需要登录**: 是
- **参数**:
- `class_id` (int, 必填): 班级ID
- `file` (string, 必填): 文件路径通过upload接口获取
### 9. 用户管理(仅超级管理员)
#### 9.1 获取用户列表
- **URL**: `GET /api/admin/user/list`
- **说明**: 获取用户列表
- **需要登录**: 是(仅超级管理员)
- **参数**:
- `page` (int): 页码
- `limit` (int): 每页数量
- `keyword` (string): 关键词(昵称/手机/邮箱)
- `status` (int): 状态
#### 9.2 获取用户详情
- **URL**: `GET /api/admin/user/detail`
- **说明**: 获取用户详细信息
- **需要登录**: 是(仅超级管理员)
- **参数**:
- `id` (int, 必填): 用户ID
#### 9.3 保存用户(新增/编辑)
- **URL**: `POST /api/admin/user/save`
- **说明**: 新增或编辑用户
- **需要登录**: 是(仅超级管理员)
- **参数**: 用户信息
#### 9.4 删除用户
- **URL**: `POST /api/admin/user/delete`
- **说明**: 删除用户
- **需要登录**: 是(仅超级管理员)
- **参数**:
- `ids` (array, 必填): 用户ID数组
#### 9.5 修改用户状态
- **URL**: `POST /api/admin/user/status`
- **说明**: 修改用户状态
- **需要登录**: 是(仅超级管理员)
- **参数**:
- `id` (int, 必填): 用户ID
- `status` (int, 必填): 状态0/1
### 10. 学校管理(仅超级管理员)
#### 10.1 获取学校列表
- **URL**: `GET /api/admin/school/list`
- **说明**: 获取学校列表
- **需要登录**: 是(仅超级管理员)
- **参数**:
- `page` (int): 页码
- `limit` (int): 每页数量
- `name` (string): 学校名称
#### 10.2 获取学校详情
- **URL**: `GET /api/admin/school/detail`
- **说明**: 获取学校详细信息
- **需要登录**: 是(仅超级管理员)
- **参数**:
- `id` (int, 必填): 学校ID
#### 10.3 保存学校(新增/编辑)
- **URL**: `POST /api/admin/school/save`
- **说明**: 新增或编辑学校
- **需要登录**: 是(仅超级管理员)
- **参数**: 学校信息
#### 10.4 删除学校
- **URL**: `POST /api/admin/school/delete`
- **说明**: 删除学校
- **需要登录**: 是(仅超级管理员)
- **参数**:
- `id` (int, 必填): 学校ID
#### 10.5 获取分校账号列表
- **URL**: `GET /api/admin/school/accountList`
- **说明**: 获取分校账号列表
- **需要登录**: 是(仅超级管理员)
- **参数**:
- `school_id` (int, 必填): 学校ID
#### 10.6 保存分校账号
- **URL**: `POST /api/admin/school/saveAccount`
- **说明**: 新增或编辑分校账号
- **需要登录**: 是(仅超级管理员)
- **参数**: 账号信息
#### 10.7 删除分校账号
- **URL**: `POST /api/admin/school/deleteAccount`
- **说明**: 删除分校账号
- **需要登录**: 是(仅超级管理员)
- **参数**:
- `id` (int, 必填): 账号ID
### 11. 教师管理
#### 11.1 获取教师列表
- **URL**: `GET /api/admin/teacher/list`
- **说明**: 获取教师列表
- **需要登录**: 是
- **参数**:
- `page` (int): 页码
- `limit` (int): 每页数量
- `teacher_id` (string): 工号
- `name` (string): 姓名
- `department_id` (int): 部门ID
- `nickname` (string): 昵称
#### 11.2 获取教师详情
- **URL**: `GET /api/admin/teacher/detail`
- **说明**: 获取教师详细信息
- **需要登录**: 是
- **参数**:
- `id` (int, 必填): 教师ID
#### 11.3 保存教师(新增/编辑)
- **URL**: `POST /api/admin/teacher/save`
- **说明**: 新增或编辑教师
- **需要登录**: 是
- **参数**: 教师信息
#### 11.4 删除教师
- **URL**: `POST /api/admin/teacher/delete`
- **说明**: 删除教师
- **需要登录**: 是
- **参数**:
- `id` (int, 必填): 教师ID
### 12. 通用功能
#### 12.1 获取必应每日壁纸
- **URL**: `GET /api/admin/common/getBingWallpaper`
- **说明**: 获取必应每日桌面壁纸
- **需要登录**: 是
## 权限说明
- **超级管理员**: 可以访问所有接口,可以管理所有分校的数据
- **分校管理员**: 只能访问和操作本分校的数据,无法访问系统设置、用户管理、学校管理等接口
## 注意事项
1. 所有需要登录的接口都需要在请求时携带有效的Session
2. 分校管理员在操作数据时,系统会自动限制只能操作本分校的数据
3. 文件上传接口支持的文件类型和大小限制请参考具体接口说明
4. 分页接口默认返回格式为layui table格式code, msg, count, data

9
LICENSE Normal file
View File

@@ -0,0 +1,9 @@
MIT License
Copyright (c) 2024-present, Vben
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

157
README.ja-JP.md Normal file
View File

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

157
README.md
View File

@@ -1,2 +1,157 @@
# booking_admin <div align="center">
<a href="https://github.com/anncwb/vue-vben-admin">
<img alt="VbenAdmin Logo" width="215" src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp">
</a>
<br>
<br>
[![license](https://img.shields.io/github/license/anncwb/vue-vben-admin.svg)](LICENSE)
<h1>Vue Vben Admin</h1>
</div>
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=vbenjs_vue-vben-admin&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin) [![codeql](https://github.com/vbenjs/vue-vben-admin/actions/workflows/codeql.yml/badge.svg)](https://github.com/vbenjs/vue-vben-admin/actions/workflows/codeql.yml) [![build](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml/badge.svg)](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml) [![ci](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml/badge.svg)](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml) [![deploy](https://github.com/vbenjs/vue-vben-admin/actions/workflows/deploy.yml/badge.svg)](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.
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](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
The `Chrome 80+` browser is recommended for local development
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
[![Star History Chart](https://api.star-history.com/svg?repos=vbenjs/vue-vben-admin&type=Date)](https://star-history.com/#vbenjs/vue-vben-admin&Date)
## Donate
If you think this project is helpful to you, you can help the author buy a cup of coffee to show your support!
![donate](https://unpkg.com/@vbenjs/static-source@0.1.7/source/sponsor.png)
<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)

157
README.zh-CN.md Normal file
View File

@@ -0,0 +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](https://img.shields.io/github/license/anncwb/vue-vben-admin.svg)](LICENSE)
<h1>Vue Vben Admin</h1>
</div>
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=vbenjs_vue-vben-admin&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin) ![codeql](https://github.com/vbenjs/vue-vben-admin/actions/workflows/codeql.yml/badge.svg) ![build](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml/badge.svg) ![ci](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml/badge.svg) ![deploy](https://github.com/vbenjs/vue-vben-admin/actions/workflows/deploy.yml/badge.svg)
**中文** | [English](./README.md) | [日本語](./README.ja-JP.md)
## 简介
Vue Vben Admin 是 Vue Vben Admin 的升级版本。作为一个免费开源的中后台模板,它采用了最新的 Vue 3、Vite、TypeScript 等主流技术开发,开箱即用,可用于中后台前端开发,也适合学习参考。
## 升级提示
该版本为最新版本 `5.0`,与其他版本不兼容,如果你是新项目,建议使用最新版本。如果你想查看旧版本,请使用 [v2 分支](https://github.com/vbenjs/vue-vben-admin/tree/v2)
## 特性
- **最新技术栈**:使用 Vue3/vite 等前端前沿技术开发
- **TypeScript**:应用程序级 JavaScript 的语言
- **主题**:提供多套主题色彩,可配置自定义主题
- **国际化**:内置完善的国际化方案
- **权限**:内置完善的动态路由权限生成方案
## 预览
- [Vben Admin](https://vben.pro/) - 完整版中文站点
测试账号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>
### 使用 Gitpod
在 Gitpod适用于 GitHub 的免费在线开发环境)中打开项目,并立即开始编码。
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/vbenjs/vue-vben-admin)
## 文档
[文档地址](https://doc.vben.pro/)
## 安装使用
1. 获取项目代码
```bash
git clone https://github.com/vbenjs/vue-vben-admin.git
```
2. 安装依赖
```bash
cd vue-vben-admin
npm i -g corepack
pnpm install
```
3. 运行
```bash
pnpm dev
```
4. 打包
```bash
pnpm build
```
## 更新日志
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases)
## 如何贡献
非常欢迎你的加入![提一个 Issue](https://github.com/anncwb/vue-vben-admin/issues/new/choose) 或者提交一个 Pull Request。
**Pull Request 流程:**
1. Fork 代码
2. 创建自己的分支:`git checkout -b feature/xxxx`
3. 提交你的修改:`git commit -am 'feat(function): add xxxxx'`
4. 推送您的分支:`git push origin feature/xxxx`
5. 提交 `pull request`
## Git 贡献提交规范
参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 规范 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
- `feat` 增加新功能
- `fix` 修复问题/BUG
- `style` 代码风格相关无影响运行结果的
- `perf` 优化/性能提升
- `refactor` 重构
- `revert` 撤销修改
- `test` 测试相关
- `docs` 文档/注释
- `chore` 依赖更新/脚手架配置修改等
- `ci` 持续集成
- `types` 类型定义文件更改
## 浏览器支持
本地开发推荐使用 `Chrome 80+` 浏览器
支持现代浏览器,不支持 IE
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>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 |
## 维护者
[@Vben](https://github.com/anncwb)
## Star 历史
[![Star History Chart](https://api.star-history.com/svg?repos=vbenjs/vue-vben-admin&type=Date)](https://star-history.com/#vbenjs/vue-vben-admin&Date)
## 捐赠
如果你觉得这个项目对你有帮助,你可以帮作者买一杯咖啡表示支持!
![donate](https://unpkg.com/@vbenjs/static-source@0.1.7/source/sponsor.png)
<a style="display: block;width: 100px;height: 50px;line-height: 50px; color: #fff;text-align: center; background: #408aed;border-radius: 4px;" href="https://www.paypal.com/paypalme/cvvben">Paypal Me</a>
## 贡献者
<a href="https://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)
## 许可证
[MIT © Vben-2020](./LICENSE)

5
apps/web-antd/.env Normal file
View File

@@ -0,0 +1,5 @@
# 应用标题
VITE_APP_TITLE=教室预订管理系统
# 应用命名空间
VITE_APP_NAMESPACE=booking-admin

View File

@@ -0,0 +1,7 @@
# public path
VITE_BASE=/
# Basic interface address SPA
VITE_GLOB_API_URL=/api
VITE_VISUALIZER=true

View File

@@ -0,0 +1,18 @@
# 端口号
VITE_PORT=5555
# 资源公共路径
VITE_BASE=/
# 接口地址(开发环境使用相对路径,通过 Vite 代理)
# 注意API 定义中已经包含了 /api 前缀,所以这里设置为 /
VITE_GLOB_API_URL=/
# 是否开启 Nitro Mock服务
VITE_NITRO_MOCK=false
# 是否打开 devtools
VITE_DEVTOOLS=true
# 是否注入全局loading
VITE_INJECT_APP_LOADING=true

View File

@@ -0,0 +1,8 @@
# 生产环境配置
VITE_BASE=/super/
VITE_GLOB_API_URL=https://xuanzuo.dhdjy.com
VITE_ROUTER_HISTORY=history
VITE_COMPRESS=gzip
VITE_PWA=false
VITE_INJECT_APP_LOADING=true
VITE_ARCHIVER=true

35
apps/web-antd/index.html Normal file
View File

@@ -0,0 +1,35 @@
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="renderer" content="webkit" />
<meta name="description" content="A Modern Back-end Management System" />
<meta name="keywords" content="Vben Admin Vue3 Vite" />
<meta name="author" content="Vben" />
<meta
name="viewport"
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
/>
<!-- 由 vite 注入 VITE_APP_TITLE 变量,在 .env 文件内配置 -->
<title><%= VITE_APP_TITLE %></title>
<link rel="icon" href="/favicon.ico" />
<script>
// 生产环境下注入百度统计
if (window._VBEN_ADMIN_PRO_APP_CONF_) {
var _hmt = _hmt || [];
(function () {
var hm = document.createElement('script');
hm.src =
'https://hm.baidu.com/hm.js?b38e689f40558f20a9a686d7f6f33edf';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(hm, s);
})();
}
</script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,50 @@
{
"name": "@vben/web-antd",
"version": "5.5.9",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
"type": "git",
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
"directory": "apps/web-antd"
},
"license": "MIT",
"author": {
"name": "vben",
"email": "ann.vben@gmail.com",
"url": "https://github.com/anncwb"
},
"type": "module",
"scripts": {
"build": "pnpm vite build --mode production",
"build:analyze": "pnpm vite build --mode analyze",
"dev": "pnpm vite --mode development",
"preview": "vite preview",
"typecheck": "vue-tsc --noEmit --skipLibCheck"
},
"imports": {
"#/*": "./src/*"
},
"dependencies": {
"@vben/access": "workspace:*",
"@vben/common-ui": "workspace:*",
"@vben/constants": "workspace:*",
"@vben/hooks": "workspace:*",
"@vben/icons": "workspace:*",
"@vben/layouts": "workspace:*",
"@vben/locales": "workspace:*",
"@vben/plugins": "workspace:*",
"@vben/preferences": "workspace:*",
"@vben/request": "workspace:*",
"@vben/stores": "workspace:*",
"@vben/styles": "workspace:*",
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"@vueuse/core": "catalog:",
"ant-design-vue": "catalog:",
"dayjs": "catalog:",
"pinia": "catalog:",
"vue": "catalog:",
"vue-router": "catalog:"
}
}

View File

@@ -0,0 +1 @@
export { default } from '@vben/tailwind-config/postcss';

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -0,0 +1,441 @@
/**
* 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
*/
import type {
UploadChangeParam,
UploadFile,
UploadProps,
} from 'ant-design-vue';
import type { Component, Ref } from 'vue';
import type { BaseFormComponentType } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import {
defineAsyncComponent,
defineComponent,
h,
ref,
render,
unref,
watch,
} from 'vue';
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { $t } from '@vben/locales';
import { isEmpty } from '@vben/utils';
import { notification } from 'ant-design-vue';
const AutoComplete = defineAsyncComponent(
() => import('ant-design-vue/es/auto-complete'),
);
const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
const Checkbox = defineAsyncComponent(
() => import('ant-design-vue/es/checkbox'),
);
const CheckboxGroup = defineAsyncComponent(() =>
import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup),
);
const DatePicker = defineAsyncComponent(
() => import('ant-design-vue/es/date-picker'),
);
const Divider = defineAsyncComponent(() => import('ant-design-vue/es/divider'));
const Input = defineAsyncComponent(() => import('ant-design-vue/es/input'));
const InputNumber = defineAsyncComponent(
() => import('ant-design-vue/es/input-number'),
);
const InputPassword = defineAsyncComponent(() =>
import('ant-design-vue/es/input').then((res) => res.InputPassword),
);
const Mentions = defineAsyncComponent(
() => import('ant-design-vue/es/mentions'),
);
const Radio = defineAsyncComponent(() => import('ant-design-vue/es/radio'));
const RadioGroup = defineAsyncComponent(() =>
import('ant-design-vue/es/radio').then((res) => res.RadioGroup),
);
const RangePicker = defineAsyncComponent(() =>
import('ant-design-vue/es/date-picker').then((res) => res.RangePicker),
);
const Rate = defineAsyncComponent(() => import('ant-design-vue/es/rate'));
const Select = defineAsyncComponent(() => import('ant-design-vue/es/select'));
const Space = defineAsyncComponent(() => import('ant-design-vue/es/space'));
const Switch = defineAsyncComponent(() => import('ant-design-vue/es/switch'));
const Textarea = defineAsyncComponent(() =>
import('ant-design-vue/es/input').then((res) => res.Textarea),
);
const TimePicker = defineAsyncComponent(
() => import('ant-design-vue/es/time-picker'),
);
const TreeSelect = defineAsyncComponent(
() => import('ant-design-vue/es/tree-select'),
);
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>(
component: T,
type: 'input' | 'select',
componentProps: Recordable<any> = {},
) => {
return defineComponent({
name: component.name,
inheritAttrs: false,
setup: (props: any, { attrs, expose, slots }) => {
const placeholder =
props?.placeholder ||
attrs?.placeholder ||
$t(`ui.placeholder.${type}`);
// 透传组件暴露的方法
const innerRef = ref();
expose(
new Proxy(
{},
{
get: (_target, key) => innerRef.value?.[key],
has: (_target, key) => key in (innerRef.value || {}),
},
),
);
return () =>
h(
component,
{ ...componentProps, placeholder, ...props, ...attrs, ref: innerRef },
slots,
);
},
});
};
const withPreviewUpload = () => {
return defineComponent({
name: Upload.name,
emits: ['change', '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 handleChange = async (event: UploadChangeParam) => {
fileList.value = event.fileList;
emit('change', event);
emit(
'update:modelValue',
event.fileList?.length ? fileList.value : undefined,
);
};
const handlePreview = async (file: UploadFile) => {
previewVisible.value = true;
await previewImage(file, previewVisible, fileList);
};
const renderUploadButton = (): any => {
const isDisabled = attrs.disabled;
// 如果禁用,不渲染上传按钮
if (isDisabled) {
return null;
}
// 否则渲染默认上传按钮
return isEmpty(slots)
? createDefaultSlotsWithUpload(listType, placeholder)
: slots;
};
// 可以监听到表单API设置的值
watch(
() => attrs.modelValue,
(res) => {
fileList.value = res;
},
);
return () =>
h(
Upload,
{
...props,
...attrs,
fileList: fileList.value,
onChange: handleChange,
onPreview: handlePreview,
},
renderUploadButton(),
);
},
});
};
const createDefaultSlotsWithUpload = (
listType: string,
placeholder: string,
) => {
switch (listType) {
case 'picture-card': {
return {
default: () => placeholder,
};
}
default: {
return {
default: () =>
h(
Button,
{
icon: h(IconifyIcon, {
icon: 'ant-design:upload-outlined',
class: 'mb-1 size-4',
}),
},
() => placeholder,
),
};
}
}
};
const previewImage = async (
file: UploadFile,
visible: Ref<boolean>,
fileList: Ref<UploadProps['fileList']>,
) => {
// 检查是否为图片文件的辅助函数
const isImageFile = (file: UploadFile): boolean => {
const imageExtensions = new Set([
'bmp',
'gif',
'jpeg',
'jpg',
'png',
'webp',
]);
if (file.url) {
const ext = file.url?.split('.').pop()?.toLowerCase();
return ext ? imageExtensions.has(ext) : false;
}
if (!file.type) {
const ext = file.name?.split('.').pop()?.toLowerCase();
return ext ? imageExtensions.has(ext) : false;
}
return file.type.startsWith('image/');
};
// 如果当前文件不是图片,直接打开
if (!isImageFile(file)) {
if (file.url) {
window.open(file.url, '_blank');
} else if (file.preview) {
window.open(file.preview, '_blank');
} else {
console.warn('无法打开文件没有可用的URL或预览地址');
}
return;
}
// 对于图片文件,继续使用预览组
const [ImageComponent, PreviewGroupComponent] = await Promise.all([
Image,
PreviewGroup,
]);
const getBase64 = (file: File) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.addEventListener('load', () => resolve(reader.result));
reader.addEventListener('error', (error) => reject(error));
});
};
// 从fileList中过滤出所有图片文件
const imageFiles = (unref(fileList) || []).filter((element) =>
isImageFile(element),
);
// 为所有没有预览地址的图片生成预览
for (const imgFile of imageFiles) {
if (!imgFile.url && !imgFile.preview && imgFile.originFileObj) {
imgFile.preview = (await getBase64(imgFile.originFileObj)) as string;
}
}
const container: HTMLElement | null = document.createElement('div');
document.body.append(container);
// 用于追踪组件是否已卸载
let isUnmounted = false;
const PreviewWrapper = {
setup() {
return () => {
if (isUnmounted) return null;
return h(
PreviewGroupComponent,
{
class: 'hidden',
preview: {
visible: visible.value,
// 设置初始显示的图片索引
current: imageFiles.findIndex((f) => f.uid === file.uid),
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);
};
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type ComponentType =
| 'ApiSelect'
| 'ApiTreeSelect'
| 'AutoComplete'
| 'Checkbox'
| 'CheckboxGroup'
| 'DatePicker'
| 'DefaultButton'
| 'Divider'
| 'IconPicker'
| 'Input'
| 'InputNumber'
| 'InputPassword'
| 'Mentions'
| 'PrimaryButton'
| 'Radio'
| 'RadioGroup'
| 'RangePicker'
| 'Rate'
| 'Select'
| 'Space'
| 'Switch'
| 'Textarea'
| 'TimePicker'
| 'TreeSelect'
| 'Upload'
| BaseFormComponentType;
async function initComponentAdapter() {
const components: Partial<Record<ComponentType, Component>> = {
// 如果你的组件体积比较大,可以使用异步加载
// Button: () =>
// import('xxx').then((res) => res.Button),
ApiSelect: withDefaultPlaceholder(
{
...ApiComponent,
name: 'ApiSelect',
},
'select',
{
component: Select,
loadingSlot: 'suffixIcon',
visibleEvent: 'onDropdownVisibleChange',
modelPropName: 'value',
},
),
ApiTreeSelect: withDefaultPlaceholder(
{
...ApiComponent,
name: 'ApiTreeSelect',
},
'select',
{
component: TreeSelect,
fieldNames: { label: 'label', value: 'value', children: 'children' },
loadingSlot: 'suffixIcon',
modelPropName: 'value',
optionsPropName: 'treeData',
visibleEvent: 'onVisibleChange',
},
),
AutoComplete,
Checkbox,
CheckboxGroup,
DatePicker,
// 自定义默认按钮
DefaultButton: (props, { attrs, slots }) => {
return h(Button, { ...props, attrs, type: 'default' }, slots);
},
Divider,
IconPicker: withDefaultPlaceholder(IconPicker, 'select', {
iconSlot: 'addonAfter',
inputComponent: Input,
modelValueProp: 'value',
}),
Input: withDefaultPlaceholder(Input, 'input'),
InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
Mentions: withDefaultPlaceholder(Mentions, 'input'),
// 自定义主要按钮
PrimaryButton: (props, { attrs, slots }) => {
return h(Button, { ...props, attrs, type: 'primary' }, slots);
},
Radio,
RadioGroup,
RangePicker,
Rate,
Select: withDefaultPlaceholder(Select, 'select'),
Space,
Switch,
Textarea: withDefaultPlaceholder(Textarea, 'input'),
TimePicker,
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
Upload: withPreviewUpload(),
};
// 将组件注册到全局共享状态中
globalShareState.setComponents(components);
// 定义全局共享状态中的消息提示
globalShareState.defineMessage({
// 复制成功消息提示
copyPreferencesSuccess: (title, content) => {
notification.success({
description: content,
message: title,
placement: 'bottomRight',
});
},
});
}
export { initComponentAdapter };

View File

@@ -0,0 +1,49 @@
import type {
VbenFormSchema as FormSchema,
VbenFormProps,
} from '@vben/common-ui';
import type { ComponentType } from './component';
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
async function initSetupVbenForm() {
setupVbenForm<ComponentType>({
config: {
// ant design vue组件库默认都是 v-model:value
baseModelPropName: 'value',
// 一些组件是 v-model:checked 或者 v-model:fileList
modelPropNameMap: {
Checkbox: 'checked',
Radio: 'checked',
Switch: 'checked',
Upload: 'fileList',
},
},
defineRules: {
// 输入项目必填国际化适配
required: (value, _params, ctx) => {
if (value === undefined || value === null || value.length === 0) {
return $t('ui.formRules.required', [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>;
export { initSetupVbenForm, useVbenForm, z };
export type VbenFormSchema = FormSchema<ComponentType>;
export type { VbenFormProps };

View File

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

View File

@@ -0,0 +1,78 @@
import { requestClient } from '#/api/request';
export namespace BookingApi {
/** 预订列表查询参数 */
export interface ListParams {
page?: number;
limit?: number;
classroom_id?: number;
booking_date?: string;
status?: number;
student_id?: number;
type?: string;
}
/** 预订信息 */
export interface BookingInfo {
id?: number;
booking_no?: string | null;
classroom_id?: number;
seat_number?: string;
user_id?: number | null;
user_name?: string | null;
booking_date?: string;
start_time?: string;
end_time?: string;
purpose?: string | null;
status?: number;
remark?: string | null;
create_time?: string;
update_time?: string;
seat_row_index?: number;
seat_col_index?: number;
student_id?: number;
classroom_name?: string;
time_range?: string;
[key: string]: any;
}
/** 同意座位变更申请参数 */
export interface ApproveParams {
id: number;
}
/** 拒绝座位变更申请参数 */
export interface RejectParams {
id: number;
reason: string;
}
}
/**
* 获取预订列表
* GET /api/admin/booking/list
*/
export async function getBookingListApi(params?: BookingApi.ListParams) {
// 使用 responseReturn: 'body' 获取完整响应体
return requestClient.get<any>('/api/admin/booking/list', {
params,
responseReturn: 'body',
});
}
/**
* 同意座位变更申请
* POST /api/admin/booking/approve
*/
export async function approveBookingApi(data: BookingApi.ApproveParams) {
return requestClient.post('/api/admin/booking/approve', data);
}
/**
* 拒绝座位变更申请
* POST /api/admin/booking/reject
*/
export async function rejectBookingApi(data: BookingApi.RejectParams) {
return requestClient.post('/api/admin/booking/reject', data);
}

View File

@@ -0,0 +1,143 @@
import { requestClient } from '#/api/request';
export namespace ClassApi {
/** 班级列表查询参数 */
export interface ListParams {
page?: number;
limit?: number;
name?: string;
teacher_name?: string;
school_id?: number;
}
/** 班级信息 */
export interface ClassInfo {
id?: number;
name?: string;
grade?: string;
department_id?: number;
teacher_id?: number;
student_count?: number;
class_room_id?: number;
description?: string;
status?: number;
create_time?: string;
update_time?: string;
teacher?: {
id?: number;
teacher_id?: string | null;
name?: string;
gender?: string;
department_id?: number;
title?: string | null;
phone?: string;
email?: string;
avatar?: string | null;
remark?: string;
status?: number;
user_id?: number | null;
create_time?: string;
update_time?: string | null;
nickname?: string;
};
classroom?: {
id?: number;
name?: string;
location?: string;
teacher_id?: number;
type?: string;
capacity?: number;
layout?: string;
status?: number;
description?: string;
create_time?: string;
update_time?: string;
layout_cols?: number;
layout_rows?: number;
fiexd_start_time?: string;
fiexd_end_time?: string;
view_images?: string;
school_id?: number;
school?: any;
};
[key: string]: any;
}
/** 保存班级参数 */
export interface SaveParams extends ClassInfo {
id?: number;
}
/** 删除班级参数 */
export interface DeleteParams {
id: number;
}
/** 班级详情参数 */
export interface DetailParams {
id: number;
}
}
/**
* 获取班级列表
* GET /api/admin/classmanager/list
*/
export async function getClassListApi(params?: ClassApi.ListParams) {
// 使用 responseReturn: 'body' 获取完整响应体
return requestClient.get<any>('/api/admin/classmanager/list', {
params,
responseReturn: 'body',
});
}
/**
* 获取班主任列表
* GET /api/admin/classmanager/getTeachers
*/
export async function getTeachersApi() {
// 使用 responseReturn: 'body' 获取完整响应体,支持可能的 code/data 格式
return requestClient.get<any>('/api/admin/classmanager/getTeachers', {
responseReturn: 'body',
});
}
/**
* 获取教室列表
* GET /api/admin/classmanager/getClassrooms
*/
export async function getClassroomsApi() {
// 使用 responseReturn: 'body' 获取完整响应体,支持可能的 code/data 格式
return requestClient.get<any>('/api/admin/classmanager/getClassrooms', {
responseReturn: 'body',
});
}
/**
* 获取班级详情
* GET /api/admin/classmanager/detail
*/
export async function getClassDetailApi(params: ClassApi.DetailParams) {
// 使用 responseReturn: 'body' 获取完整响应体
return requestClient.get<any>('/api/admin/classmanager/detail', {
params,
responseReturn: 'body',
});
}
/**
* 保存班级(新增/编辑)
* POST /api/admin/classmanager/save
*/
export async function saveClassApi(data: ClassApi.SaveParams) {
return requestClient.post<ClassApi.ClassInfo>('/api/admin/classmanager/save', data);
}
/**
* 删除班级
* POST /api/admin/classmanager/deleteClass
*/
export async function deleteClassApi(data: ClassApi.DeleteParams) {
return requestClient.post('/api/admin/classmanager/deleteClass', data);
}

View File

@@ -0,0 +1,237 @@
import { requestClient } from '#/api/request';
export namespace ClassroomApi {
/** 教室列表查询参数 */
export interface ListParams {
page?: number;
limit?: number;
name?: string;
building?: string;
type?: 'normal' | 'multimedia' | 'lab';
school_id?: number;
}
/** 教室位置信息 */
export interface ClassroomLocation {
room?: string;
floor?: string;
building?: string;
}
/** 教室座位布局单元格 */
export interface ClassroomLayoutCell {
col: number;
row: number;
type: 'empty' | 'pillar' | 'aisle' | 'seat' | 'door';
number?: string;
status?: number;
name?: string;
}
/** 教室座位布局 */
export interface ClassroomLayout {
cols: number;
rows: number;
cells: ClassroomLayoutCell[];
}
/** 教室信息 */
export interface ClassroomInfo {
id?: number;
name?: string;
location?: ClassroomLocation;
teacher_id?: number;
type?: 'normal' | 'multimedia' | 'lab' | string;
capacity?: number;
layout?: ClassroomLayout;
school_id?: number;
[key: string]: any;
}
/** 教室详情参数 */
export interface DetailParams {
id: number;
}
/** 保存教室参数 */
export interface SaveParams extends ClassroomInfo {
id?: number;
}
/** 删除教室参数 */
export interface DeleteParams {
id: number;
}
/** 批量删除教室参数 */
export interface BatchDeleteParams {
ids: number[];
}
/** 保存座位布局参数 */
export interface SaveLayoutParams {
id: number;
layout: any;
}
/** 获取教室座位状态参数 */
export interface StatusParams {
id: number;
}
/** 分配座位参数 */
export interface AssignSeatParams {
classroomId: number;
studentId: number;
row: number;
col: number;
number: string;
}
/** 随机分配座位参数 */
export interface RandomAssignSeatsParams {
classroomId: number;
}
/** 获取未分配座位的学生列表参数 */
export interface UnassignedStudentsParams {
classroomId: number;
}
/** 取消选座参数 */
export interface CancelBookingParams {
id: number;
}
/** 批量取消选座参数 */
export interface CancelBookingAutoParams {
ids: number[];
}
}
/**
* 获取教室列表
* GET /api/admin/classroom/list
*/
export async function getClassroomListApi(params?: ClassroomApi.ListParams) {
// 使用 responseReturn: 'body' 获取完整响应体,包含 code, count, data 等字段
return requestClient.get<any>('/api/admin/classroom/list', {
params,
responseReturn: 'body',
});
}
/**
* 获取教室详情
* GET /api/admin/classroom/detail
*/
export async function getClassroomDetailApi(params: ClassroomApi.DetailParams) {
// 使用 responseReturn: 'body' 获取完整响应体
return requestClient.get<any>('/api/admin/classroom/detail', {
params,
responseReturn: 'body',
});
}
/**
* 保存教室(新增/编辑)
* POST /api/admin/classroom/save
*/
export async function saveClassroomApi(data: ClassroomApi.SaveParams) {
return requestClient.post<ClassroomApi.ClassroomInfo>('/api/admin/classroom/save', data);
}
/**
* 删除教室
* POST /api/admin/classroom/delete
*/
export async function deleteClassroomApi(data: ClassroomApi.DeleteParams) {
return requestClient.post('/api/admin/classroom/delete', data);
}
/**
* 批量删除教室
* POST /api/admin/classroom/batchDelete
*/
export async function batchDeleteClassroomApi(data: ClassroomApi.BatchDeleteParams) {
return requestClient.post('/api/admin/classroom/batchDelete', data);
}
/**
* 保存座位布局
* POST /api/admin/classroom/saveLayout
*/
export async function saveClassroomLayoutApi(data: ClassroomApi.SaveLayoutParams) {
return requestClient.post('/api/admin/classroom/saveLayout', data);
}
/**
* 获取教室座位状态
* GET /api/admin/classroom/status
*/
export async function getClassroomStatusApi(params: ClassroomApi.StatusParams) {
return requestClient.get<any>('/api/admin/classroom/status', { params });
}
/**
* 分配座位
* POST /api/admin/classroom/assignSeat
*/
export async function assignSeatApi(data: ClassroomApi.AssignSeatParams) {
return requestClient.post('/api/admin/classroom/assignSeat', data);
}
/**
* 随机分配座位
* POST /api/admin/classroom/randomAssignSeats
*/
export async function randomAssignSeatsApi(data: ClassroomApi.RandomAssignSeatsParams) {
return requestClient.post('/api/admin/classroom/randomAssignSeats', data);
}
/**
* 获取未分配座位的学生列表
* GET /api/admin/classroom/getUnassignedStudents
*/
export async function getUnassignedStudentsApi(params: ClassroomApi.UnassignedStudentsParams) {
return requestClient.get<any>('/api/admin/classroom/getUnassignedStudents', { params });
}
/**
* 取消选座
* POST /api/admin/classroom/cancelBooking
*/
export async function cancelBookingApi(data: ClassroomApi.CancelBookingParams) {
return requestClient.post('/api/admin/classroom/cancelBooking', data);
}
/**
* 批量取消选座
* POST /api/admin/classroom/cancelBookingAuto
*/
export async function cancelBookingAutoApi(data: ClassroomApi.CancelBookingAutoParams) {
return requestClient.post('/api/admin/classroom/cancelBookingAuto', data);
}
/**
* 获取分校列表(用于教室管理)
* GET /api/admin/classroom/getSchoolList
*/
export async function getClassroomSchoolListApi() {
return requestClient.get<any[]>('/api/admin/classroom/getSchoolList');
}
/**
* 上传图片
* POST /api/admin/classroom/upload
*/
export async function uploadClassroomImageApi(file: File) {
const formData = new FormData();
formData.append('file', file);
return requestClient.post<string>('/api/admin/classroom/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
}

View File

@@ -0,0 +1,19 @@
import { requestClient } from '#/api/request';
export namespace CommonApi {
/** 必应壁纸信息 */
export interface BingWallpaper {
url?: string;
copyright?: string;
[key: string]: any;
}
}
/**
* 获取必应每日壁纸
* GET /api/admin/common/getBingWallpaper
*/
export async function getBingWallpaperApi() {
return requestClient.get<CommonApi.BingWallpaper>('/api/admin/common/getBingWallpaper');
}

View File

@@ -0,0 +1,31 @@
import { requestClient } from '#/api/request';
export namespace IndexApi {
/** 系统信息 */
export interface SystemInfo {
os?: string;
php?: string;
server?: string;
mysql?: string;
upload_max?: string;
max_execution_time?: string;
disk_free_space?: string;
disk_total_space?: string;
disk_usage?: string;
runtime_path?: string;
framework_version?: string;
[key: string]: any;
}
}
/**
* 获取系统信息
* GET /api/admin/index/systemInfo
*/
export async function getSystemInfoApi() {
// 使用 responseReturn: 'body' 获取完整响应体
return requestClient.get<any>('/api/admin/index/systemInfo', {
responseReturn: 'body',
});
}

View File

@@ -0,0 +1,10 @@
export * from './classroom';
export * from './booking';
export * from './student';
export * from './class';
export * from './teacher';
export * from './school';
export * from './setting';
export * from './profile';
export * from './common';
export * from './index-page';

View File

@@ -0,0 +1,75 @@
import { requestClient } from '#/api/request';
export namespace ProfileApi {
/** 个人信息 */
export interface ProfileInfo {
id?: number;
username?: string;
nickname?: string;
email?: string | null;
avatar?: string | null;
role?: string;
school_id?: number | null;
status?: number;
[key: string]: any;
}
/** 更新个人信息参数 */
export interface UpdateProfileParams {
nickname?: string;
email?: string;
avatar?: File | string;
}
/** 修改密码参数 */
export interface UpdatePasswordParams {
old_password: string;
new_password: string;
confirm_password: string;
}
}
/**
* 获取个人信息
* GET /api/admin/profile/info
*/
export async function getProfileInfoApi() {
return requestClient.get<ProfileApi.ProfileInfo>('/api/admin/profile/info');
}
/**
* 更新个人信息
* POST /api/admin/profile/update
*/
export async function updateProfileApi(data: ProfileApi.UpdateProfileParams) {
const formData = new FormData();
if (data.nickname) {
formData.append('nickname', data.nickname);
}
// email 可能是空字符串,需要明确处理
if (data.email !== undefined && data.email !== null && data.email !== '') {
formData.append('email', data.email);
} else if (data.email === '') {
// 如果传了空字符串,可能需要传空值,根据后端需求调整
formData.append('email', '');
}
if (data.avatar instanceof File) {
formData.append('avatar', data.avatar);
} else if (data.avatar) {
formData.append('avatar', data.avatar);
}
return requestClient.post<ProfileApi.ProfileInfo>('/api/admin/profile/update', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
}
/**
* 修改密码
* POST /api/admin/profile/updatePassword
*/
export async function updatePasswordApi(data: ProfileApi.UpdatePasswordParams) {
return requestClient.post('/api/admin/profile/updatePassword', data);
}

View File

@@ -0,0 +1,134 @@
import { requestClient } from '#/api/request';
export namespace SchoolApi {
/** 学校列表查询参数 */
export interface ListParams {
page?: number;
limit?: number;
name?: string;
}
/** 学校信息 */
export interface SchoolInfo {
id?: number;
name?: string;
address?: string;
description?: string;
contact?: string;
phone?: string;
create_time?: string;
update_time?: string;
[key: string]: any;
}
/** 学校详情参数 */
export interface DetailParams {
id: number;
}
/** 保存学校参数 */
export interface SaveParams extends SchoolInfo {
id?: number;
}
/** 删除学校参数 */
export interface DeleteParams {
id: number;
}
/** 获取分校账号列表参数 */
export interface AccountListParams {
school_id: number;
}
/** 分校账号信息 */
export interface AccountInfo {
id?: number;
school_id?: number;
username?: string;
password?: string;
status?: number;
create_time?: string;
update_time?: string;
[key: string]: any;
}
/** 保存分校账号参数 */
export interface SaveAccountParams extends AccountInfo {
id?: number;
}
/** 删除分校账号参数 */
export interface DeleteAccountParams {
id: number;
}
}
/**
* 获取学校列表(仅超级管理员)
* GET /api/admin/school/list
*/
export async function getSchoolListApi(params?: SchoolApi.ListParams) {
// 使用 responseReturn: 'body' 获取完整响应体
return requestClient.get<any>('/api/admin/school/list', {
params,
responseReturn: 'body',
});
}
/**
* 获取学校详情(仅超级管理员)
* GET /api/admin/school/detail
*/
export async function getSchoolDetailApi(params: SchoolApi.DetailParams) {
// 使用 responseReturn: 'body' 获取完整响应体
return requestClient.get<any>('/api/admin/school/detail', {
params,
responseReturn: 'body',
});
}
/**
* 保存学校(新增/编辑)(仅超级管理员)
* POST /api/admin/school/save
*/
export async function saveSchoolApi(data: SchoolApi.SaveParams) {
return requestClient.post<SchoolApi.SchoolInfo>('/api/admin/school/save', data);
}
/**
* 删除学校(仅超级管理员)
* POST /api/admin/school/delete
*/
export async function deleteSchoolApi(data: SchoolApi.DeleteParams) {
return requestClient.post('/api/admin/school/delete', data);
}
/**
* 获取分校账号列表(仅超级管理员)
* GET /api/admin/school/accountList
*/
export async function getSchoolAccountListApi(params: SchoolApi.AccountListParams) {
// 使用 responseReturn: 'body' 获取完整响应体
return requestClient.get<any>('/api/admin/school/accountList', {
params,
responseReturn: 'body',
});
}
/**
* 保存分校账号(仅超级管理员)
* POST /api/admin/school/saveAccount
*/
export async function saveSchoolAccountApi(data: SchoolApi.SaveAccountParams) {
return requestClient.post<SchoolApi.AccountInfo>('/api/admin/school/saveAccount', data);
}
/**
* 删除分校账号(仅超级管理员)
* POST /api/admin/school/deleteAccount
*/
export async function deleteSchoolAccountApi(data: SchoolApi.DeleteAccountParams) {
return requestClient.post('/api/admin/school/deleteAccount', data);
}

View File

@@ -0,0 +1,65 @@
import { requestClient } from '#/api/request';
export namespace SettingApi {
/** 系统设置 */
export interface SettingInfo {
site_name?: string;
site_desc?: string;
site_icp?: string;
site_banners?: string; // JSON字符串
default_avatar?: string;
upload_allowed_ext?: string;
upload_max_size?: string;
upload_path?: string;
upload_image_size?: string;
upload_image_ext?: string;
mail_host?: string;
mail_port?: string;
mail_username?: string;
mail_password?: string;
wxapp_appid?: string;
wxapp_secret?: string;
wxapp_name?: string;
wxapp_original?: string;
[key: string]: any;
}
/** 保存系统设置参数 */
export interface SaveParams extends SettingInfo {
[key: string]: any;
}
}
/**
* 获取系统设置(仅超级管理员)
* GET /api/admin/setting/get
*/
export async function getSettingApi() {
// 使用 responseReturn: 'body' 获取完整响应体
return requestClient.get<any>('/api/admin/setting/get', {
responseReturn: 'body',
});
}
/**
* 保存系统设置(仅超级管理员)
* POST /api/admin/setting/save
*/
export async function saveSettingApi(data: SettingApi.SaveParams) {
return requestClient.post('/api/admin/setting/save', data);
}
/**
* 上传图片(仅超级管理员)
* POST /api/admin/setting/upload
*/
export async function uploadSettingImageApi(file: File) {
const formData = new FormData();
formData.append('file', file);
return requestClient.post<string>('/api/admin/setting/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
}

View File

@@ -0,0 +1,154 @@
import { requestClient } from '#/api/request';
export namespace StudentApi {
/** 学生列表查询参数 */
export interface ListParams {
page?: number;
limit?: number;
student_id?: string;
name?: string;
phone?: string;
class_id?: number;
school_id?: number;
bind_status?: string;
}
/** 学生信息 */
export interface StudentInfo {
id?: number;
student_id?: string | null;
name?: string;
gender?: string;
class_id?: number;
phone?: string;
email?: string;
avatar?: string | null;
remark?: string;
status?: number;
user_id?: number | null;
create_time?: string;
update_time?: string | null;
class_name?: string;
bind_status?: string;
school_name?: string;
[key: string]: any;
}
/** 学生详情参数 */
export interface DetailParams {
id: number;
}
/** 保存学生参数 */
export interface SaveParams extends StudentInfo {
id?: number;
}
/** 删除学生参数 */
export interface DeleteParams {
id: number;
}
/** 更新学生状态参数 */
export interface StatusParams {
id: number;
status: number;
}
/** 解绑学生参数 */
export interface UnbindParams {
id: number;
}
/** 上传学生导入文件参数 */
export interface UploadParams {
file: File;
}
/** 导入学生数据参数 */
export interface ImportParams {
class_id: number;
file: string;
}
}
/**
* 获取学生列表
* GET /api/admin/student/list
*/
export async function getStudentListApi(params?: StudentApi.ListParams) {
// 使用 responseReturn: 'body' 获取完整响应体
return requestClient.get<any>('/api/admin/student/list', {
params,
responseReturn: 'body',
});
}
/**
* 获取学生详情
* GET /api/admin/student/detail
*/
export async function getStudentDetailApi(params: StudentApi.DetailParams) {
// 使用 responseReturn: 'body' 获取完整响应体
return requestClient.get<any>('/api/admin/student/detail', {
params,
responseReturn: 'body',
});
}
/**
* 保存学生(新增/编辑)
* POST /api/admin/student/save
*/
export async function saveStudentApi(data: StudentApi.SaveParams) {
return requestClient.post<StudentApi.StudentInfo>('/api/admin/student/save', data);
}
/**
* 删除学生
* POST /api/admin/student/delete
*/
export async function deleteStudentApi(data: StudentApi.DeleteParams) {
return requestClient.post('/api/admin/student/delete', data);
}
/**
* 更新学生状态
* POST /api/admin/student/status
*/
export async function updateStudentStatusApi(data: StudentApi.StatusParams) {
return requestClient.post('/api/admin/student/status', data);
}
/**
* 解绑学生
* POST /api/admin/student/unbind
*/
export async function unbindStudentApi(data: StudentApi.UnbindParams) {
return requestClient.post('/api/admin/student/unbind', data);
}
/**
* 上传学生导入文件
* POST /api/admin/student/upload
*/
export async function uploadStudentFileApi(file: File) {
const formData = new FormData();
formData.append('file', file);
// 使用 responseReturn: 'body' 获取完整响应体
return requestClient.post<any>('/api/admin/student/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
responseReturn: 'body',
});
}
/**
* 导入学生数据
* POST /api/admin/student/import
*/
export async function importStudentDataApi(data: StudentApi.ImportParams) {
return requestClient.post('/api/admin/student/import', data);
}

View File

@@ -0,0 +1,92 @@
import { requestClient } from '#/api/request';
export namespace TeacherApi {
/** 教师列表查询参数 */
export interface ListParams {
page?: number;
limit?: number;
teacher_id?: string;
name?: string;
department_id?: number;
nickname?: string;
}
/** 教师信息 */
export interface TeacherInfo {
id?: number;
teacher_id?: string | null;
name?: string;
gender?: string;
department_id?: number;
title?: string | null;
phone?: string;
email?: string;
avatar?: string | null;
remark?: string;
status?: number;
user_id?: number | null;
create_time?: string;
update_time?: string | null;
nickname?: string;
department_name?: string | null;
classroom_count?: number;
classroom_names?: string[];
[key: string]: any;
}
/** 教师详情参数 */
export interface DetailParams {
id: number;
}
/** 保存教师参数 */
export interface SaveParams extends TeacherInfo {
id?: number;
}
/** 删除教师参数 */
export interface DeleteParams {
id: number;
}
}
/**
* 获取教师列表
* GET /api/admin/teacher/list
*/
export async function getTeacherListApi(params?: TeacherApi.ListParams) {
// 使用 responseReturn: 'body' 获取完整响应体
return requestClient.get<any>('/api/admin/teacher/list', {
params,
responseReturn: 'body',
});
}
/**
* 获取教师详情
* GET /api/admin/teacher/detail
*/
export async function getTeacherDetailApi(params: TeacherApi.DetailParams) {
// 使用 responseReturn: 'body' 获取完整响应体
return requestClient.get<any>('/api/admin/teacher/detail', {
params,
responseReturn: 'body',
});
}
/**
* 保存教师(新增/编辑)
* POST /api/admin/teacher/save
*/
export async function saveTeacherApi(data: TeacherApi.SaveParams) {
return requestClient.post<TeacherApi.TeacherInfo>('/api/admin/teacher/save', data);
}
/**
* 删除教师
* POST /api/admin/teacher/delete
*/
export async function deleteTeacherApi(data: TeacherApi.DeleteParams) {
return requestClient.post('/api/admin/teacher/delete', data);
}

View File

@@ -0,0 +1,58 @@
import { baseRequestClient, requestClient } from '#/api/request';
export namespace AuthApi {
/** 登录接口参数 */
export interface LoginParams {
username: string;
password: string;
}
/** 登录接口返回值 */
export interface LoginResult {
accessToken?: string;
[key: string]: any;
}
export interface RefreshTokenResult {
data: string;
status: number;
}
}
/**
* 管理员登录
* POST /api/admin/login
*/
export async function loginApi(data: AuthApi.LoginParams) {
return requestClient.post<AuthApi.LoginResult>('/api/admin/login', data);
}
/**
* 获取当前管理员信息
* GET /api/admin/login/info
*/
export async function getAdminInfoApi() {
return requestClient.get<any>('/api/admin/login/info');
}
/**
* 退出登录
* POST /api/admin/login/logout
*/
export async function logoutApi() {
return baseRequestClient.post('/api/admin/login/logout');
}
/**
* 刷新accessToken如果后端支持
*/
export async function refreshTokenApi() {
return baseRequestClient.post<AuthApi.RefreshTokenResult>('/auth/refresh');
}
/**
* 获取用户权限码
*/
export async function getAccessCodesApi() {
return requestClient.get<string[]>('/auth/codes');
}

View File

@@ -0,0 +1,4 @@
export * from './auth';
export * from './menu';
export * from './user';
export * from '../admin';

View File

@@ -0,0 +1,17 @@
import type { RouteRecordStringComponent } from '@vben/types';
import { requestClient } from '#/api/request';
/**
* 获取用户所有菜单
* 如果后端没有此接口,返回空数组,使用静态路由
*/
export async function getAllMenusApi() {
try {
return await requestClient.get<RouteRecordStringComponent[]>('/menu/all');
} catch (error) {
console.warn('获取菜单接口不存在,使用静态路由:', error);
// 返回空数组,使用静态路由
return [];
}
}

View File

@@ -0,0 +1,11 @@
import type { UserInfo } from '@vben/types';
import { requestClient } from '#/api/request';
/**
* 获取用户信息
* GET /api/admin/login/info
*/
export async function getUserInfoApi() {
return requestClient.get<UserInfo>('/api/admin/login/info');
}

View File

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

View File

@@ -0,0 +1,129 @@
/**
* 该文件可自行根据业务逻辑进行调整
*/
import type { RequestClientOptions } from '@vben/request';
import { useAppConfig } from '@vben/hooks';
import { preferences } from '@vben/preferences';
import {
authenticateResponseInterceptor,
defaultResponseInterceptor,
errorMessageResponseInterceptor,
RequestClient,
} from '@vben/request';
import { useAccessStore } from '@vben/stores';
import { message } from 'ant-design-vue';
import { useAuthStore } from '#/store';
import { refreshTokenApi } from './core';
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
function createRequestClient(baseURL: string, options?: RequestClientOptions) {
const client = new RequestClient({
...options,
baseURL,
});
/**
* 重新认证逻辑
*/
async function doReAuthenticate() {
console.warn('Access token or refresh token is invalid or expired. ');
const accessStore = useAccessStore();
const authStore = useAuthStore();
accessStore.setAccessToken(null);
if (
preferences.app.loginExpiredMode === 'modal' &&
accessStore.isAccessChecked
) {
accessStore.setLoginExpired(true);
} else {
await authStore.logout();
}
}
/**
* 刷新token逻辑
*/
async function doRefreshToken() {
const accessStore = useAccessStore();
const resp = await refreshTokenApi();
const newToken = resp.data;
accessStore.setAccessToken(newToken);
return newToken;
}
function formatToken(token: null | string) {
return token ? `Bearer ${token}` : null;
}
// 请求头处理
// 根据 API.md使用 Session 认证,不需要 Authorization header
// 但需要设置 withCredentials: true 以支持跨域携带 Cookie
client.addRequestInterceptor({
fulfilled: async (config) => {
const accessStore = useAccessStore();
// 使用 Session 认证,通过 Cookie 传递,需要设置 withCredentials
config.withCredentials = true;
config.headers['Accept-Language'] = preferences.app.locale;
// 如果需要 token可以保留但主要依赖 Session
if (accessStore.accessToken) {
config.headers.Authorization = formatToken(accessStore.accessToken);
}
return config;
},
});
// 处理返回的响应数据格式
// 根据实际接口,成功响应 code 可能为 0 或 200可能是字符串或数字
client.addResponseInterceptor(
defaultResponseInterceptor({
codeField: 'code',
dataField: 'data',
successCode: (code: any) => {
// 处理字符串和数字两种情况
const codeNum = typeof code === 'string' ? parseInt(code, 10) : code;
return codeNum === 0 || codeNum === 200;
},
}),
);
// token过期的处理
client.addResponseInterceptor(
authenticateResponseInterceptor({
client,
doReAuthenticate,
doRefreshToken,
enableRefreshToken: preferences.app.enableRefreshToken,
formatToken,
}),
);
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
client.addResponseInterceptor(
errorMessageResponseInterceptor((msg: string, error) => {
// 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
// 当前mock接口返回的错误字段是 error 或者 message
const responseData = error?.response?.data ?? {};
const errorMessage = responseData?.error ?? responseData?.message ?? '';
// 如果没有错误信息,则会根据状态码进行提示
message.error(errorMessage || msg);
}),
);
return client;
}
export const requestClient = createRequestClient(apiURL, {
responseReturn: 'data',
withCredentials: true, // 支持 Session 认证
});
export const baseRequestClient = new RequestClient({
baseURL: apiURL,
withCredentials: true, // 支持 Session 认证
});

39
apps/web-antd/src/app.vue Normal file
View File

@@ -0,0 +1,39 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { useAntdDesignTokens } from '@vben/hooks';
import { preferences, usePreferences } from '@vben/preferences';
import { App, ConfigProvider, theme } from 'ant-design-vue';
import { antdLocale } from '#/locales';
defineOptions({ name: 'App' });
const { isDark } = usePreferences();
const { tokens } = useAntdDesignTokens();
const tokenTheme = computed(() => {
const algorithm = isDark.value
? [theme.darkAlgorithm]
: [theme.defaultAlgorithm];
// antd 紧凑模式算法
if (preferences.app.compact) {
algorithm.push(theme.compactAlgorithm);
}
return {
algorithm,
token: tokens,
};
});
</script>
<template>
<ConfigProvider :locale="antdLocale" :theme="tokenTheme">
<App>
<RouterView />
</App>
</ConfigProvider>
</template>

View File

@@ -0,0 +1,76 @@
import { createApp, watchEffect } from 'vue';
import { registerAccessDirective } from '@vben/access';
import { registerLoadingDirective } from '@vben/common-ui/es/loading';
import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores';
import '@vben/styles';
import '@vben/styles/antd';
import { useTitle } from '@vueuse/core';
import { $t, setupI18n } from '#/locales';
import { initComponentAdapter } from './adapter/component';
import { initSetupVbenForm } from './adapter/form';
import App from './app.vue';
import { router } from './router';
async function bootstrap(namespace: string) {
// 初始化组件适配器
await initComponentAdapter();
// 初始化表单组件
await initSetupVbenForm();
// // 设置弹窗的默认配置
// setDefaultModalProps({
// fullscreenButton: false,
// });
// // 设置抽屉的默认配置
// setDefaultDrawerProps({
// zIndex: 1020,
// });
const app = createApp(App);
// 注册v-loading指令
registerLoadingDirective(app, {
loading: 'loading', // 在这里可以自定义指令名称也可以明确提供false表示不注册这个指令
spinning: 'spinning',
});
// 国际化 i18n 配置
await setupI18n(app);
// 配置 pinia-tore
await initStores(app, { namespace });
// 安装权限指令
registerAccessDirective(app);
// 初始化 tippy
const { initTippy } = await import('@vben/common-ui/es/tippy');
initTippy(app);
// 配置路由及路由守卫
app.use(router);
// 配置Motion插件
const { MotionPlugin } = await import('@vben/plugins/motion');
app.use(MotionPlugin);
// 动态更新标题
watchEffect(() => {
if (preferences.app.dynamicTitle) {
const routeTitle = router.currentRoute.value.meta?.title;
const pageTitle =
(routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name;
useTitle(pageTitle);
}
});
app.mount('#app');
}
export { bootstrap };

View File

@@ -0,0 +1,904 @@
<script lang="ts" setup>
import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
import { Button, Input, Select, Space, message, Modal, Radio, Divider, Card } from 'ant-design-vue';
import type { ClassroomApi } from '#/api';
import { saveClassroomLayoutApi } from '#/api';
defineOptions({ name: 'SeatLayoutEditor' });
interface Props {
classroomId: number;
layout?: ClassroomApi.ClassroomLayout | null;
}
const props = withDefaults(defineProps<Props>(), {
layout: null,
});
const emit = defineEmits<{
saved: [layout: ClassroomApi.ClassroomLayout];
}>();
const loading = ref(false);
const cols = ref(10);
const rows = ref(8);
const selectedCellType = ref<ClassroomApi.ClassroomLayoutCell['type']>('seat');
const editingCell = ref<{ col: number; row: number } | null>(null);
const seatNumber = ref('');
const isDrawing = ref(false);
const autoNumbering = ref(true);
const seatNumberStart = ref(1);
const lastDrawnCell = ref<{ col: number; row: number } | null>(null);
const hasChangesInDrawing = ref(false); // 标记当前绘制操作是否有变化
// 初始化布局数据
const cells = ref<ClassroomApi.ClassroomLayoutCell[]>([]);
// 历史记录(用于撤销)
interface HistoryState {
cols: number;
rows: number;
cells: ClassroomApi.ClassroomLayoutCell[];
}
const history = ref<HistoryState[]>([]);
const canUndo = computed(() => history.value.length > 0);
// 保存当前状态到历史记录
const saveHistory = () => {
history.value.push({
cols: cols.value,
rows: rows.value,
cells: JSON.parse(JSON.stringify(cells.value)),
});
// 限制历史记录数量,避免内存占用过大
if (history.value.length > 50) {
history.value.shift();
}
};
// 撤销操作
const handleUndo = () => {
if (history.value.length === 0) {
message.warning('没有可撤销的操作');
return;
}
const lastState = history.value.pop()!;
cols.value = lastState.cols;
rows.value = lastState.rows;
cells.value = JSON.parse(JSON.stringify(lastState.cells));
message.success('已撤销');
};
// 初始化布局
const initLayout = () => {
cells.value = [];
for (let row = 0; row < rows.value; row++) {
for (let col = 0; col < cols.value; col++) {
cells.value.push({
col,
row,
type: 'empty',
});
}
}
};
// 从现有布局加载
const loadLayout = () => {
if (props.layout) {
cols.value = props.layout.cols || 10;
rows.value = props.layout.rows || 8;
if (props.layout.cells && props.layout.cells.length > 0) {
cells.value = JSON.parse(JSON.stringify(props.layout.cells));
} else {
initLayout();
}
} else {
initLayout();
}
};
// 监听布局变化
watch(
() => props.layout,
() => {
loadLayout();
},
{ immediate: true },
);
// 在左侧添加列
const addColumnLeft = () => {
saveHistory();
// 所有现有单元格的列号加1
cells.value.forEach((cell) => {
cell.col += 1;
});
// 添加新列列号为0
for (let row = 0; row < rows.value; row++) {
cells.value.push({
col: 0,
row,
type: 'empty',
});
}
cols.value += 1;
message.success('已在左侧添加一列');
};
// 在右侧添加列
const addColumnRight = () => {
saveHistory();
// 添加新列(列号为当前最大列号+1
for (let row = 0; row < rows.value; row++) {
cells.value.push({
col: cols.value,
row,
type: 'empty',
});
}
cols.value += 1;
message.success('已在右侧添加一列');
};
// 在上方添加行
const addRowTop = () => {
saveHistory();
// 所有现有单元格的行号加1
cells.value.forEach((cell) => {
cell.row += 1;
});
// 添加新行行号为0
for (let col = 0; col < cols.value; col++) {
cells.value.push({
col,
row: 0,
type: 'empty',
});
}
rows.value += 1;
message.success('已在上方添加一行');
};
// 在下方添加行
const addRowBottom = () => {
saveHistory();
// 添加新行(行号为当前最大行号+1
for (let col = 0; col < cols.value; col++) {
cells.value.push({
col,
row: rows.value,
type: 'empty',
});
}
rows.value += 1;
message.success('已在下方添加一行');
};
// 获取单元格
const getCell = (col: number, row: number) => {
return cells.value.find((c) => c.col === col && c.row === row);
};
// 获取下一个座位编号
const getNextSeatNumber = () => {
const seats = cells.value.filter((c) => c.type === 'seat' && c.number);
if (seats.length === 0) {
return String(seatNumberStart.value);
}
// 找到最大的座位编号
const maxNumber = Math.max(
...seats.map((c) => {
const num = parseInt(c.number || '0', 10);
return isNaN(num) ? 0 : num;
}),
);
return String(maxNumber + 1);
};
// 设置单元格类型
const setCellType = (col: number, row: number, type: ClassroomApi.ClassroomLayoutCell['type']) => {
const cell = getCell(col, row);
if (!cell) return;
const wasSeat = cell.type === 'seat';
const oldType = cell.type;
cell.type = type;
if (type !== 'seat') {
delete cell.number;
delete cell.status;
}
if (type === 'empty') {
delete cell.name;
} else if (type === 'pillar' || type === 'aisle' || type === 'door') {
cell.name = type === 'pillar' ? '柱子' : type === 'aisle' ? '过道' : '门';
} else if (type === 'seat') {
// 如果是座位类型
if (autoNumbering.value) {
// 自动编号:只有从非座位类型变为座位类型时才分配新编号
if (!wasSeat || !cell.number) {
cell.number = getNextSeatNumber();
cell.status = 1;
}
// 如果已经是座位类型,保持原有编号不变
} else if (!cell.number) {
// 手动输入模式,如果没有编号,设置为空
cell.status = 1;
}
}
};
// 处理单元格设置(用于点击和拖动)
const applyCellType = (col: number, row: number, skipSameType = false) => {
const cell = getCell(col, row);
if (!cell) return;
const oldType = cell.type;
// 如果跳过相同类型,且当前类型已经是目标类型,则不处理
if (skipSameType && cell.type === selectedCellType.value) {
// 如果是座位类型但没有编号,需要输入
if (selectedCellType.value === 'seat' && !autoNumbering.value && !cell.number) {
editingCell.value = { col, row };
seatNumber.value = '';
}
return;
}
// 记录最后绘制的单元格,避免重复绘制
if (lastDrawnCell.value && lastDrawnCell.value.col === col && lastDrawnCell.value.row === row) {
return;
}
if (selectedCellType.value === 'seat' && !autoNumbering.value && !isDrawing.value) {
// 如果是座位且不自动编号,且不是拖动模式,需要输入座位号
editingCell.value = { col, row };
seatNumber.value = cell?.number || '';
} else {
// 其他情况直接设置
setCellType(col, row, selectedCellType.value);
lastDrawnCell.value = { col, row };
// 标记有变化
if (oldType !== selectedCellType.value) {
hasChangesInDrawing.value = true;
}
}
};
// 点击单元格
const handleCellClick = (col: number, row: number) => {
if (!isDrawing.value) {
// 点击操作前保存历史记录
const cell = getCell(col, row);
if (cell && cell.type !== selectedCellType.value) {
saveHistory();
}
applyCellType(col, row, true);
}
};
// 鼠标按下开始绘制
const handleCellMouseDown = (col: number, row: number, event: MouseEvent) => {
event.preventDefault();
isDrawing.value = true;
hasChangesInDrawing.value = false; // 重置变化标记
lastDrawnCell.value = null; // 重置最后绘制的单元格
// 拖动操作开始前保存历史记录
saveHistory();
applyCellType(col, row, false);
};
// 鼠标移动绘制
const handleCellMouseEnter = (col: number, row: number) => {
if (isDrawing.value) {
applyCellType(col, row, false);
}
};
// 鼠标释放停止绘制
const handleCellMouseUp = () => {
if (isDrawing.value) {
isDrawing.value = false;
lastDrawnCell.value = null;
// 如果拖动过程中没有实际变化,撤销刚才保存的历史记录
if (!hasChangesInDrawing.value && history.value.length > 0) {
history.value.pop();
}
hasChangesInDrawing.value = false;
}
};
// 全局鼠标事件处理(确保即使鼠标移出单元格也能停止绘制)
const handleGlobalMouseUp = () => {
handleCellMouseUp();
};
// 组件挂载时添加全局事件监听
onMounted(() => {
document.addEventListener('mouseup', handleGlobalMouseUp);
document.addEventListener('mouseleave', handleGlobalMouseUp);
});
onUnmounted(() => {
document.removeEventListener('mouseup', handleGlobalMouseUp);
document.removeEventListener('mouseleave', handleGlobalMouseUp);
});
// 确认座位号
const confirmSeatNumber = () => {
if (!editingCell.value) return;
const cell = getCell(editingCell.value.col, editingCell.value.row);
if (cell) {
// 如果类型或编号发生变化,保存历史记录
if (cell.type !== 'seat' || cell.number !== seatNumber.value) {
saveHistory();
}
cell.type = 'seat';
cell.number = seatNumber.value;
cell.status = 1; // 默认可用
}
editingCell.value = null;
seatNumber.value = '';
};
// 批量填充
const handleFillAll = () => {
if (selectedCellType.value === 'empty') {
message.warning('不能批量填充为空白类型');
return;
}
Modal.confirm({
title: '确认批量填充',
content: `确定要将所有空白单元格填充为"${getTypeName(selectedCellType.value)}"吗?`,
onOk: () => {
// 如果是座位类型且自动编号,先获取当前最大编号
const emptyCells = cells.value.filter((cell) => cell.type === 'empty');
emptyCells.forEach((cell) => {
setCellType(cell.col, cell.row, selectedCellType.value);
});
message.success(`批量填充完成,共填充 ${emptyCells.length} 个单元格`);
},
});
};
// 获取类型名称
const getTypeName = (type: ClassroomApi.ClassroomLayoutCell['type']) => {
const typeMap: Record<string, string> = {
empty: '空白',
seat: '座位',
pillar: '柱子',
aisle: '过道',
door: '门',
};
return typeMap[type] || type;
};
// 统计座位数量
const seatCount = computed(() => {
return cells.value.filter((c) => c.type === 'seat').length;
});
// 取消编辑
const cancelEdit = () => {
editingCell.value = null;
seatNumber.value = '';
};
// 获取单元格样式
const getCellClass = (col: number, row: number) => {
const cell = getCell(col, row);
if (!cell) return 'cell-empty';
const classes = ['cell', `cell-${cell.type}`];
if (editingCell.value && editingCell.value.col === col && editingCell.value.row === row) {
classes.push('cell-editing');
}
return classes.join(' ');
};
// 获取单元格显示文本
const getCellText = (col: number, row: number) => {
const cell = getCell(col, row);
if (!cell) return '';
if (cell.type === 'seat' && cell.number) {
return cell.number;
}
if (cell.name) {
return cell.name;
}
return '';
};
// 保存布局
const handleSave = async () => {
if (!props.classroomId) {
message.error('教室ID不能为空');
return;
}
loading.value = true;
try {
const layout: ClassroomApi.ClassroomLayout = {
cols: cols.value,
rows: rows.value,
cells: cells.value,
};
await saveClassroomLayoutApi({
id: props.classroomId,
layout,
});
message.success('座位布局保存成功');
emit('saved', layout);
} catch (error) {
console.error('保存座位布局失败:', error);
message.error('保存座位布局失败');
} finally {
loading.value = false;
}
};
// 清空布局
const handleClear = () => {
Modal.confirm({
title: '确认清空',
content: '确定要清空所有座位布局吗?',
onOk: () => {
saveHistory(); // 保存清空前的状态,可以撤销
initLayout();
message.success('已清空布局');
},
});
};
</script>
<template>
<div class="seat-layout-editor" :class="{ drawing: isDrawing }">
<Card>
<div class="editor-toolbar">
<div class="toolbar-section">
<div class="toolbar-item">
<label class="toolbar-label">当前列数</label>
<span class="toolbar-value">{{ cols }}</span>
<Button size="small" @click="addColumnLeft">左侧添加</Button>
<Button size="small" @click="addColumnRight">右侧添加</Button>
</div>
<Divider type="vertical" />
<div class="toolbar-item">
<label class="toolbar-label">当前行数</label>
<span class="toolbar-value">{{ rows }}</span>
<Button size="small" @click="addRowTop">上方添加</Button>
<Button size="small" @click="addRowBottom">下方添加</Button>
</div>
<Divider type="vertical" />
<div class="toolbar-item">
<label class="toolbar-label">选择类型</label>
<Radio.Group v-model:value="selectedCellType" button-style="solid" size="large">
<Radio.Button value="empty">
<span class="type-icon type-empty"></span>
空白
</Radio.Button>
<Radio.Button value="seat">
<span class="type-icon type-seat"></span>
座位
</Radio.Button>
<Radio.Button value="pillar">
<span class="type-icon type-pillar"></span>
柱子
</Radio.Button>
<Radio.Button value="aisle">
<span class="type-icon type-aisle"></span>
过道
</Radio.Button>
<Radio.Button value="door">
<span class="type-icon type-door"></span>
</Radio.Button>
</Radio.Group>
</div>
</div>
<Divider />
<div class="toolbar-section">
<div class="toolbar-item">
<label class="toolbar-label">座位编号</label>
<Radio.Group v-model:value="autoNumbering">
<Radio :value="true">自动编号</Radio>
<Radio :value="false">手动输入</Radio>
</Radio.Group>
<Input
v-if="autoNumbering"
:value="String(seatNumberStart)"
type="number"
:min="1"
:style="{ width: '100px', marginLeft: '8px' }"
placeholder="起始编号"
@change="(e: any) => { const val = parseInt(e.target.value); if (!isNaN(val) && val > 0) seatNumberStart = val; }"
/>
</div>
<Divider type="vertical" />
<Space>
<Button :disabled="!canUndo" @click="handleUndo">
<template #icon>
<span></span>
</template>
撤销
</Button>
<Button @click="handleFillAll">批量填充空白</Button>
<Button danger @click="handleClear">清空布局</Button>
<Button type="primary" size="large" :loading="loading" @click="handleSave">
保存布局
</Button>
</Space>
</div>
<div class="toolbar-stats">
<span>座位总数<strong>{{ seatCount }}</strong></span>
</div>
</div>
</Card>
<Card class="layout-card">
<div class="editor-hint">
<p>
<strong>操作提示</strong>
选择类型后<strong>点击</strong>单元格即可设置类型<strong>按住鼠标左键拖动</strong>可以连续绘制多个单元格
{{ autoNumbering ? '座位将自动编号。' : '选择"座位"时需要手动输入座位号。' }}
<span v-if="isDrawing" class="drawing-indicator">正在绘制中...</span>
</p>
</div>
<div class="layout-container">
<div class="layout-header">
<div class="header-cell"></div>
<div
v-for="col in cols"
:key="`header-col-${col}`"
class="header-cell header-col"
>
{{ col }}
</div>
</div>
<div class="layout-body">
<div
v-for="row in rows"
:key="`row-${row}`"
class="layout-row"
>
<div class="header-cell header-row">{{ row }}</div>
<div
v-for="col in cols"
:key="`cell-${col}-${row}`"
:class="getCellClass(col - 1, row - 1)"
@click="handleCellClick(col - 1, row - 1)"
@mousedown="(e) => handleCellMouseDown(col - 1, row - 1, e)"
@mouseenter="handleCellMouseEnter(col - 1, row - 1)"
>
{{ getCellText(col - 1, row - 1) }}
</div>
</div>
</div>
</div>
</Card>
<Modal
:open="!!editingCell"
title="输入座位号"
@ok="confirmSeatNumber"
@cancel="cancelEdit"
>
<Input
v-model:value="seatNumber"
placeholder="请输入座位号"
@press-enter="confirmSeatNumber"
/>
</Modal>
</div>
</template>
<style scoped>
.seat-layout-editor {
padding: 0;
}
.editor-toolbar {
padding: 0;
}
.toolbar-section {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 12px;
}
.toolbar-item {
display: flex;
align-items: center;
gap: 8px;
}
.toolbar-label {
font-size: 15px;
font-weight: 600;
color: var(--ant-color-text);
white-space: nowrap;
}
.toolbar-value {
display: inline-block;
min-width: 30px;
padding: 0 8px;
font-size: 15px;
font-weight: 700;
color: var(--ant-color-primary);
text-align: center;
}
.toolbar-item :deep(.ant-radio-wrapper),
.toolbar-item :deep(.ant-radio-button-wrapper) {
color: var(--ant-color-text);
font-weight: 500;
}
.toolbar-item :deep(.ant-radio-button-wrapper-checked) {
color: var(--ant-color-text-inverse);
}
.toolbar-stats {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--ant-color-border-secondary);
font-size: 15px;
color: var(--ant-color-text-secondary);
font-weight: 500;
}
.toolbar-stats strong {
color: var(--ant-color-primary);
font-size: 18px;
font-weight: 700;
}
.type-icon {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 2px;
margin-right: 4px;
vertical-align: middle;
}
.type-empty {
background: #fff;
border: 1px solid #d9d9d9;
}
.type-seat {
background: #52c41a;
}
.type-pillar {
background: #8c8c8c;
}
.type-aisle {
background: #faad14;
}
.type-door {
background: #1890ff;
}
.layout-card {
margin-top: 16px;
}
.editor-hint {
margin-bottom: 16px;
padding: 12px 16px;
background: var(--ant-color-info-bg, #e6f7ff);
border: 1px solid var(--ant-color-info-border, #91d5ff);
border-radius: 4px;
}
.editor-hint p {
margin: 0;
font-size: 14px;
color: var(--ant-color-info, #1890ff);
line-height: 1.6;
}
.drawing-indicator {
display: inline-block;
margin-left: 12px;
padding: 2px 8px;
background: #ff4d4f;
color: #fff;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
animation: blink 1s infinite;
}
@keyframes blink {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
}
.layout-container {
overflow-x: auto;
padding: 10px;
background: var(--ant-color-fill-tertiary, #fafafa);
border-radius: 4px;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.layout-header {
display: flex;
margin-bottom: 4px;
gap: 4px;
}
.layout-body {
display: flex;
flex-direction: column;
gap: 4px;
}
.layout-row {
display: flex;
gap: 4px;
}
.header-cell {
width: 45px;
min-width: 45px;
height: 45px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 600;
color: var(--ant-color-text-secondary, #595959);
background: var(--ant-color-fill-secondary, #f5f5f5);
border: 2px solid var(--ant-color-border-secondary, #e8e8e8);
border-radius: 2px;
user-select: none;
box-sizing: border-box;
}
.header-col {
background: var(--ant-color-primary-bg, #e6f7ff);
color: var(--ant-color-primary, #1890ff);
font-weight: 700;
}
.header-row {
background: var(--ant-color-success-bg, #f6ffed);
color: var(--ant-color-success, #52c41a);
font-weight: 700;
}
.cell {
width: 45px;
min-width: 45px;
min-height: 45px;
height: 45px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border: 2px solid #d9d9d9;
border-radius: 4px;
font-size: 13px;
font-weight: 600;
transition: all 0.15s;
user-select: none;
position: relative;
line-height: 1;
box-sizing: border-box;
}
.cell:hover {
border-color: #40a9ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1);
transform: scale(1.05);
z-index: 1;
}
.seat-layout-editor.drawing .cell {
cursor: crosshair;
}
.cell-empty {
background: var(--ant-color-bg-container, #ffffff);
color: var(--ant-color-text-disabled, #bfbfbf);
border-color: var(--ant-color-border, #d9d9d9);
}
.cell-seat {
background: #52c41a;
color: #ffffff;
border-color: #389e0d;
font-weight: 700;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.cell-seat:hover {
background: #73d13d;
border-color: #52c41a;
box-shadow: 0 0 0 3px rgba(82, 196, 26, 0.2);
}
.cell-pillar {
background: #595959;
color: #ffffff;
border-color: #434343;
font-weight: 600;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.cell-pillar:hover {
background: #8c8c8c;
box-shadow: 0 0 0 3px rgba(89, 89, 89, 0.2);
}
.cell-aisle {
background: #fa8c16;
color: #ffffff;
border-color: #d46b08;
font-weight: 600;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.cell-aisle:hover {
background: #ffa940;
box-shadow: 0 0 0 3px rgba(250, 140, 22, 0.2);
}
.cell-door {
background: #1890ff;
color: #ffffff;
border-color: #096dd9;
font-weight: 600;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.cell-door:hover {
background: #40a9ff;
box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.2);
}
.cell-editing {
border-color: #ff4d4f !important;
box-shadow: 0 0 0 3px rgba(255, 77, 79, 0.2) !important;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.8;
}
}
</style>

View File

@@ -0,0 +1,126 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { Modal, Form, Input, message } from 'ant-design-vue';
import { updatePasswordApi, type ProfileApi } from '#/api/admin/profile';
const props = defineProps<{
open: boolean;
}>();
const emit = defineEmits<{
'update:open': [value: boolean];
}>();
const formRef = ref();
const loading = ref(false);
const formData = ref<ProfileApi.UpdatePasswordParams>({
old_password: '',
new_password: '',
confirm_password: '',
});
// 监听对话框打开
watch(
() => props.open,
(newVal) => {
if (!newVal) {
// 关闭时重置表单
formRef.value?.resetFields();
formData.value = {
old_password: '',
new_password: '',
confirm_password: '',
};
}
},
);
// 验证确认密码
function validateConfirmPassword(_rule: any, value: string) {
if (!value) {
return Promise.reject(new Error('请再次输入新密码'));
}
if (value !== formData.value.new_password) {
return Promise.reject(new Error('两次输入的密码不一致'));
}
return Promise.resolve();
}
// 提交表单
async function handleSubmit() {
try {
await formRef.value?.validate();
loading.value = true;
await updatePasswordApi(formData.value);
message.success('密码修改成功');
emit('update:open', false);
} catch (error: any) {
if (error?.errorFields) {
// 表单验证错误
return;
}
message.error(error?.message || '修改密码失败');
} finally {
loading.value = false;
}
}
function handleCancel() {
emit('update:open', false);
}
</script>
<template>
<Modal
:open="open"
title="修改密码"
:confirm-loading="loading"
@ok="handleSubmit"
@cancel="handleCancel"
width="500px"
>
<Form
ref="formRef"
:model="formData"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<Form.Item
label="当前密码"
name="old_password"
:rules="[{ required: true, message: '请输入当前密码' }]"
>
<Input.Password
v-model:value="formData.old_password"
placeholder="请输入当前密码"
/>
</Form.Item>
<Form.Item
label="新密码"
name="new_password"
:rules="[
{ required: true, message: '请输入新密码' },
{ min: 6, message: '密码长度至少6位' },
]"
>
<Input.Password
v-model:value="formData.new_password"
placeholder="请输入新密码"
/>
</Form.Item>
<Form.Item
label="确认密码"
name="confirm_password"
:rules="[{ validator: validateConfirmPassword }]"
>
<Input.Password
v-model:value="formData.confirm_password"
placeholder="请再次输入新密码"
/>
</Form.Item>
</Form>
</Modal>
</template>

View File

@@ -0,0 +1,259 @@
<script setup lang="ts">
import { ref, watch, computed } from 'vue';
import { Modal, Form, Input, Upload, message } from 'ant-design-vue';
import { useAppConfig } from '@vben/hooks';
import { getProfileInfoApi, updateProfileApi, type ProfileApi } from '#/api/admin/profile';
import { useUserStore } from '@vben/stores';
const props = defineProps<{
open: boolean;
}>();
const emit = defineEmits<{
'update:open': [value: boolean];
}>();
const formRef = ref();
const loading = ref(false);
const userStore = useUserStore();
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
const formData = ref<{
nickname: string;
email: string;
avatar?: string;
avatarFile?: File;
}>({
nickname: '',
email: '',
avatar: '',
});
const fileList = ref<any[]>([]);
// 获取完整的头像URL
const getAvatarUrl = (avatarPath?: string | null): string => {
if (!avatarPath) return '';
// 如果已经是完整URL直接返回
if (avatarPath.startsWith('http://') || avatarPath.startsWith('https://')) {
return avatarPath;
}
// 拼接完整的URL
// apiURL 是字符串,不是 ref所以不需要 .value
if (!apiURL) {
console.warn('apiURL is not configured');
return avatarPath;
}
const baseUrl = apiURL.replace(/\/$/, '');
const path = avatarPath.startsWith('/') ? avatarPath : `/${avatarPath}`;
return `${baseUrl}${path}`;
};
// 获取个人信息
async function loadProfileInfo() {
try {
loading.value = true;
const res = await getProfileInfoApi();
console.log('获取个人信息成功:', res);
// 检查返回的数据
if (!res) {
throw new Error('返回数据为空');
}
formData.value = {
nickname: res.nickname || '',
email: res.email || '',
avatar: res.avatar || '',
};
if (res.avatar) {
const avatarUrl = getAvatarUrl(res.avatar);
fileList.value = [
{
uid: '-1',
name: 'avatar',
status: 'done',
url: avatarUrl,
},
];
} else {
fileList.value = [];
}
} catch (error: any) {
console.error('获取个人信息失败:', error);
console.error('错误详情:', {
message: error?.message,
response: error?.response,
data: error?.response?.data,
});
const errorMessage = error?.response?.data?.message || error?.message || '获取个人信息失败';
message.error(errorMessage);
} finally {
loading.value = false;
}
}
// 监听对话框打开
watch(
() => props.open,
(newVal) => {
if (newVal) {
loadProfileInfo();
} else {
// 关闭时重置表单
formRef.value?.resetFields();
fileList.value = [];
}
},
);
// 处理头像上传前
function beforeUpload(file: File) {
const isImage = file.type.startsWith('image/');
if (!isImage) {
message.error('只能上传图片文件!');
return false;
}
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isLt2M) {
message.error('图片大小不能超过 2MB');
return false;
}
formData.value.avatarFile = file;
// 生成预览
const reader = new FileReader();
reader.onload = (e) => {
const previewUrl = e.target?.result as string;
fileList.value = [
{
uid: Date.now().toString(),
name: file.name,
status: 'done',
url: previewUrl,
originFileObj: file,
},
];
};
reader.readAsDataURL(file);
return false; // 阻止自动上传
}
// 处理头像上传变化
function handleAvatarChange(info: any) {
if (info.file.status === 'removed') {
fileList.value = [];
formData.value.avatarFile = undefined;
formData.value.avatar = '';
}
}
// 提交表单
async function handleSubmit() {
try {
await formRef.value?.validate();
loading.value = true;
const params: ProfileApi.UpdateProfileParams = {
nickname: formData.value.nickname,
email: formData.value.email || undefined, // 如果为空字符串,传 undefined
};
// 只有选择了新文件才上传
if (formData.value.avatarFile) {
params.avatar = formData.value.avatarFile;
}
const res = await updateProfileApi(params);
message.success('个人信息更新成功');
// 更新用户信息
if (userStore.userInfo) {
userStore.userInfo.nickname = formData.value.nickname;
userStore.userInfo.email = formData.value.email || null;
// 更新后重新获取数据以获取最新的头像路径
if (res?.avatar) {
userStore.userInfo.avatar = res.avatar;
// 更新文件列表中的头像URL
const avatarUrl = getAvatarUrl(res.avatar);
if (fileList.value.length > 0) {
fileList.value[0].url = avatarUrl;
}
}
}
emit('update:open', false);
} catch (error: any) {
if (error?.errorFields) {
// 表单验证错误
return;
}
message.error(error?.message || '更新个人信息失败');
} finally {
loading.value = false;
}
}
function handleCancel() {
emit('update:open', false);
}
</script>
<template>
<Modal
:open="open"
title="个人信息"
:confirm-loading="loading"
@ok="handleSubmit"
@cancel="handleCancel"
width="600px"
>
<Form
ref="formRef"
:model="formData"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<Form.Item
label="昵称"
name="nickname"
:rules="[{ required: true, message: '请输入昵称' }]"
>
<Input
v-model:value="formData.nickname"
placeholder="请输入昵称"
/>
</Form.Item>
<Form.Item
label="邮箱"
name="email"
:rules="[
{ type: 'email', message: '请输入正确的邮箱格式' },
]"
>
<Input
v-model:value="formData.email"
placeholder="请输入邮箱(可选)"
/>
</Form.Item>
<Form.Item label="头像" name="avatar">
<Upload
v-model:file-list="fileList"
list-type="picture-card"
:max-count="1"
:before-upload="beforeUpload"
@change="handleAvatarChange"
accept="image/*"
>
<div v-if="fileList.length < 1">
<div style="margin-top: 8px">
<span>上传</span>
</div>
</div>
</Upload>
</Form.Item>
</Form>
</Modal>
</template>

View File

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

View File

@@ -0,0 +1,187 @@
<script lang="ts" setup>
import type { NotificationItem } from '@vben/layouts';
import { computed, ref, watch } from 'vue';
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
import { useWatermark } from '@vben/hooks';
import {
BasicLayout,
LockScreen,
Notification,
UserDropdown,
} from '@vben/layouts';
import { preferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores';
import { useAuthStore } from '#/store';
import LoginForm from '#/views/_core/authentication/login.vue';
import ProfileInfoModal from '#/components/profile/ProfileInfoModal.vue';
import ChangePasswordModal from '#/components/profile/ChangePasswordModal.vue';
const notifications = ref<NotificationItem[]>([
{
id: 1,
avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
date: '3小时前',
isRead: true,
message: '描述信息描述信息描述信息',
title: '收到了 14 份新周报',
},
{
id: 2,
avatar: 'https://avatar.vercel.sh/1',
date: '刚刚',
isRead: false,
message: '描述信息描述信息描述信息',
title: '朱偏右 回复了你',
},
{
id: 3,
avatar: 'https://avatar.vercel.sh/1',
date: '2024-01-01',
isRead: false,
message: '描述信息描述信息描述信息',
title: '曲丽丽 评论了你',
},
{
id: 4,
avatar: 'https://avatar.vercel.sh/satori',
date: '1天前',
isRead: false,
message: '描述信息描述信息描述信息',
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 userStore = useUserStore();
const authStore = useAuthStore();
const accessStore = useAccessStore();
const { destroyWatermark, updateWatermark } = useWatermark();
const showDot = computed(() =>
notifications.value.some((item) => !item.isRead),
);
const profileInfoModalOpen = ref(false);
const changePasswordModalOpen = ref(false);
const menus = computed(() => [
{
handler: () => {
profileInfoModalOpen.value = true;
},
icon: 'lucide:user',
text: '个人信息',
},
{
handler: () => {
changePasswordModalOpen.value = true;
},
icon: 'lucide:lock',
text: '修改密码',
},
]);
const avatar = computed(() => {
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
});
async function handleLogout() {
await authStore.logout(false);
}
function handleNoticeClear() {
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() {
notifications.value.forEach((item) => (item.isRead = true));
}
watch(
() => ({
enable: preferences.app.watermark,
content: preferences.app.watermarkContent,
}),
async ({ enable, content }) => {
if (enable) {
await updateWatermark({
content:
content ||
`${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
});
} else {
destroyWatermark();
}
},
{
immediate: true,
},
);
</script>
<template>
<BasicLayout @clear-preferences-and-logout="handleLogout">
<template #user-dropdown>
<UserDropdown
:avatar
:menus
:text="userStore.userInfo?.realName"
description="ann.vben@gmail.com"
tag-text="Pro"
@logout="handleLogout"
/>
</template>
<template #notification>
<Notification
:dot="showDot"
:notifications="notifications"
@clear="handleNoticeClear"
@read="(item) => item.id && markRead(item.id)"
@remove="(item) => item.id && remove(item.id)"
@make-all="handleMakeAll"
/>
</template>
<template #extra>
<AuthenticationLoginExpiredModal
v-model:open="accessStore.loginExpired"
:avatar
>
<LoginForm />
</AuthenticationLoginExpiredModal>
</template>
<template #lock-screen>
<LockScreen :avatar @to-login="handleLogout" />
</template>
</BasicLayout>
<ProfileInfoModal v-model:open="profileInfoModalOpen" />
<ChangePasswordModal v-model:open="changePasswordModalOpen" />
</template>

View File

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

View File

@@ -0,0 +1,3 @@
# locale
每个app使用的国际化可能不同这里用于扩展国际化的功能例如扩展 dayjs、antd组件库的多语言切换以及app本身的国际化文件。

View File

@@ -0,0 +1,102 @@
import type { Locale } from 'ant-design-vue/es/locale';
import type { App } from 'vue';
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
import { ref } from 'vue';
import {
$t,
setupI18n as coreSetup,
loadLocalesMapFromDir,
} from '@vben/locales';
import { preferences } from '@vben/preferences';
import antdEnLocale from 'ant-design-vue/es/locale/en_US';
import antdDefaultLocale from 'ant-design-vue/es/locale/zh_CN';
import dayjs from 'dayjs';
const antdLocale = ref<Locale>(antdDefaultLocale);
const modules = import.meta.glob('./langs/**/*.json');
const localesMap = loadLocalesMapFromDir(
/\.\/langs\/([^/]+)\/(.*)\.json$/,
modules,
);
/**
* 加载应用特有的语言包
* 这里也可以改造为从服务端获取翻译数据
* @param lang
*/
async function loadMessages(lang: SupportedLanguagesType) {
const [appLocaleMessages] = await Promise.all([
localesMap[lang]?.(),
loadThirdPartyMessage(lang),
]);
return appLocaleMessages?.default;
}
/**
* 加载第三方组件库的语言包
* @param lang
*/
async function loadThirdPartyMessage(lang: SupportedLanguagesType) {
await Promise.all([loadAntdLocale(lang), loadDayjsLocale(lang)]);
}
/**
* 加载dayjs的语言包
* @param lang
*/
async function loadDayjsLocale(lang: SupportedLanguagesType) {
let locale;
switch (lang) {
case 'en-US': {
locale = await import('dayjs/locale/en');
break;
}
case 'zh-CN': {
locale = await import('dayjs/locale/zh-cn');
break;
}
// 默认使用英语
default: {
locale = await import('dayjs/locale/en');
}
}
if (locale) {
dayjs.locale(locale);
} else {
console.error(`Failed to load dayjs locale for ${lang}`);
}
}
/**
* 加载antd的语言包
* @param lang
*/
async function loadAntdLocale(lang: SupportedLanguagesType) {
switch (lang) {
case 'en-US': {
antdLocale.value = antdEnLocale;
break;
}
case 'zh-CN': {
antdLocale.value = antdDefaultLocale;
break;
}
}
}
async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
await coreSetup(app, {
defaultLocale: preferences.app.locale,
loadMessages,
missingWarn: !import.meta.env.PROD,
...options,
});
}
export { $t, antdLocale, setupI18n };

View File

@@ -0,0 +1,13 @@
{
"title": "Demos",
"antd": "Ant Design Vue",
"vben": {
"title": "Project",
"about": "About",
"document": "Document",
"antdv": "Ant Design Vue Version",
"naive-ui": "Naive UI Version",
"element-plus": "Element Plus Version",
"tdesign": "TDesign Vue Version"
}
}

View File

@@ -0,0 +1,15 @@
{
"auth": {
"login": "Login",
"register": "Register",
"codeLogin": "Code Login",
"qrcodeLogin": "Qr Code Login",
"forgetPassword": "Forget Password",
"profile": "Profile"
},
"dashboard": {
"title": "Dashboard",
"analytics": "Analytics",
"workspace": "Workspace"
}
}

View File

@@ -0,0 +1,13 @@
{
"title": "演示",
"antd": "Ant Design Vue",
"vben": {
"title": "项目",
"about": "关于",
"document": "文档",
"antdv": "Ant Design Vue 版本",
"naive-ui": "Naive UI 版本",
"element-plus": "Element Plus 版本",
"tdesign": "TDesign Vue 版本"
}
}

View File

@@ -0,0 +1,51 @@
{
"auth": {
"login": "登录",
"register": "注册",
"codeLogin": "验证码登录",
"qrcodeLogin": "二维码登录",
"forgetPassword": "忘记密码",
"profile": "个人中心"
},
"dashboard": {
"title": "概览",
"analytics": "系统概览"
},
"classroom": {
"title": "教室管理",
"list": "教室列表",
"detail": "教室详情"
},
"booking": {
"title": "预订管理",
"list": "预订列表"
},
"student": {
"title": "学生管理",
"list": "学生列表",
"detail": "学生详情"
},
"class": {
"title": "班级管理",
"list": "班级列表",
"detail": "班级详情"
},
"teacher": {
"title": "教师管理",
"list": "教师列表",
"detail": "教师详情"
},
"user": {
"title": "用户管理",
"list": "用户列表"
},
"school": {
"title": "学校管理",
"list": "学校列表",
"detail": "学校详情"
},
"setting": {
"title": "系统设置",
"index": "系统设置"
}
}

40
apps/web-antd/src/main.ts Normal file
View File

@@ -0,0 +1,40 @@
import { initPreferences, updatePreferences } from '@vben/preferences';
import { unmountGlobalLoading } from '@vben/utils';
import { overridesPreferences } from './preferences';
/**
* 应用初始化完成之后再进行页面加载渲染
*/
async function initApplication() {
// name用于指定项目唯一标识
// 用于区分不同项目的偏好设置以及存储数据的key前缀以及其他一些需要隔离的数据
const env = import.meta.env.PROD ? 'prod' : 'dev';
const appVersion = import.meta.env.VITE_APP_VERSION;
const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${appVersion}-${env}`;
// app偏好设置初始化
await initPreferences({
namespace,
overrides: overridesPreferences,
});
// 强制更新 widget 配置,确保时区和语言切换菜单被禁用
// 这样可以覆盖 localStorage 中可能存在的旧配置
updatePreferences({
widget: {
timezone: false,
languageToggle: false,
},
});
// 启动应用并挂载
// vue应用主要逻辑及视图
const { bootstrap } = await import('./bootstrap');
await bootstrap(namespace);
// 移除并销毁loading
unmountGlobalLoading();
}
initApplication();

View File

@@ -0,0 +1,19 @@
import { defineOverridesPreferences } from '@vben/preferences';
/**
* @description 项目配置文件
* 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
* !!! 更改配置后请清空缓存,否则可能不生效
*/
export const overridesPreferences = defineOverridesPreferences({
// overrides
app: {
name: import.meta.env.VITE_APP_TITLE,
},
widget: {
// 禁用时区菜单
timezone: false,
// 禁用语言切换菜单
languageToggle: false,
},
});

View File

@@ -0,0 +1,42 @@
import type {
ComponentRecordType,
GenerateMenuAndRoutesOptions,
} from '@vben/types';
import { generateAccessible } from '@vben/access';
import { preferences } from '@vben/preferences';
import { message } from 'ant-design-vue';
import { getAllMenusApi } from '#/api';
import { BasicLayout, IFrameView } from '#/layouts';
import { $t } from '#/locales';
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
const layoutMap: ComponentRecordType = {
BasicLayout,
IFrameView,
};
return await generateAccessible(preferences.app.accessMode, {
...options,
fetchMenuListAsync: async () => {
message.loading({
content: `${$t('common.loadingMenu')}...`,
duration: 1.5,
});
return await getAllMenusApi();
},
// 可以指定没有权限跳转403页面
forbiddenComponent,
// 如果 route.meta.menuVisibleWithForbidden = true
layoutMap,
pageMap,
});
}
export { generateAccess };

View File

@@ -0,0 +1,252 @@
import type { RouteRecordRaw, Router } from 'vue-router';
import { nextTick } from 'vue';
import { LOGIN_PATH } from '@vben/constants';
import { preferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores';
import { startProgress, stopProgress } from '@vben/utils';
import { accessRoutes, coreRouteNames } from '#/router/routes';
import { useAuthStore } from '#/store';
import { generateAccess } from './access';
/**
* 通用守卫配置
* @param router
*/
function setupCommonGuard(router: Router) {
// 记录已经加载的页面
const loadedPaths = new Set<string>();
router.beforeEach((to) => {
to.meta.loaded = loadedPaths.has(to.path);
// 页面加载进度条
if (!to.meta.loaded && preferences.transition.progress) {
startProgress();
}
return true;
});
router.afterEach((to) => {
// 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
loadedPaths.add(to.path);
// 关闭页面加载进度条
if (preferences.transition.progress) {
stopProgress();
}
});
}
/**
* 权限访问守卫配置
* @param router
*/
function setupAccessGuard(router: Router) {
router.beforeEach(async (to, from) => {
const accessStore = useAccessStore();
const userStore = useUserStore();
const authStore = useAuthStore();
// 基本路由,这些路由不需要进入权限拦截
if (coreRouteNames.includes(to.name as string)) {
if (to.path === LOGIN_PATH && accessStore.accessToken) {
const redirectPath = decodeURIComponent(
(to.query?.redirect as string) ||
userStore.userInfo?.homePath ||
'/dashboard/analytics',
);
return redirectPath;
}
return true;
}
// accessToken 检查
if (!accessStore.accessToken) {
// 明确声明忽略权限访问权限,则可以访问
if (to.meta.ignoreAccess) {
return true;
}
// 没有访问权限,跳转登录页面
if (to.fullPath !== LOGIN_PATH) {
return {
path: LOGIN_PATH,
// 如不需要,直接删除 query
query:
to.fullPath === preferences.app.defaultHomePath
? {}
: { redirect: encodeURIComponent(to.fullPath) },
// 携带当前跳转的页面,登录后重新跳转该页面
replace: true,
};
}
return to;
}
// 是否已经生成过动态路由
if (accessStore.isAccessChecked) {
// 即使已经检查过,也需要验证当前路径是否能正确解析
// 如果路径无法解析(比如直接访问 /admin/dashboard/analytics需要重新解析
try {
const resolved = router.resolve(to.path);
// 如果路由无法解析或者是 404尝试重新解析或重定向
if (!resolved.name || resolved.name === 'FallbackNotFound') {
// 如果路径包含 /admin 前缀,尝试去掉前缀
let normalizedPath = to.path;
if (normalizedPath.startsWith('/admin')) {
normalizedPath = normalizedPath.replace(/^\/admin/, '');
}
// 尝试解析去掉前缀后的路径
const normalizedResolved = router.resolve(normalizedPath);
if (normalizedResolved.name && normalizedResolved.name !== 'FallbackNotFound') {
return {
...normalizedResolved,
replace: true,
};
}
// 如果还是无法解析,尝试解析默认首页
const defaultResolved = router.resolve('/dashboard/analytics');
if (defaultResolved.name && defaultResolved.name !== 'FallbackNotFound') {
return {
...defaultResolved,
replace: true,
};
}
}
} catch (error) {
console.error('路由解析失败:', error);
}
return true;
}
// 生成路由表
// 当前登录用户拥有的角色标识列表
const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
const userRoles = userInfo?.roles ?? [];
// 生成菜单和路由
let accessibleMenus: any[] = [];
let accessibleRoutes: RouteRecordRaw[] = [];
try {
const result = await generateAccess({
roles: userRoles,
router,
// 则会在菜单中显示但是访问会被重定向到403
routes: accessRoutes,
});
accessibleMenus = result.accessibleMenus;
accessibleRoutes = result.accessibleRoutes;
} catch (error) {
console.error('生成路由失败:', error);
// 如果生成路由失败,使用静态路由并手动添加到 router
accessibleRoutes = accessRoutes;
const root = router.getRoutes().find((item) => item.path === '/');
if (root) {
const names = root?.children?.map((item) => item.name) ?? [];
accessibleRoutes.forEach((route) => {
if (root && !route.meta?.noBasicLayout) {
if (route.children && route.children.length > 0) {
delete route.component;
}
if (!names?.includes(route.name)) {
root.children?.push(route);
}
} else {
router.addRoute(route);
}
});
if (root.name) {
router.removeRoute(root.name);
}
router.addRoute(root);
}
accessibleMenus = [];
}
// 保存菜单信息和路由信息
accessStore.setAccessMenus(accessibleMenus);
accessStore.setAccessRoutes(accessibleRoutes);
accessStore.setIsAccessChecked(true);
// 等待路由添加到 router 中
await nextTick();
// 确定跳转路径
let redirectPath: string;
// 优先使用 to.query.redirect从登录页跳转过来
if (to.query?.redirect) {
redirectPath = decodeURIComponent(to.query.redirect as string);
} else if (from.query?.redirect) {
redirectPath = decodeURIComponent(from.query.redirect as string);
} else if (to.path === '/dashboard/analytics' || to.path === preferences.app.defaultHomePath || to.path === '/') {
redirectPath = userInfo?.homePath || '/dashboard/analytics';
} else {
redirectPath = to.fullPath;
}
// 尝试解析路由
try {
const resolved = router.resolve(redirectPath);
// 检查路由是否存在(有 name 或者匹配到了路由)
if (resolved.name && resolved.name !== 'FallbackNotFound') {
return {
...resolved,
replace: true,
};
}
// 如果解析的路由是404说明路由不存在尝试查找默认首页
if (resolved.name === 'FallbackNotFound') {
// 尝试解析默认首页
const defaultResolved = router.resolve('/dashboard/analytics');
if (defaultResolved.name && defaultResolved.name !== 'FallbackNotFound') {
return {
...defaultResolved,
replace: true,
};
}
}
} catch (error) {
console.error('路由解析失败:', error);
}
// 如果解析失败,尝试通过路由名称查找
// 查找 Analytics 路由dashboard/analytics 的路由名称)
const analyticsRoute = router.getRoutes().find(
(route) => route.name === 'Analytics' || route.path === '/dashboard/analytics',
);
if (analyticsRoute) {
return {
name: analyticsRoute.name || undefined,
path: analyticsRoute.path,
replace: true,
};
}
// 如果还是找不到,跳转到默认首页路径
return {
path: '/dashboard/analytics',
replace: true,
};
});
}
/**
* 项目守卫配置
* @param router
*/
function createRouterGuard(router: Router) {
/** 通用 */
setupCommonGuard(router);
/** 权限访问 */
setupAccessGuard(router);
}
export { createRouterGuard };

View File

@@ -0,0 +1,37 @@
import {
createRouter,
createWebHashHistory,
createWebHistory,
} from 'vue-router';
import { resetStaticRoutes } from '@vben/utils';
import { createRouterGuard } from './guard';
import { routes } from './routes';
/**
* @zh_CN 创建vue-router实例
*/
const router = createRouter({
history:
import.meta.env.VITE_ROUTER_HISTORY === 'hash'
? createWebHashHistory(import.meta.env.VITE_BASE)
: createWebHistory(import.meta.env.VITE_BASE),
// 应该添加到路由的初始路由列表。
routes,
scrollBehavior: (to, _from, savedPosition) => {
if (savedPosition) {
return savedPosition;
}
return to.hash ? { behavior: 'smooth', el: to.hash } : { left: 0, top: 0 };
},
// 是否应该禁止尾部斜杠。
// strict: true,
});
const resetRoutes = () => resetStaticRoutes(router, routes);
// 创建路由守卫
createRouterGuard(router);
export { resetRoutes, router };

View File

@@ -0,0 +1,97 @@
import type { RouteRecordRaw } from 'vue-router';
import { LOGIN_PATH } from '@vben/constants';
import { preferences } from '@vben/preferences';
import { $t } from '#/locales';
const BasicLayout = () => import('#/layouts/basic.vue');
const AuthPageLayout = () => import('#/layouts/auth.vue');
/** 全局404页面 */
const fallbackNotFoundRoute: RouteRecordRaw = {
component: () => import('#/views/_core/fallback/not-found.vue'),
meta: {
hideInBreadcrumb: true,
hideInMenu: true,
hideInTab: true,
title: '404',
},
name: 'FallbackNotFound',
path: '/:path(.*)*',
};
/** 基本路由,这些路由是必须存在的 */
const coreRoutes: RouteRecordRaw[] = [
/**
* 根路由
* 使用基础布局作为所有页面的父级容器子级就不必配置BasicLayout。
* 此路由必须存在,且不应修改
*/
{
component: BasicLayout,
meta: {
hideInBreadcrumb: true,
title: 'Root',
},
name: 'Root',
path: '/',
redirect: preferences.app.defaultHomePath,
children: [],
},
{
component: AuthPageLayout,
meta: {
hideInTab: true,
title: 'Authentication',
},
name: 'Authentication',
path: '/auth',
redirect: LOGIN_PATH,
children: [
{
name: 'Login',
path: 'login',
component: () => import('#/views/_core/authentication/login.vue'),
meta: {
title: $t('page.auth.login'),
},
},
{
name: 'CodeLogin',
path: 'code-login',
component: () => import('#/views/_core/authentication/code-login.vue'),
meta: {
title: $t('page.auth.codeLogin'),
},
},
{
name: 'QrCodeLogin',
path: 'qrcode-login',
component: () =>
import('#/views/_core/authentication/qrcode-login.vue'),
meta: {
title: $t('page.auth.qrcodeLogin'),
},
},
{
name: 'ForgetPassword',
path: 'forget-password',
component: () =>
import('#/views/_core/authentication/forget-password.vue'),
meta: {
title: $t('page.auth.forgetPassword'),
},
},
{
name: 'Register',
path: 'register',
component: () => import('#/views/_core/authentication/register.vue'),
meta: {
title: $t('page.auth.register'),
},
},
],
},
];
export { coreRoutes, fallbackNotFoundRoute };

View File

@@ -0,0 +1,37 @@
import type { RouteRecordRaw } from 'vue-router';
import { mergeRouteModules, traverseTreeValues } from '@vben/utils';
import { coreRoutes, fallbackNotFoundRoute } from './core';
const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
eager: true,
});
// 有需要可以自行打开注释,并创建文件夹
// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
/** 动态路由 */
const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
/** 外部路由列表访问这些页面可以不需要Layout可能用于内嵌在别的系统(不会显示在菜单中) */
// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
const staticRoutes: RouteRecordRaw[] = [];
const externalRoutes: RouteRecordRaw[] = [];
/** 路由列表由基本路由、外部路由和404兜底路由组成
* 无需走权限验证(会一直显示在菜单中) */
const routes: RouteRecordRaw[] = [
...coreRoutes,
...externalRoutes,
fallbackNotFoundRoute,
];
/** 基本路由列表,这些路由不需要进入权限拦截 */
const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
/** 有权限校验的路由列表,包含动态路由和静态路由 */
const accessRoutes = [...dynamicRoutes, ...staticRoutes];
export { accessRoutes, coreRouteNames, routes };

View File

@@ -0,0 +1,30 @@
import type { RouteRecordRaw } from 'vue-router';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
meta: {
icon: 'lucide:calendar-check',
order: 2,
title: $t('page.booking.title'),
},
name: 'Booking',
path: '/booking',
redirect: '/booking/list',
children: [
{
name: 'BookingList',
path: 'list',
component: () => import('#/views/booking/list.vue'),
meta: {
icon: 'lucide:list',
title: $t('page.booking.list'),
},
},
],
},
];
export default routes;

View File

@@ -0,0 +1,39 @@
import type { RouteRecordRaw } from 'vue-router';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
meta: {
icon: 'lucide:graduation-cap',
order: 4,
title: $t('page.class.title'),
},
name: 'Class',
path: '/class',
redirect: '/class/list',
children: [
{
name: 'ClassList',
path: 'list',
component: () => import('#/views/class/list.vue'),
meta: {
icon: 'lucide:list',
title: $t('page.class.list'),
},
},
{
name: 'ClassDetail',
path: 'detail/:id?',
component: () => import('#/views/class/detail.vue'),
meta: {
hideInMenu: true,
title: $t('page.class.detail'),
},
},
],
},
];
export default routes;

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