This commit is contained in:
Your Name
2026-08-27 14:04:28 +08:00
parent f7720831be
commit 334890171e
3016 changed files with 263403 additions and 27971 deletions
+4
View File
@@ -0,0 +1,4 @@
> 1%
last 2 versions
not dead
not ie 11
+3
View File
@@ -0,0 +1,3 @@
{
"singleQuote": true
}
+5
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
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": []
}
+7
View File
@@ -0,0 +1,7 @@
---
'@vben/styles': patch
'@vben-core/form-ui': patch
'@vben/web-naive': patch
---
feat(@core/form-ui): 新增 useVbenForm 数组编辑器 VbenFormFieldArray
+1
View File
@@ -0,0 +1 @@
export { default } from '@vben/commitlint-config';
+7
View File
@@ -0,0 +1,7 @@
node_modules
.git
.gitignore
*.md
dist
.turbo
dist.zip
+18
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
+21
View File
@@ -0,0 +1,21 @@
# 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 text eol=crlf
*.[cC][mM][dD] text eol=crlf
*.bat text eol=crlf
*.[bB][aA][tT] text eol=crlf
# Denote all files that are truly binary and should not be modified.
*.ico binary
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.webp binary
*.svg binary
*.woff binary
*.woff2 binary
+2
View File
@@ -0,0 +1,2 @@
[core]
ignorecase = false
+14
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
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
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`
+70
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
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@v6
- name: Install Node.js
uses: actions/setup-node@v6
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
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.
+36
View File
@@ -0,0 +1,36 @@
# 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
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
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
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
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
View File
@@ -0,0 +1,13 @@
titleAndCommits: true
types:
- feat
- fix
- docs
- chore
- style
- refactor
- perf
- test
- build
- ci
- revert
+48
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@v7
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
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@v7
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
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@v7
with:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v6
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@v7
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@v7
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
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@v7
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
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@v4
with:
category: '/language:${{matrix.language}}'
+172
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@v7
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.4.0
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@v7
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.4.0
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@v7
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.4.0
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@v7
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.4.0
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@v7
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.4.0
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
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@v7
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+31
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@v11
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
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-backup@d65454423c6fbbd20026b9b499d403f79422ac69
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-backup@d65454423c6fbbd20026b9b499d403f79422ac69
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-backup@d65454423c6fbbd20026b9b499d403f79422ac69
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
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@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
issue-inactive-days: '14'
issue-lock-reason: ''
pr-inactive-days: '30'
pr-lock-reason: ''
process-only: 'issues, prs'
+50
View File
@@ -0,0 +1,50 @@
name: Create Release Tag
on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:
inputs:
tag:
description: 'Tag to create (e.g. v1.2.3)'
required: true
type: string
permissions:
contents: write
jobs:
release:
name: Create Release
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Extract version
id: version
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
raw_tag="${{ inputs.tag }}"
else
raw_tag="${GITHUB_REF_NAME}"
fi
# Normalize: ensure v prefix
tag="${raw_tag}"
[[ "${tag:0:1}" != "v" ]] && tag="v${tag}"
version="${tag#v}"
major="${version%%.*}"
echo "tag=${tag}" >> "${GITHUB_OUTPUT}"
echo "version=${version}" >> "${GITHUB_OUTPUT}"
echo "major=${major}" >> "${GITHUB_OUTPUT}"
- uses: release-drafter/release-drafter@v7
with:
version: ${{ steps.version.outputs.version }}
publish: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+19
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
+41
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@v6
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
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@v11
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
+63
View File
@@ -0,0 +1,63 @@
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
.omx
.pnpm-store
# 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
# AI
.agent
.agents
.claude
.codex
skills-lock.json
.atomcode
datalog
.playwright-mcp
+6
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
View File
@@ -0,0 +1 @@
24.16.0
+14
View File
@@ -0,0 +1,14 @@
# registry.npmjs.org 在这台机器上完全不通(curl 直接超时),而 lockfile 里锁的
# 本来就是 npmmirror 的地址。两边不一致时 pnpm 会去撞不通的那个,表现是零星几
# 个大包反复超时,而大部分包因为有缓存又能装上——很难一眼看出是源的问题。
registry=https://registry.npmmirror.com
# 大二进制包(echarts / turbo / rolldown 的平台产物)在国内镜像上经常拉超时。
# 默认 60 秒不够,给足并降低并发,避免同时抢带宽把每一路都拖到超时。
fetch-timeout=600000
fetch-retries=6
fetch-retry-maxtimeout=120000
network-concurrency=6
# 本机 corepack 提供的是 pnpm 11.5.1,项目钉的是 11.16.0。
# 版本差异只影响 lockfile 格式的边角,不值得让 postinstall 整个失败——降成警告。
pm-on-fail=warn
+8
View File
@@ -0,0 +1,8 @@
dist
public
__tests__
coverage
.codex
.claude
.agent
.agents
+34
View File
@@ -0,0 +1,34 @@
{
"recommendations": [
// Vue 3 的语言支持
"Vue.volar",
// 将 eslint 集成到 VS Code 中。
"dbaeumer.vscode-eslint",
// 将 oxlint 集成到 VS Code 中。
"oxc.oxc-vscode",
// Visual Studio Code 的官方 Stylelint 扩展
"stylelint.vscode-stylelint",
// 使用 oxfmt 的代码格式化程序
"oxc.oxc-vscode",
// 支持 dotenv 文件语法
"mikestead.dotenv",
// YAML 语言支持,供 ESLint 校验 pnpm-workspace.yaml 等文件
"redhat.vscode-yaml",
// 源代码的拼写检查器
"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
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
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"
}
]
}
+243
View File
@@ -0,0 +1,243 @@
{
"tailwindCSS.experimental.configFile": "internal/tailwind-config/src/theme.css",
"tailwindCSS.lint.suggestCanonicalClasses": "ignore",
// 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.quickSuggestions": {
"strings": "on"
},
// lint && format
"oxc.enable": true,
"oxc.typeAware": true,
"oxc.configPath": "oxlint.config.ts",
"oxc.fmt.configPath": "oxfmt.config.ts",
"eslint.useFlatConfig": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.fixAll.oxc": "explicit",
"source.fixAll.stylelint": "explicit",
"source.organizeImports": "never"
},
"editor.defaultFormatter": "oxc.oxc-vscode",
"[html]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[css]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[scss]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[json]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[markdown]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[vue]": {
"editor.defaultFormatter": "oxc.oxc-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": {
"*.css": "tailwindcss",
"*.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"],
"stylelint.enable": true,
"stylelint.packageManager": "pnpm",
"stylelint.validate": ["css", "less", "postcss", "scss", "vue"],
"stylelint.customSyntax": "postcss-html",
"stylelint.snippet": ["css", "less", "postcss", "scss", "vue"],
"js/ts.tsdk.path": "node_modules/typescript/lib",
"js/ts.inlayHints.enumMemberValues.enabled": true,
"js/ts.preferences.preferTypeOnlyAutoImports": true,
"js/ts.preferences.includePackageJsonAutoImports": "on",
"eslint.validate": [
"javascript",
"typescript",
"javascriptreact",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"json5",
"yaml"
],
"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",
"oxlint.config.ts": ".eslintignore,.stylelintignore,.commitlintrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json,lefthook.yml,oxfmt.config.*,eslint.config.*"
},
"commentTranslate.hover.enabled": false,
"commentTranslate.multiLineMerge": true,
"vue.server.hybridMode": true
}
+9
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
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を使用
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. コードをフォーク
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` 型定義ファイルの変更
## ブラウザサポート
Tailwind CSS v4.0 is designed for Safari 16.4+, Chrome 111+, and Firefox 128+
モダンブラウザをサポートし、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://star-history.dera.page/svg?repos=vbenjs/vue-vben-admin&type=Date)](https://star-history.dera.page/#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
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)](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
Tailwind CSS v4.0 is designed for Safari 16.4+, Chrome 111+, and Firefox 128+
Support modern browsers, not IE
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
| :-: | :-: | :-: | :-: |
| last 2 versions | last 2 versions | last 2 versions | last 2 versions |
## Maintainer
[@Vben](https://github.com/anncwb)
## Star History
[![Star History Chart](https://star-history.dera.page/svg?repos=vbenjs/vue-vben-admin&type=Date)](https://star-history.dera.page/#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
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` 类型定义文件更改
## 浏览器支持
Tailwind CSS v4.0 is designed for Safari 16.4+, Chrome 111+, and Firefox 128+
支持现代浏览器,不支持 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://star-history.dera.page/svg?repos=vbenjs/vue-vben-admin&type=Date)](https://star-history.dera.page/#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)
+3
View File
@@ -0,0 +1,3 @@
PORT=5320
ACCESS_TOKEN_SECRET=access_token_secret
REFRESH_TOKEN_SECRET=refresh_token_secret
+15
View File
@@ -0,0 +1,15 @@
# @vben/backend-mock
## Description
Vben Admin 数据 mock 服务,没有对接任何的数据库,所有数据都是模拟的,用于前端开发时提供数据支持。线上环境不再提供 mock 集成,可自行部署服务或者对接真实数据,由于 `mock.js` 等工具有一些限制,比如上传文件不行、无法模拟复杂的逻辑等,所以这里使用了真实的后端服务来实现。唯一麻烦的是本地需要同时启动后端服务和前端服务,但是这样可以更好的模拟真实环境。该服务不需要手动启动,已经集成在 vite 插件内,随应用一起启用。
## Running the app
```bash
# development
$ pnpm run start
# production mode
$ pnpm run build
```
@@ -0,0 +1,16 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_CODES } from '~/utils/mock-data';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const codes =
MOCK_CODES.find((item) => item.username === userinfo.username)?.codes ?? [];
return useResponseSuccess(codes);
});
@@ -0,0 +1,42 @@
import { defineEventHandler, readBody, setResponseStatus } from 'h3';
import {
clearRefreshTokenCookie,
setRefreshTokenCookie,
} from '~/utils/cookie-utils';
import { generateAccessToken, generateRefreshToken } from '~/utils/jwt-utils';
import { MOCK_USERS } from '~/utils/mock-data';
import {
forbiddenResponse,
useResponseError,
useResponseSuccess,
} from '~/utils/response';
export default defineEventHandler(async (event) => {
const { password, username } = await readBody(event);
if (!password || !username) {
setResponseStatus(event, 400);
return useResponseError(
'BadRequestException',
'Username and password are required',
);
}
const findUser = MOCK_USERS.find(
(item) => item.username === username && item.password === password,
);
if (!findUser) {
clearRefreshTokenCookie(event);
return forbiddenResponse(event, 'Username or password is incorrect.');
}
const accessToken = generateAccessToken(findUser);
const refreshToken = generateRefreshToken(findUser);
setRefreshTokenCookie(event, refreshToken);
return useResponseSuccess({
...findUser,
accessToken,
});
});
@@ -0,0 +1,17 @@
import { defineEventHandler } from 'h3';
import {
clearRefreshTokenCookie,
getRefreshTokenFromCookie,
} from '~/utils/cookie-utils';
import { useResponseSuccess } from '~/utils/response';
export default defineEventHandler(async (event) => {
const refreshToken = getRefreshTokenFromCookie(event);
if (!refreshToken) {
return useResponseSuccess('');
}
clearRefreshTokenCookie(event);
return useResponseSuccess('');
});
@@ -0,0 +1,35 @@
import { defineEventHandler } from 'h3';
import {
clearRefreshTokenCookie,
getRefreshTokenFromCookie,
setRefreshTokenCookie,
} from '~/utils/cookie-utils';
import { generateAccessToken, verifyRefreshToken } from '~/utils/jwt-utils';
import { MOCK_USERS } from '~/utils/mock-data';
import { forbiddenResponse } from '~/utils/response';
export default defineEventHandler(async (event) => {
const refreshToken = getRefreshTokenFromCookie(event);
if (!refreshToken) {
return forbiddenResponse(event);
}
clearRefreshTokenCookie(event);
const userinfo = verifyRefreshToken(refreshToken);
if (!userinfo) {
return forbiddenResponse(event);
}
const findUser = MOCK_USERS.find(
(item) => item.username === userinfo.username,
);
if (!findUser) {
return forbiddenResponse(event);
}
const accessToken = generateAccessToken(findUser);
setRefreshTokenCookie(event, refreshToken);
return accessToken;
});
@@ -0,0 +1,32 @@
import { eventHandler, setHeader } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const data = `
{
"code": 0,
"message": "success",
"data": [
{
"id": 123456789012345678901234567890123456789012345678901234567890,
"name": "John Doe",
"age": 30,
"email": "john-doe@demo.com"
},
{
"id": 987654321098765432109876543210987654321098765432109876543210,
"name": "Jane Smith",
"age": 25,
"email": "jane@demo.com"
}
]
}
`;
setHeader(event, 'Content-Type', 'application/json');
return data;
});
@@ -0,0 +1,15 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENUS } from '~/utils/mock-data';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const menus =
MOCK_MENUS.find((item) => item.username === userinfo.username)?.menus ?? [];
return useResponseSuccess(menus);
});
@@ -0,0 +1,8 @@
import { eventHandler, getQuery, setResponseStatus } from 'h3';
import { useResponseError } from '~/utils/response';
export default eventHandler((event) => {
const { status } = getQuery(event);
setResponseStatus(event, Number(status));
return useResponseError(`${status}`);
});
@@ -0,0 +1,16 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,
unAuthorizedResponse,
useResponseSuccess,
} from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
await sleep(600);
return useResponseSuccess(null);
});
@@ -0,0 +1,16 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,
unAuthorizedResponse,
useResponseSuccess,
} from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
await sleep(1000);
return useResponseSuccess(null);
});
@@ -0,0 +1,16 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,
unAuthorizedResponse,
useResponseSuccess,
} from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
await sleep(2000);
return useResponseSuccess(null);
});
@@ -0,0 +1,62 @@
import { faker } from '@faker-js/faker';
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
const formatterCN = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
function generateMockDataList(count: number) {
const dataList = [];
for (let i = 0; i < count; i++) {
const dataItem: Record<string, any> = {
id: faker.string.uuid(),
pid: 0,
name: faker.commerce.department(),
status: faker.helpers.arrayElement([0, 1]),
createTime: formatterCN.format(
faker.date.between({ from: '2021-01-01', to: '2022-12-31' }),
),
remark: faker.lorem.sentence(),
};
if (faker.datatype.boolean()) {
dataItem.children = Array.from(
{ length: faker.number.int({ min: 1, max: 5 }) },
() => ({
id: faker.string.uuid(),
pid: dataItem.id,
name: faker.commerce.department(),
status: faker.helpers.arrayElement([0, 1]),
createTime: formatterCN.format(
faker.date.between({ from: '2023-01-01', to: '2023-12-31' }),
),
remark: faker.lorem.sentence(),
}),
);
}
dataList.push(dataItem);
}
return dataList;
}
const mockData = generateMockDataList(10);
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const listData = structuredClone(mockData);
return useResponseSuccess(listData);
});
@@ -0,0 +1,13 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
return useResponseSuccess(MOCK_MENU_LIST);
});
@@ -0,0 +1,29 @@
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
const namesMap: Record<string, any> = {};
function getNames(menus: any[]) {
menus.forEach((menu) => {
namesMap[menu.name] = String(menu.id);
if (menu.children) {
getNames(menu.children);
}
});
}
getNames(MOCK_MENU_LIST);
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const { id, name } = getQuery(event);
return (name as string) in namesMap &&
(!id || namesMap[name as string] !== String(id))
? useResponseSuccess(true)
: useResponseSuccess(false);
});
@@ -0,0 +1,29 @@
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
const pathMap: Record<string, any> = { '/': 0 };
function getPaths(menus: any[]) {
menus.forEach((menu) => {
pathMap[menu.path] = String(menu.id);
if (menu.children) {
getPaths(menu.children);
}
});
}
getPaths(MOCK_MENU_LIST);
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const { id, path } = getQuery(event);
return (path as string) in pathMap &&
(!id || pathMap[path as string] !== String(id))
? useResponseSuccess(true)
: useResponseSuccess(false);
});
@@ -0,0 +1,84 @@
import { faker } from '@faker-js/faker';
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { getMenuIds, MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
const formatterCN = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
const menuIds = getMenuIds(MOCK_MENU_LIST);
function generateMockDataList(count: number) {
const dataList = [];
for (let i = 0; i < count; i++) {
const dataItem: Record<string, any> = {
id: faker.string.uuid(),
name: faker.commerce.product(),
status: faker.helpers.arrayElement([0, 1]),
createTime: formatterCN.format(
faker.date.between({ from: '2022-01-01', to: '2025-01-01' }),
),
permissions: faker.helpers.arrayElements(menuIds),
remark: faker.lorem.sentence(),
};
dataList.push(dataItem);
}
return dataList;
}
const mockData = generateMockDataList(100);
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const {
page = 1,
pageSize = 20,
name,
id,
remark,
startTime,
endTime,
status,
} = getQuery(event);
let listData = structuredClone(mockData);
if (name) {
listData = listData.filter((item) =>
item.name.toLowerCase().includes(String(name).toLowerCase()),
);
}
if (id) {
listData = listData.filter((item) =>
item.id.toLowerCase().includes(String(id).toLowerCase()),
);
}
if (remark) {
listData = listData.filter((item) =>
item.remark?.toLowerCase()?.includes(String(remark).toLowerCase()),
);
}
if (startTime) {
listData = listData.filter((item) => item.createTime >= startTime);
}
if (endTime) {
listData = listData.filter((item) => item.createTime <= endTime);
}
if (['0', '1'].includes(status as string)) {
listData = listData.filter((item) => item.status === Number(status));
}
return usePageResponseSuccess(page as string, pageSize as string, listData);
});
@@ -0,0 +1,85 @@
import { faker } from '@faker-js/faker';
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
const formatterCN = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
function generateMockDataList(count: number) {
const dataList = [];
for (let i = 0; i < count; i++) {
const dataItem: Record<string, any> = {
id: faker.string.uuid(),
name: faker.commerce.product(),
status: faker.helpers.arrayElement([0, 1]),
createTime: formatterCN.format(
faker.date.between({ from: '2022-01-01', to: '2025-01-01' }),
),
deptId: faker.string.uuid(),
remark: faker.lorem.sentence(),
};
dataList.push(dataItem);
}
return dataList;
}
const mockData = generateMockDataList(100);
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const {
page = 1,
pageSize = 20,
name,
id,
remark,
startTime,
endTime,
deptId,
status,
} = getQuery(event);
let listData = structuredClone(mockData);
if (name) {
listData = listData.filter((item) =>
item.name.toLowerCase().includes(String(name).toLowerCase()),
);
}
if (id) {
listData = listData.filter((item) =>
item.id.toLowerCase().includes(String(id).toLowerCase()),
);
}
if (remark) {
listData = listData.filter((item) =>
item.remark?.toLowerCase()?.includes(String(remark).toLowerCase()),
);
}
if (startTime) {
listData = listData.filter((item) => item.createTime >= startTime);
}
if (endTime) {
listData = listData.filter((item) => item.createTime <= endTime);
}
if (['0', '1'].includes(status as string)) {
listData = listData.filter((item) => item.status === Number(status));
}
if (deptId) {
listData = listData.filter((item) => item.deptId === deptId);
}
return usePageResponseSuccess(page as string, pageSize as string, listData);
});
@@ -0,0 +1,117 @@
import { faker } from '@faker-js/faker';
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,
unAuthorizedResponse,
usePageResponseSuccess,
} from '~/utils/response';
function generateMockDataList(count: number) {
const dataList = [];
for (let i = 0; i < count; i++) {
const dataItem = {
id: faker.string.uuid(),
imageUrl: faker.image.avatar(),
imageUrl2: faker.image.avatar(),
open: faker.datatype.boolean(),
status: faker.helpers.arrayElement(['success', 'error', 'warning']),
productName: faker.commerce.productName(),
price: faker.commerce.price(),
currency: faker.finance.currencyCode(),
quantity: faker.number.int({ min: 1, max: 100 }),
available: faker.datatype.boolean(),
category: faker.commerce.department(),
releaseDate: faker.date.past(),
rating: faker.number.float({ min: 1, max: 5 }),
description: faker.commerce.productDescription(),
weight: faker.number.float({ min: 0.1, max: 10 }),
color: faker.color.human(),
inProduction: faker.datatype.boolean(),
tags: Array.from({ length: 3 }, () => faker.commerce.productAdjective()),
};
dataList.push(dataItem);
}
return dataList;
}
const mockData = generateMockDataList(100);
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
await sleep(600);
const { page, pageSize, sortBy, sortOrder } = getQuery(event);
// 规范化分页参数,处理 string[]
const pageRaw = Array.isArray(page) ? page[0] : page;
const pageSizeRaw = Array.isArray(pageSize) ? pageSize[0] : pageSize;
const pageNumber = Math.max(
1,
Number.parseInt(String(pageRaw ?? '1'), 10) || 1,
);
const pageSizeNumber = Math.min(
100,
Math.max(1, Number.parseInt(String(pageSizeRaw ?? '10'), 10) || 10),
);
const listData = structuredClone(mockData);
// 规范化 query 入参,兼容 string[]
const sortKeyRaw = Array.isArray(sortBy) ? sortBy[0] : sortBy;
const sortOrderRaw = Array.isArray(sortOrder) ? sortOrder[0] : sortOrder;
// 检查 sortBy 是否是 listData 元素的合法属性键
if (
typeof sortKeyRaw === 'string' &&
listData[0] &&
Object.prototype.hasOwnProperty.call(listData[0], sortKeyRaw)
) {
// 定义数组元素的类型
type ItemType = (typeof listData)[0];
const sortKey = sortKeyRaw as keyof ItemType; // 将 sortBy 断言为合法键
const isDesc = sortOrderRaw === 'desc';
listData.sort((a, b) => {
const aValue = a[sortKey] as unknown;
const bValue = b[sortKey] as unknown;
let result: number;
if (typeof aValue === 'number' && typeof bValue === 'number') {
result = aValue - bValue;
} else if (aValue instanceof Date && bValue instanceof Date) {
result = aValue.getTime() - bValue.getTime();
} else if (typeof aValue === 'boolean' && typeof bValue === 'boolean') {
if (aValue === bValue) {
result = 0;
} else {
result = aValue ? 1 : -1;
}
} else {
const aStr = String(aValue);
const bStr = String(bValue);
const aNum = Number(aStr);
const bNum = Number(bStr);
result =
Number.isFinite(aNum) && Number.isFinite(bNum)
? aNum - bNum
: aStr.localeCompare(bStr, undefined, {
numeric: true,
sensitivity: 'base',
});
}
return isDesc ? -result : result;
});
}
return usePageResponseSuccess(
String(pageNumber),
String(pageSizeNumber),
listData,
);
});
@@ -0,0 +1,3 @@
import { defineEventHandler } from 'h3';
export default defineEventHandler(() => 'Test get handler');
@@ -0,0 +1,3 @@
import { defineEventHandler } from 'h3';
export default defineEventHandler(() => 'Test post handler');
@@ -0,0 +1,12 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
import { getTimezone } from '~/utils/timezone-utils';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
return useResponseSuccess(getTimezone());
});
@@ -0,0 +1,11 @@
import { eventHandler } from 'h3';
import { TIME_ZONE_OPTIONS } from '~/utils/mock-data';
import { useResponseSuccess } from '~/utils/response';
export default eventHandler(() => {
const data = TIME_ZONE_OPTIONS.map((o) => ({
label: `${o.timezone} (GMT${o.offset >= 0 ? `+${o.offset}` : o.offset})`,
value: o.timezone,
}));
return useResponseSuccess(data);
});
@@ -0,0 +1,26 @@
import { eventHandler, readBody, setResponseStatus } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { TIME_ZONE_OPTIONS } from '~/utils/mock-data';
import {
unAuthorizedResponse,
useResponseError,
useResponseSuccess,
} from '~/utils/response';
import { setTimezone } from '~/utils/timezone-utils';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const body = await readBody<{ timezone?: unknown }>(event);
const timezone =
typeof body?.timezone === 'string' ? body.timezone : undefined;
const allowed = TIME_ZONE_OPTIONS.some((o) => o.timezone === timezone);
if (!timezone || !allowed) {
setResponseStatus(event, 400);
return useResponseError('Bad Request', 'Invalid timezone');
}
setTimezone(timezone);
return useResponseSuccess({});
});
+14
View File
@@ -0,0 +1,14 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
return useResponseSuccess({
url: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
});
// return useResponseError("test")
});
@@ -0,0 +1,11 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
return useResponseSuccess(userinfo);
});
+7
View File
@@ -0,0 +1,7 @@
import type { NitroErrorHandler } from 'nitropack';
const errorHandler: NitroErrorHandler = function (error, event) {
event.node.res.end(`[Error Handler] ${error.stack}`);
};
export default errorHandler;
@@ -0,0 +1,20 @@
import { defineEventHandler } from 'h3';
import { forbiddenResponse, sleep } from '~/utils/response';
export default defineEventHandler(async (event) => {
event.node.res.setHeader(
'Access-Control-Allow-Origin',
event.headers.get('Origin') ?? '*',
);
if (event.method === 'OPTIONS') {
event.node.res.statusCode = 204;
event.node.res.statusMessage = 'No Content.';
return 'OK';
} else if (
['DELETE', 'PATCH', 'POST', 'PUT'].includes(event.method) &&
event.path.startsWith('/api/system/')
) {
await sleep(Math.floor(Math.random() * 2000));
return forbiddenResponse(event, '演示环境,禁止修改');
}
});
@@ -0,0 +1,22 @@
import { defineNitroConfig } from 'nitropack/config';
import errorHandler from './error';
process.env.COMPATIBILITY_DATE = new Date().toISOString();
export default defineNitroConfig({
devErrorHandler: errorHandler,
errorHandler: '~/error',
routeRules: {
'/api/**': {
cors: true,
headers: {
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Headers':
'Accept, Authorization, Content-Length, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-CSRF-TOKEN, X-Requested-With',
'Access-Control-Allow-Methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
'Access-Control-Allow-Origin': '*',
'Access-Control-Expose-Headers': '*',
},
},
},
});
+21
View File
@@ -0,0 +1,21 @@
{
"name": "@vben/backend-mock",
"version": "5.7.0",
"description": "",
"private": true,
"license": "MIT",
"author": "",
"scripts": {
"build": "nitro build",
"start": "nitro dev"
},
"dependencies": {
"@faker-js/faker": "catalog:",
"jsonwebtoken": "catalog:",
"nitropack": "catalog:"
},
"devDependencies": {
"@types/jsonwebtoken": "catalog:",
"h3": "catalog:"
}
}
@@ -0,0 +1,15 @@
import { defineEventHandler } from 'h3';
export default defineEventHandler(() => {
return `
<h1>Hello Vben Admin</h1>
<h2>Mock service is running</h2>
<ul>
<li><a href="/api/auth/login">/api/auth/login</a></li>
<li><a href="/api/auth/codes">/api/auth/codes</a></li>
<li><a href="/api/user/info">/api/user/info</a></li>
<li><a href="/api/menu/all">/api/menu/all</a></li>
<li><a href="/api/auth/logout">/api/auth/logout</a></li>
</ul>
`;
});
@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}
+11
View File
@@ -0,0 +1,11 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/node.json",
"compilerOptions": {
"paths": {
"~/*": ["./*"]
}
},
"include": ["**/*.ts", "**/.*.ts"],
"exclude": ["node_modules", "dist", ".nitro"]
}
@@ -0,0 +1,28 @@
import type { EventHandlerRequest, H3Event } from 'h3';
import { deleteCookie, getCookie, setCookie } from 'h3';
export function clearRefreshTokenCookie(event: H3Event<EventHandlerRequest>) {
deleteCookie(event, 'jwt', {
httpOnly: true,
sameSite: 'none',
secure: true,
});
}
export function setRefreshTokenCookie(
event: H3Event<EventHandlerRequest>,
refreshToken: string,
) {
setCookie(event, 'jwt', refreshToken, {
httpOnly: true,
maxAge: 24 * 60 * 60, // unit: seconds
sameSite: 'none',
secure: true,
});
}
export function getRefreshTokenFromCookie(event: H3Event<EventHandlerRequest>) {
const refreshToken = getCookie(event, 'jwt');
return refreshToken;
}
@@ -0,0 +1,77 @@
import type { EventHandlerRequest, H3Event } from 'h3';
import type { UserInfo } from './mock-data';
import { getHeader } from 'h3';
import jwt from 'jsonwebtoken';
import { MOCK_USERS } from './mock-data';
// TODO: Replace with your own secret key
const ACCESS_TOKEN_SECRET = 'access_token_secret';
const REFRESH_TOKEN_SECRET = 'refresh_token_secret';
export interface UserPayload extends UserInfo {
iat: number;
exp: number;
}
export function generateAccessToken(user: UserInfo) {
return jwt.sign(user, ACCESS_TOKEN_SECRET, { expiresIn: '7d' });
}
export function generateRefreshToken(user: UserInfo) {
return jwt.sign(user, REFRESH_TOKEN_SECRET, {
expiresIn: '30d',
});
}
export function verifyAccessToken(
event: H3Event<EventHandlerRequest>,
): null | Omit<UserInfo, 'password'> {
const authHeader = getHeader(event, 'Authorization');
if (!authHeader?.startsWith('Bearer')) {
return null;
}
const tokenParts = authHeader.split(' ');
if (tokenParts.length !== 2) {
return null;
}
const token = tokenParts[1] as string;
try {
const decoded = jwt.verify(
token,
ACCESS_TOKEN_SECRET,
) as unknown as UserPayload;
const username = decoded.username;
const user = MOCK_USERS.find((item) => item.username === username);
if (!user) {
return null;
}
const { password: _pwd, ...userinfo } = user;
return userinfo;
} catch {
return null;
}
}
export function verifyRefreshToken(
token: string,
): null | Omit<UserInfo, 'password'> {
try {
const decoded = jwt.verify(token, REFRESH_TOKEN_SECRET) as UserPayload;
const username = decoded.username;
const user = MOCK_USERS.find(
(item) => item.username === username,
) as UserInfo;
if (!user) {
return null;
}
const { password: _pwd, ...userinfo } = user;
return userinfo;
} catch {
return null;
}
}
@@ -0,0 +1,450 @@
export interface UserInfo {
id: number;
password: string;
realName: string;
roles: string[];
username: string;
homePath?: string;
}
export interface TimezoneOption {
offset: number;
timezone: string;
}
export const MOCK_USERS: UserInfo[] = [
{
id: 0,
password: '123456',
realName: 'Vben',
roles: ['super'],
username: 'vben',
homePath: '/dashboard/workspace',
},
{
id: 1,
password: '123456',
realName: 'Admin',
roles: ['admin'],
username: 'admin',
homePath: '/dashboard/workspace',
},
{
id: 2,
password: '123456',
realName: 'Jack',
roles: ['user'],
username: 'jack',
homePath: '/dashboard/analytics',
},
];
export const MOCK_CODES = [
// super
{
codes: ['AC_100100', 'AC_100110', 'AC_100120', 'AC_100010'],
username: 'vben',
},
{
// admin
codes: ['AC_100010', 'AC_100020', 'AC_100030'],
username: 'admin',
},
{
// user
codes: ['AC_1000001', 'AC_1000002'],
username: 'jack',
},
];
const dashboardMenus = [
{
meta: {
order: -1,
title: 'page.dashboard.title',
},
name: 'Dashboard',
path: '/dashboard',
redirect: '/dashboard/analytics',
children: [
{
name: 'Analytics',
path: 'analytics',
component: '/dashboard/analytics/index',
meta: {
affixTab: true,
title: 'page.dashboard.analytics',
},
},
{
name: 'Workspace',
path: 'workspace',
component: '/dashboard/workspace/index',
meta: {
title: 'page.dashboard.workspace',
},
},
],
},
];
const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
const roleWithMenus = {
admin: {
component: '/demos/access/admin-visible',
meta: {
icon: 'mdi:button-cursor',
title: 'demos.access.adminVisible',
},
name: 'AccessAdminVisibleDemo',
path: 'admin-visible',
},
super: {
component: '/demos/access/super-visible',
meta: {
icon: 'mdi:button-cursor',
title: 'demos.access.superVisible',
},
name: 'AccessSuperVisibleDemo',
path: 'super-visible',
},
user: {
component: '/demos/access/user-visible',
meta: {
icon: 'mdi:button-cursor',
title: 'demos.access.userVisible',
},
name: 'AccessUserVisibleDemo',
path: 'user-visible',
},
};
return [
{
meta: {
icon: 'ic:baseline-view-in-ar',
keepAlive: true,
order: 1000,
title: 'demos.title',
},
name: 'Demos',
path: '/demos',
redirect: '/demos/access',
children: [
{
name: 'AccessDemos',
path: 'access',
meta: {
icon: 'mdi:cloud-key-outline',
title: 'demos.access.backendPermissions',
},
redirect: '/demos/access/page-control',
children: [
{
name: 'AccessPageControlDemo',
path: 'page-control',
component: '/demos/access/index',
meta: {
icon: 'mdi:page-previous-outline',
title: 'demos.access.pageAccess',
},
},
{
name: 'AccessButtonControlDemo',
path: 'button-control',
component: '/demos/access/button-control',
meta: {
icon: 'mdi:button-cursor',
title: 'demos.access.buttonControl',
},
},
{
name: 'AccessMenuVisible403Demo',
path: 'menu-visible-403',
component: '/demos/access/menu-visible-403',
meta: {
authority: ['no-body'],
icon: 'mdi:button-cursor',
menuVisibleWithForbidden: true,
title: 'demos.access.menuVisible403',
},
},
roleWithMenus[role],
],
},
],
},
];
};
export const MOCK_MENUS = [
{
menus: [...dashboardMenus, ...createDemosMenus('super')],
username: 'vben',
},
{
menus: [...dashboardMenus, ...createDemosMenus('admin')],
username: 'admin',
},
{
menus: [...dashboardMenus, ...createDemosMenus('user')],
username: 'jack',
},
];
export const MOCK_MENU_LIST = [
{
id: 1,
name: 'Dashboard',
status: 1,
type: 'catalog',
icon: 'lucide:layout-dashboard',
path: '/dashboard',
meta: {
icon: 'lucide:layout-dashboard',
order: -1,
title: 'page.dashboard.title',
},
children: [
{
id: 101,
pid: 1,
status: 1,
type: 'menu',
name: 'Analytics',
path: 'analytics',
component: '/dashboard/analytics/index',
meta: {
affixTab: true,
icon: 'lucide:area-chart',
title: 'page.dashboard.analytics',
keepAlive: true,
},
},
{
id: 102,
pid: 1,
status: 1,
type: 'menu',
name: 'Workspace',
path: 'workspace',
component: '/views/dashboard/workspace/index',
meta: {
icon: 'carbon:workspace',
title: 'page.dashboard.workspace',
},
},
],
},
{
id: 2,
meta: {
icon: 'carbon:settings',
order: 9997,
title: 'system.title',
badge: 'new',
badgeType: 'normal',
badgeVariants: 'primary',
},
status: 1,
type: 'catalog',
name: 'System',
path: '/system',
children: [
{
id: 201,
pid: 2,
path: '/system/menu',
name: 'SystemMenu',
authCode: 'System:Menu:List',
status: 1,
type: 'menu',
meta: {
icon: 'carbon:menu',
title: 'system.menu.title',
},
component: '/system/menu/list',
children: [
{
id: 20_101,
pid: 201,
name: 'SystemMenuCreate',
status: 1,
type: 'button',
authCode: 'System:Menu:Create',
meta: { title: 'common.create' },
},
{
id: 20_102,
pid: 201,
name: 'SystemMenuEdit',
status: 1,
type: 'button',
authCode: 'System:Menu:Edit',
meta: { title: 'common.edit' },
},
{
id: 20_103,
pid: 201,
name: 'SystemMenuDelete',
status: 1,
type: 'button',
authCode: 'System:Menu:Delete',
meta: { title: 'common.delete' },
},
],
},
{
id: 202,
pid: 2,
path: '/system/dept',
name: 'SystemDept',
status: 1,
type: 'menu',
authCode: 'System:Dept:List',
meta: {
icon: 'carbon:container-services',
title: 'system.dept.title',
},
component: '/system/dept/list',
children: [
{
id: 20_401,
pid: 202,
name: 'SystemDeptCreate',
status: 1,
type: 'button',
authCode: 'System:Dept:Create',
meta: { title: 'common.create' },
},
{
id: 20_402,
pid: 202,
name: 'SystemDeptEdit',
status: 1,
type: 'button',
authCode: 'System:Dept:Edit',
meta: { title: 'common.edit' },
},
{
id: 20_403,
pid: 202,
name: 'SystemDeptDelete',
status: 1,
type: 'button',
authCode: 'System:Dept:Delete',
meta: { title: 'common.delete' },
},
],
},
],
},
{
id: 9,
meta: {
badgeType: 'dot',
order: 9998,
title: 'demos.vben.title',
icon: 'carbon:data-center',
},
name: 'Project',
path: '/vben-admin',
type: 'catalog',
status: 1,
children: [
{
id: 901,
pid: 9,
name: 'VbenDocument',
path: '/vben-admin/document',
component: 'IFrameView',
type: 'embedded',
status: 1,
meta: {
icon: 'carbon:book',
iframeSrc: 'https://doc.vben.pro',
title: 'demos.vben.document',
},
},
{
id: 902,
pid: 9,
name: 'VbenGithub',
path: '/vben-admin/github',
component: 'IFrameView',
type: 'link',
status: 1,
meta: {
icon: 'carbon:logo-github',
link: 'https://github.com/vbenjs/vue-vben-admin',
title: 'Github',
},
},
{
id: 903,
pid: 9,
name: 'VbenAntdv',
path: '/vben-admin/antdv',
component: 'IFrameView',
type: 'link',
status: 0,
meta: {
icon: 'carbon:hexagon-vertical-solid',
badgeType: 'dot',
link: 'https://ant.vben.pro',
title: 'demos.vben.antdv',
},
},
],
},
{
id: 10,
component: '_core/about/index',
type: 'menu',
status: 1,
meta: {
icon: 'lucide:copyright',
order: 9999,
title: 'demos.vben.about',
},
name: 'About',
path: '/about',
},
];
export function getMenuIds(menus: any[]) {
const ids: number[] = [];
menus.forEach((item) => {
ids.push(item.id);
if (item.children && item.children.length > 0) {
ids.push(...getMenuIds(item.children));
}
});
return ids;
}
/**
* 时区选项
*/
export const TIME_ZONE_OPTIONS: TimezoneOption[] = [
{
offset: -5,
timezone: 'America/New_York',
},
{
offset: 0,
timezone: 'Europe/London',
},
{
offset: 8,
timezone: 'Asia/Shanghai',
},
{
offset: 9,
timezone: 'Asia/Tokyo',
},
{
offset: 9,
timezone: 'Asia/Seoul',
},
];
@@ -0,0 +1,70 @@
import type { EventHandlerRequest, H3Event } from 'h3';
import { setResponseStatus } from 'h3';
export function useResponseSuccess<T = any>(data: T) {
return {
code: 0,
data,
error: null,
message: 'ok',
};
}
export function usePageResponseSuccess<T = any>(
page: number | string,
pageSize: number | string,
list: T[],
{ message = 'ok' } = {},
) {
const pageData = pagination(
Number.parseInt(`${page}`),
Number.parseInt(`${pageSize}`),
list,
);
return {
...useResponseSuccess({
items: pageData,
total: list.length,
}),
message,
};
}
export function useResponseError(message: string, error: any = null) {
return {
code: -1,
data: null,
error,
message,
};
}
export function forbiddenResponse(
event: H3Event<EventHandlerRequest>,
message = 'Forbidden Exception',
) {
setResponseStatus(event, 403);
return useResponseError(message, message);
}
export function unAuthorizedResponse(event: H3Event<EventHandlerRequest>) {
setResponseStatus(event, 401);
return useResponseError('Unauthorized Exception', 'Unauthorized Exception');
}
export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function pagination<T = any>(
pageNo: number,
pageSize: number,
array: T[],
): T[] {
const offset = (pageNo - 1) * Number(pageSize);
return offset + Number(pageSize) >= array.length
? array.slice(offset)
: array.slice(offset, offset + Number(pageSize));
}
@@ -0,0 +1,9 @@
let mockTimeZone: null | string = null;
export const setTimezone = (timeZone: string) => {
mockTimeZone = timeZone;
};
export const getTimezone = () => {
return mockTimeZone;
};
+9
View File
@@ -0,0 +1,9 @@
# 应用标题
VITE_APP_TITLE=真羊 AI 客服 · 管理后台
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离
VITE_APP_NAMESPACE=zhenyang-admin
# 对 store 持久化到 localStorage 时加密用的密钥。
# 模板里默认是 please-replace-me-with-your-own-key——那是公开值,等于没加密。
VITE_APP_STORE_SECURE_KEY=DHIpryct4GsHrszDncvvxvzb8cp69mhEoXZvIU-ubik
+7
View File
@@ -0,0 +1,7 @@
# public path
VITE_BASE=/
# Basic interface address SPA
VITE_GLOB_API_URL=/api
VITE_VISUALIZER=true
+16
View File
@@ -0,0 +1,16 @@
# 端口号
VITE_PORT=5666
VITE_BASE=/
# 接口地址
VITE_GLOB_API_URL=/api
# 是否开启 Nitro Mock服务,true 为开启,false 为关闭
VITE_NITRO_MOCK=false
# 是否打开 devtoolstrue 为打开,false 为关闭
VITE_DEVTOOLS=false
# 是否注入全局loading
VITE_INJECT_APP_LOADING=true
+22
View File
@@ -0,0 +1,22 @@
VITE_BASE=/
# 接口地址
# 生产走同源相对路径:前端是静态文件,由同一个 Nginx 把 /api 反代到
# admin_api.py。模板默认值是 vben 的公共 mock 服务器 mock-napi.vben.pro——
# 那意味着打包后所有管理请求(含登录凭据)都会发给第三方主机。
VITE_GLOB_API_URL=/api
# 是否开启压缩,可以设置为 none, brotli, gzip
VITE_COMPRESS=none
# 是否开启 PWA
VITE_PWA=false
# vue-router 的模式
VITE_ROUTER_HISTORY=hash
# 是否注入全局loading
VITE_INJECT_APP_LOADING=true
# 打包后是否生成dist.zip
VITE_ARCHIVER=true
+35
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>
+50
View File
@@ -0,0 +1,50 @@
{
"name": "@vben/web-antd",
"version": "5.7.0",
"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:"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

@@ -0,0 +1,744 @@
/**
* 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
*/
/* eslint-disable vue/one-component-per-file */
import type {
AutoCompleteProps,
ButtonProps,
CascaderProps,
CheckboxGroupProps,
CheckboxProps,
DatePickerProps,
DividerProps,
InputNumberProps,
InputProps,
MentionsProps,
RadioGroupProps,
RadioProps,
RateProps,
SelectProps,
SpaceProps,
SwitchProps,
TextAreaProps,
TimePickerProps,
TreeSelectProps,
UploadChangeParam,
UploadFile,
UploadProps,
} from 'ant-design-vue';
import type { RangePickerProps } from 'ant-design-vue/es/date-picker';
import type { Component, Ref } from 'vue';
import type {
ApiComponentSharedProps,
BaseFormComponentType,
IconPickerProps,
} from '@vben/common-ui';
import type { Sortable } from '@vben/hooks';
import type { Recordable } from '@vben/types';
import {
computed,
defineAsyncComponent,
defineComponent,
h,
nextTick,
onMounted,
onUnmounted,
ref,
render,
unref,
watch,
} from 'vue';
import {
ApiComponent,
globalShareState,
IconPicker,
VCropper,
} from '@vben/common-ui';
import { useSortable } from '@vben/hooks';
import { IconifyIcon } from '@vben/icons';
import { $t } from '@vben/locales';
import { isEmpty } from '@vben/utils';
import { message, Modal, notification } from 'ant-design-vue';
type AdapterUploadProps = UploadProps & {
aspectRatio?: string;
crop?: boolean;
draggable?: boolean;
handleChange?: (event: UploadChangeParam) => void;
maxSize?: number;
onDragSort?: (oldIndex: number, newIndex: number) => void;
onHandleChange?: (event: UploadChangeParam) => void;
};
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 Cascader = defineAsyncComponent(
() => import('ant-design-vue/es/cascader'),
);
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 = (
component: Component,
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 IMAGE_EXTENSIONS = new Set([
'bmp',
'gif',
'jpeg',
'jpg',
'png',
'svg',
'webp',
]);
/**
* 检查是否为图片文件
*/
function isImageFile(file: UploadFile): boolean {
if (file.url) {
try {
const pathname = new URL(file.url, 'http://localhost').pathname;
const ext = pathname.split('.').pop()?.toLowerCase();
return ext ? IMAGE_EXTENSIONS.has(ext) : false;
} catch {
const ext = file.url?.split('.').pop()?.toLowerCase();
return ext ? IMAGE_EXTENSIONS.has(ext) : false;
}
}
if (!file.type) {
const ext = file.name?.split('.').pop()?.toLowerCase();
return ext ? IMAGE_EXTENSIONS.has(ext) : false;
}
return file.type.startsWith('image/');
}
/**
* 创建默认的上传按钮插槽
*/
function createDefaultUploadSlots(listType: string, placeholder: string) {
if (listType === 'picture-card') {
return { default: () => placeholder };
}
return {
default: () =>
h(
Button,
{
icon: h(IconifyIcon, {
icon: 'ant-design:upload-outlined',
class: 'mb-1 size-4',
}),
},
() => placeholder,
),
};
}
/**
* 获取文件的 Base64
*/
function getBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.addEventListener('load', () => resolve(reader.result as string));
reader.addEventListener('error', reject);
});
}
/**
* 预览图片
*/
async function previewImage(
file: UploadFile,
visible: Ref<boolean>,
fileList: Ref<UploadProps['fileList']>,
) {
// 非图片文件直接打开链接
if (!isImageFile(file)) {
const url = file.url || file.preview;
if (url) {
window.open(url, '_blank');
} else {
message.error($t('ui.formRules.previewWarning'));
}
return;
}
const [ImageComponent, PreviewGroupComponent] = await Promise.all([
Image,
PreviewGroup,
]);
// 过滤图片文件并生成预览
const imageFiles = (unref(fileList) || []).filter((f) => isImageFile(f));
for (const imgFile of imageFiles) {
if (!imgFile.url && !imgFile.preview && imgFile.originFileObj) {
imgFile.preview = await getBase64(imgFile.originFileObj);
}
}
const container = document.createElement('div');
document.body.append(container);
let isUnmounted = false;
const currentIndex = imageFiles.findIndex((f) => f.uid === file.uid);
const PreviewWrapper = {
setup() {
return () => {
if (isUnmounted) return null;
return h(
PreviewGroupComponent,
{
class: 'hidden',
preview: {
visible: visible.value,
current: currentIndex,
onVisibleChange: (value: boolean) => {
visible.value = value;
if (!value) {
setTimeout(() => {
if (!isUnmounted && container) {
isUnmounted = true;
render(null, container);
container.remove();
}
}, 300);
}
},
},
},
() =>
imageFiles.map((imgFile) =>
h(ImageComponent, {
key: imgFile.uid,
src: imgFile.url || imgFile.preview,
}),
),
);
};
},
};
render(h(PreviewWrapper), container);
}
/**
* 图片裁剪操作
*/
function cropImage(file: File, aspectRatio: string | undefined) {
return new Promise<Blob | string | undefined>((resolve, reject) => {
const container = document.createElement('div');
document.body.append(container);
let isUnmounted = false;
let objectUrl: null | string = null;
const open = ref<boolean>(true);
const cropperRef = ref<InstanceType<typeof VCropper> | null>(null);
const closeModal = () => {
open.value = false;
setTimeout(() => {
if (!isUnmounted && container) {
if (objectUrl) {
URL.revokeObjectURL(objectUrl);
}
isUnmounted = true;
render(null, container);
container.remove();
}
}, 300);
};
const CropperWrapper = {
setup() {
return () => {
if (isUnmounted) return null;
if (!objectUrl) {
objectUrl = URL.createObjectURL(file);
}
return h(
Modal,
{
open: open.value,
title: h('div', {}, [
$t('ui.crop.title'),
h(
'span',
{
class: `${aspectRatio ? '' : 'hidden'} ml-2 text-sm text-gray-400 font-normal`,
},
$t('ui.crop.titleTip', [aspectRatio]),
),
]),
centered: true,
width: 548,
zIndex: 9999,
keyboard: false,
maskClosable: false,
closable: false,
cancelText: $t('common.cancel'),
okText: $t('ui.crop.confirm'),
destroyOnClose: true,
onOk: async () => {
const cropper = cropperRef.value;
if (!cropper) {
reject(new Error('Cropper not found'));
closeModal();
return;
}
try {
const dataUrl = await cropper.getCropImage();
if (dataUrl) {
resolve(dataUrl);
} else {
reject(new Error($t('ui.crop.errorTip')));
}
} catch {
reject(new Error($t('ui.crop.errorTip')));
} finally {
closeModal();
}
},
onCancel() {
resolve('');
closeModal();
},
},
() =>
h(VCropper, {
ref: (ref: any) => (cropperRef.value = ref),
img: objectUrl as string,
aspectRatio,
}),
);
};
},
};
render(h(CropperWrapper), container);
});
}
/**
* 带预览功能的上传组件
*/
const withPreviewUpload = () => {
return defineComponent({
name: Upload.name,
emits: ['update:modelValue'],
setup(
props: any,
{ attrs, slots, emit }: { attrs: any; emit: any; slots: any },
) {
const previewVisible = ref<boolean>(false);
const placeholder = attrs?.placeholder || $t('ui.placeholder.upload');
const listType = attrs?.listType || attrs?.['list-type'] || 'text';
const fileList = ref<UploadProps['fileList']>(
attrs?.fileList || attrs?.['file-list'] || [],
);
const maxSize = computed(() => attrs?.maxSize ?? attrs?.['max-size']);
const aspectRatio = computed(
() => attrs?.aspectRatio ?? attrs?.['aspect-ratio'],
);
const handleBeforeUpload = async (
file: UploadFile,
originFileList: Array<File>,
) => {
// 文件大小限制
if (maxSize.value && (file.size || 0) / 1024 / 1024 > maxSize.value) {
message.error($t('ui.formRules.sizeLimit', [maxSize.value]));
file.status = 'removed';
return false;
}
// 图片裁剪处理
if (
attrs.crop &&
!attrs.multiple &&
originFileList[0] &&
isImageFile(file)
) {
file.status = 'removed';
const blob = await cropImage(originFileList[0], aspectRatio.value);
if (!blob) {
throw new Error($t('ui.crop.errorTip'));
}
return blob;
}
return attrs.beforeUpload?.(file) ?? true;
};
const handleChange = (event: UploadChangeParam) => {
try {
attrs.handleChange?.(event);
attrs.onHandleChange?.(event);
} catch (error) {
console.error(error);
}
fileList.value = event.fileList.filter(
(file) => file.status !== 'removed',
);
emit(
'update:modelValue',
event.fileList?.length ? fileList.value : undefined,
);
};
const handlePreview = async (file: UploadFile) => {
previewVisible.value = true;
await previewImage(file, previewVisible, fileList);
};
const renderUploadButton = () => {
if (attrs.disabled) return null;
return isEmpty(slots)
? createDefaultUploadSlots(listType, placeholder)
: slots;
};
// 拖拽排序
const draggable = computed(
() => (attrs.draggable ?? false) && !attrs.disabled,
);
const uploadId = `upload-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
const sortableInstance = ref<null | Sortable>(null);
const styleId = `upload-drag-style-${uploadId}`;
function injectDragStyle() {
if (!document.querySelector(`[id="${styleId}"]`)) {
const style = document.createElement('style');
style.id = styleId;
style.textContent = `
[data-upload-id="${uploadId}"] .ant-upload-list-item { cursor: move; }
[data-upload-id="${uploadId}"] .ant-upload-list-item:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.15); }
`;
document.head.append(style);
}
}
function removeDragStyle() {
document.querySelector(`[id="${styleId}"]`)?.remove();
}
async function initSortable(retryCount = 0) {
if (!draggable.value) return;
injectDragStyle();
await nextTick();
await new Promise((resolve) => setTimeout(resolve, 100));
const container = document.querySelector(
`[data-upload-id="${uploadId}"] .ant-upload-list`,
) as HTMLElement;
if (!container) {
if (retryCount < 5) {
setTimeout(() => initSortable(retryCount + 1), 200);
}
return;
}
const { initializeSortable } = useSortable(container, {
animation: 300,
delay: 400,
delayOnTouchOnly: true,
filter:
'.ant-upload-select, .ant-upload-list-item-error, .ant-upload-list-item-uploading',
onEnd: (evt) => {
const { oldIndex, newIndex } = evt;
if (
oldIndex === undefined ||
newIndex === undefined ||
oldIndex === newIndex
) {
return;
}
const list = [...(fileList.value || [])];
const [movedItem] = list.splice(oldIndex, 1);
if (movedItem) {
list.splice(newIndex, 0, movedItem);
fileList.value = list;
}
attrs.onDragSort?.(oldIndex, newIndex);
emit('update:modelValue', fileList.value);
},
});
sortableInstance.value = await initializeSortable();
}
// 监听表单值变化
watch(
() => attrs.modelValue,
(res) => {
fileList.value = res;
},
);
onMounted(initSortable);
onUnmounted(() => {
sortableInstance.value?.destroy();
removeDragStyle();
});
return () =>
h(
'div',
{ 'data-upload-id': uploadId, class: 'w-full' },
h(
Upload,
{
...props,
...attrs,
fileList: fileList.value,
beforeUpload: handleBeforeUpload,
onChange: handleChange,
onPreview: handlePreview,
},
renderUploadButton() as any,
),
);
},
});
};
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type ComponentType =
| 'ApiCascader'
| 'ApiSelect'
| 'ApiTreeSelect'
| 'AutoComplete'
| 'Cascader'
| 'Checkbox'
| 'CheckboxGroup'
| 'DatePicker'
| 'DefaultButton'
| 'Divider'
| 'IconPicker'
| 'Input'
| 'InputNumber'
| 'InputPassword'
| 'Mentions'
| 'PrimaryButton'
| 'Radio'
| 'RadioGroup'
| 'RangePicker'
| 'Rate'
| 'Select'
| 'Space'
| 'Switch'
| 'Textarea'
| 'TimePicker'
| 'TreeSelect'
| 'Upload'
| BaseFormComponentType;
/**
* 与 {@link ComponentType} 中注册的组件名一一对应,便于 Schema 上 `component` + `componentProps` 联动提示
*/
export interface ComponentPropsMap {
ApiCascader: ApiComponentSharedProps & CascaderProps;
ApiSelect: ApiComponentSharedProps & SelectProps;
ApiTreeSelect: ApiComponentSharedProps & TreeSelectProps;
AutoComplete: AutoCompleteProps;
Cascader: CascaderProps;
Checkbox: CheckboxProps;
CheckboxGroup: CheckboxGroupProps;
DatePicker: DatePickerProps;
DefaultButton: ButtonProps;
Divider: DividerProps;
IconPicker: IconPickerProps;
Input: InputProps;
InputNumber: InputNumberProps;
InputPassword: InputProps;
Mentions: MentionsProps;
PrimaryButton: ButtonProps;
Radio: RadioProps;
RadioGroup: RadioGroupProps;
RangePicker: RangePickerProps;
Rate: RateProps;
Select: SelectProps;
Space: SpaceProps;
Switch: SwitchProps;
Textarea: TextAreaProps;
TimePicker: TimePickerProps;
TreeSelect: TreeSelectProps;
Upload: AdapterUploadProps;
}
async function initComponentAdapter() {
const components: Partial<Record<ComponentType, Component>> = {
// 如果你的组件体积比较大,可以使用异步加载
// Button: () =>
// import('xxx').then((res) => res.Button),
ApiCascader: withDefaultPlaceholder(ApiComponent, 'select', {
component: Cascader,
fieldNames: { label: 'label', value: 'value', children: 'children' },
loadingSlot: 'suffixIcon',
modelPropName: 'value',
visibleEvent: 'onVisibleChange',
}),
ApiSelect: withDefaultPlaceholder(ApiComponent, 'select', {
component: Select,
loadingSlot: 'suffixIcon',
modelPropName: 'value',
visibleEvent: 'onVisibleChange',
}),
ApiTreeSelect: withDefaultPlaceholder(ApiComponent, 'select', {
component: TreeSelect,
fieldNames: { label: 'label', value: 'value', children: 'children' },
loadingSlot: 'suffixIcon',
modelPropName: 'value',
optionsPropName: 'treeData',
visibleEvent: 'onVisibleChange',
}),
AutoComplete,
Cascader,
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', {
style: { width: '100%' },
}),
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 };
@@ -0,0 +1,68 @@
import type {
VbenFormProps as FormProps,
VbenFormSchema as FormSchema,
FormValues,
} from '@vben/common-ui';
import type { ComponentPropsMap, 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',
},
},
rules: {
// 输入项目必填国际化适配
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;
},
},
});
}
function useVbenForm<
TFormValues extends FormValues = FormValues,
TSubmitValues extends FormValues = TFormValues,
>(
options: FormProps<
ComponentType,
ComponentPropsMap,
TFormValues,
TSubmitValues
>,
) {
return useForm<TFormValues, ComponentType, ComponentPropsMap, TSubmitValues>(
options,
);
}
export { initSetupVbenForm, useVbenForm, z };
export type VbenFormSchema<TValues extends FormValues = FormValues> =
FormSchema<ComponentType, ComponentPropsMap, TValues>;
export type VbenFormProps<
TFormValues extends FormValues = FormValues,
TSubmitValues extends FormValues = TFormValues,
> = FormProps<ComponentType, ComponentPropsMap, TFormValues, TSubmitValues>;
@@ -0,0 +1,93 @@
import type { FormValues } from '@vben/common-ui';
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import type { ComponentPropsMap, ComponentType } from './component';
import { h } from 'vue';
import {
setupVbenVxeTable,
useVbenVxeGrid as useGrid,
} 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 { props } = renderOpts;
const { column, row } = params;
return h(Image, { src: row[column.field], ...props });
},
});
// 表格配置项可以用 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 const useVbenVxeGrid = <
T extends Record<string, any>,
TFormValues extends FormValues = FormValues,
TSubmitValues extends FormValues = TFormValues,
>(
...rest: Parameters<
typeof useGrid<
T,
ComponentType,
ComponentPropsMap,
TFormValues,
TSubmitValues
>
>
) =>
useGrid<T, ComponentType, ComponentPropsMap, TFormValues, TSubmitValues>(
...rest,
);
export type * from '@vben/plugins/vxe-table';
+261
View File
@@ -0,0 +1,261 @@
import { requestClient } from '#/api/request';
export interface ArchiveStats {
conversations: number;
exports: number;
imports: number;
last_message_at: string;
media: number;
media_failed: number;
media_ready: number;
messages: number;
people: number;
}
export interface ArchiveConversation {
conversation_type: string;
external_id: string;
id: string;
last_content: string;
last_message_at: string;
message_count: number;
name: string;
source_account: string;
status: string;
}
export interface ArchiveMessage {
attachment_count: number;
attachments: ArchiveAttachment[];
content: string;
direction: string;
id: string;
message_type: string;
sender_name: string;
sender_person_id: null | string;
sent_at: string;
sequence_no: null | number;
source_message_id: string;
status: string;
}
export interface ArchiveAttachment {
attachment_index: number;
attachment_role: string;
checksum?: string;
id: string;
match_confidence: number;
match_method: string;
media_type: 'audio' | 'file' | 'image' | 'video';
mime_type: string;
original_filename: string;
sha256: string;
size_bytes: number;
status: string;
}
export interface ArchiveMediaAccess {
expires_in: number;
id: string;
url: string;
}
export interface ArchiveStorage {
bucket: string;
custom_domain: string;
enabled: boolean;
encryption_mode: string;
export_prefix: string;
media_prefix: string;
region: string;
secret_id_masked: string;
secret_id_present: boolean;
secret_key_masked: string;
secret_key_present: boolean;
updated_at: string;
}
export interface ArchiveMedia {
bucket: string;
created_at: string;
encryption_mode: string;
id: string;
last_error: string;
media_type: string;
mime_type: string;
object_key: string;
original_filename: string;
region: string;
sha256: string;
size_bytes: number;
status: string;
verified_at: string;
version_id: string;
}
export interface ArchivePerson {
conversation_count: number;
created_at: string;
display_name: string;
id: string;
identity_count: number;
message_count: number;
real_name: string;
updated_at: string;
}
export interface ArchiveIdentity {
created_at: string;
external_id: string;
id: string;
identity_type: string;
scope_id: string;
source: string;
updated_at: string;
verified: boolean;
}
export interface ArchivePersonDetail {
display_name: string;
id: string;
identities: ArchiveIdentity[];
real_name: string;
}
export interface ArchiveExportFile {
file_name: string;
format: string;
id: string;
sha256: string;
size_bytes: number;
storage_status: string;
}
export interface ArchiveExportJob {
completed_at: string;
created_at: string;
cutoff_at: string;
error_message: string;
files: ArchiveExportFile[];
filters: Record<string, unknown>;
formats: string[];
id: string;
progress: number;
started_at: string;
status: 'completed' | 'failed' | 'queued' | 'running';
total_rows: number;
}
function query(values: Record<string, number | string>) {
const params = new URLSearchParams();
Object.entries(values).forEach(([key, value]) => {
if (value !== '') params.set(key, String(value));
});
const text = params.toString();
return text ? `?${text}` : '';
}
export function fetchArchiveStats() {
return requestClient.get<ArchiveStats>('/archive/stats');
}
export function fetchArchiveConversations(limit = 50, cursor = '') {
return requestClient.get<{
has_more: boolean;
items: ArchiveConversation[];
next_cursor: string;
}>(`/archive/conversations${query({ cursor, limit })}`);
}
export function fetchArchiveMessages(
conversationId: string,
limit = 100,
cursor = '',
) {
return requestClient.get<{
has_more: boolean;
items: ArchiveMessage[];
next_cursor: string;
}>(
`/archive/conversations/${encodeURIComponent(conversationId)}/messages${query({ cursor, limit })}`,
);
}
export function fetchArchiveMediaAccessUrls(mediaIds: string[], expires = 900) {
return requestClient.post<{ items: ArchiveMediaAccess[] }>(
'/archive/media/access-urls',
{ expires, media_ids: mediaIds },
);
}
export function fetchArchiveStorage() {
return requestClient.get<{ storage: ArchiveStorage }>('/archive/storage');
}
export function saveArchiveStorage(data: Partial<ArchiveStorage> & {
secret_id?: string;
secret_key?: string;
}) {
return requestClient.put<{ storage: ArchiveStorage }>(
'/archive/storage',
data,
);
}
export function testArchiveStorage() {
return requestClient.post<{
result: { bucket: string; ok: boolean; region: string; request_id: string };
}>('/archive/storage/test');
}
export function fetchArchiveMedia(limit = 100) {
return requestClient.get<{ items: ArchiveMedia[] }>(
`/archive/media${query({ limit })}`,
);
}
export function fetchArchivePeople(limit = 100, keyword = '') {
return requestClient.get<{ items: ArchivePerson[] }>(
`/archive/people${query({ keyword, limit })}`,
);
}
export function fetchArchivePerson(personId: string) {
return requestClient.get<{ person: ArchivePersonDetail }>(
`/archive/people/${encodeURIComponent(personId)}`,
);
}
export function bindArchiveIdentity(
personId: string,
data: {
external_id: string;
identity_type: string;
scope_id: string;
verified: boolean;
},
) {
return requestClient.post<{ person: ArchivePersonDetail }>(
`/archive/people/${encodeURIComponent(personId)}/identities`,
data,
);
}
export function createArchiveExport(data: {
filters: Record<string, unknown>;
formats: string[];
}) {
return requestClient.post<{ job: ArchiveExportJob }>('/archive/exports', data);
}
export function fetchArchiveExports(limit = 100) {
return requestClient.get<{ jobs: ArchiveExportJob[] }>(
`/archive/exports${query({ limit })}`,
);
}
export function downloadArchiveExport(fileId: string) {
return requestClient.download<Blob>(
`/archive/export-files/${encodeURIComponent(fileId)}/download`,
);
}
+330
View File
@@ -0,0 +1,330 @@
/**
* 业务接口:模型清单、角色权限、用户、调用统计。
*
* 全部对 `admin_api.py` 的 /api/v2/*。这一层只负责搬数据,权限判定在后端——
* 前端隐藏按钮只是体验,后端不拦就等于没有权限系统。
*/
import { requestClient } from '#/api/request';
export interface ModelProvider {
api_key?: string;
api_key_masked?: string;
base_url: string;
capabilities: string;
enabled: boolean;
/** 后端算好的实际请求地址(只读) */
endpoint?: string;
/** auto = 按接口类型补全路径;exact = 地址原样使用 */
endpoint_mode: string;
health: string;
id: string;
kind: string;
max_inflight: number;
max_tokens: number;
model: string;
name: string;
rpm_limit: number;
temperature: number;
timeout_ms: number;
}
export interface ModelPlan {
answer_ids: string;
fallback_ids: string;
judge_id: string;
judge_mode: string;
version: number;
vision_id: string;
}
export interface RoleItem {
builtin: boolean;
code: string;
name: string;
permissions: string[];
user_count: number;
}
export interface PermissionItem {
code: string;
group_name: string;
name: string;
}
export interface UserItem {
active: boolean;
created_at: string;
id: number;
must_change_password: boolean;
role: string;
username: string;
}
export interface CallStats {
avg_ms: number;
avg_score: number;
chosen: { count: number; provider: string }[];
judged: number;
max_ms: number;
risk: Record<string, number>;
score_buckets: { count: number; range: string }[];
since: string;
total: number;
}
/** 单次调用的候选出口——`model_calls.candidates_json` 解出来的一项。 */
export interface ModelCallCandidate {
error?: string;
latency_ms?: number;
provider: string;
text: string;
}
/** 单条调用留痕:客户说了什么、模型答了什么、有没有被拦下来。 */
export interface ModelCallLogItem {
candidates: ModelCallCandidate[];
chosen: string;
created_at: string;
customer_text: string;
device_id: string;
id: number;
judge_mode: string;
judge_risk: string;
judge_score: number;
judge_winner: string;
/** chat = 回客户的话;guard = 界面识别之类的内部判断 */
purpose: string;
reply_text: string;
review_reason: string;
roles_version: number;
task_id: string;
total_ms: number;
}
// ── 模型 ─────────────────────────────────────────────────────────────────────
export function fetchModels() {
return requestClient.get<{
judge_modes: string[];
kinds: string[];
models: ModelProvider[];
roles: ModelPlan;
}>('/models');
}
export function saveModel(data: Partial<ModelProvider>) {
return requestClient.post<{ models: ModelProvider[] }>('/models', data);
}
export function deleteModel(id: string) {
return requestClient.delete<{ models: ModelProvider[] }>(
`/models/${encodeURIComponent(id)}`,
);
}
export function saveModelPlan(data: Partial<ModelPlan>) {
return requestClient.post<{ roles: ModelPlan; version: number }>(
'/models/plan',
data,
);
}
// ── 角色与权限 ───────────────────────────────────────────────────────────────
export function fetchRoles() {
return requestClient.get<{ roles: RoleItem[] }>('/roles');
}
export function fetchPermissions() {
return requestClient.get<{ permissions: PermissionItem[] }>('/permissions');
}
export function saveRole(data: {
code: string;
name: string;
permissions: string[];
}) {
return requestClient.post<{ roles: RoleItem[] }>('/roles', data);
}
export function deleteRole(code: string) {
return requestClient.delete<{ roles: RoleItem[] }>(
`/roles/${encodeURIComponent(code)}`,
);
}
// ── 用户 ─────────────────────────────────────────────────────────────────────
export function fetchUsers() {
return requestClient.get<{ users: UserItem[] }>('/users');
}
export function createUser(data: {
password: string;
role: string;
username: string;
}) {
return requestClient.post<{ users: UserItem[] }>('/users', data);
}
export function updateUser(
id: number,
data: { active: boolean; role: string },
) {
return requestClient.put<{ users: UserItem[] }>(`/users/${id}`, data);
}
// ── 运营 ─────────────────────────────────────────────────────────────────────
export function fetchCallStats(days = 7) {
return requestClient.get<CallStats>(`/stats/model-calls?days=${days}`);
}
export function fetchCallLog(params: {
days?: number;
limit?: number;
offset?: number;
/** chat = 回客户的话(默认);guard = 界面识别;'' = 全都要 */
purpose?: string;
q?: string;
}) {
const query = new URLSearchParams();
query.set('days', String(params.days ?? 7));
query.set('limit', String(params.limit ?? 50));
query.set('offset', String(params.offset ?? 0));
query.set('purpose', params.purpose ?? 'chat');
if (params.q) query.set('q', params.q);
return requestClient.get<{ items: ModelCallLogItem[]; total: number }>(
`/stats/model-calls/log?${query.toString()}`,
);
}
export function fetchAudit(limit = 200) {
return requestClient.get<{
entries: {
action: string;
created_at: string;
detail: string;
id: number;
ip_address: string;
username: null | string;
}[];
}>(`/audit?limit=${limit}`);
}
export function changeMyPassword(data: {
current_password: string;
new_password: string;
}) {
return requestClient.post('/me/password', data);
}
// ── 桌面端配置 ───────────────────────────────────────────────────────────────
/**
* 下发给桌面客户端的配置。
*
* 字段名是大写下划线,和老网页后台的表单完全一致——两边共用后端同一个校验函数,
* 不会出现"这边填得进、那边填不进"。
*
* 这里**没有模型连接参数**:服务类型、API 地址、密钥、模型名、温度、tokens、
* 超时全部搬到了「AI 模型 → 模型清单 / 角色编排」。桌面端的模型调用走网关,
* 密钥不出后端。
*/
export interface DesktopConfig {
AI_AGENT_NAME: string;
AI_CONTEXT_ENABLED: boolean;
AI_CONTEXT_MAX_ROUNDS: number;
AI_COUNTER_INSULT_ENABLED: boolean;
AI_DEVELOPMENT_MODE: boolean;
AI_ENABLED: boolean;
AI_HOSPITAL_NAME: string;
AI_MCP_ENABLED: boolean;
AI_MCP_MAX_ROUNDS: number;
AI_MCP_SERVERS: unknown[];
AI_UI_GUARD_ENABLED: boolean;
AI_USE_VISION: boolean;
/** 留空 = 按后台自己的地址自动推算 */
AI_GATEWAY_URL: string;
AI_REVIEW_RULES: ReviewRule[];
}
/**
* 选择性审核规则:命中任意一条,这一条回复才会停下来等人工确认,其余照常
* 自动发送。`id` 可以留空——留空由后端根据 label 自动生成并去重。
*/
export interface ReviewRule {
id?: string;
label: string;
keywords: string[];
enabled: boolean;
}
export interface ConfigEnvelope {
bool_keys: string[];
config: DesktopConfig;
/** 留空时实际会下发给客户端的网关地址 */
gateway_url_effective: string;
/** 模型设置搬去哪儿了,用来在页面上给人指路 */
model_settings_moved_to: string;
retired_keys: string[];
updated_at: string;
updated_by: string;
version: number;
}
export function fetchConfig() {
return requestClient.get<ConfigEnvelope>('/config');
}
export function saveConfig(data: DesktopConfig) {
return requestClient.post<{ version: number }>('/config', data);
}
// ── 桌面端版本升级 ───────────────────────────────────────────────────────────
export interface ReleasePolicy {
download_url: string;
force_upgrade: boolean;
latest_version: string;
release_notes: string;
updated_at: string;
updated_by: string;
}
export function fetchRelease() {
return requestClient.get<ReleasePolicy>('/release');
}
export function saveRelease(data: {
download_url: string;
force_upgrade: boolean;
latest_version: string;
release_notes: string;
}) {
return requestClient.post<ReleasePolicy>('/release', data);
}
// ── 模型连通性测试 ───────────────────────────────────────────────────────────
export interface ModelTestResult {
endpoint: string;
http_status: null | number;
latency_ms: number;
message: string;
model: string;
ok: boolean;
provider: string;
}
/**
* 给了 provider_id 就测清单里那一条,密钥由后端从库里解密取用——
* 前端从头到尾拿不到明文,也就不可能在网络上多走一趟。
*/
export function testModel(data: {
api_key?: string;
base_url?: string;
endpoint_mode?: string;
kind?: string;
model?: string;
provider_id?: string;
timeout_seconds?: number;
}) {
return requestClient.post<{ label: string; result: ModelTestResult }>(
'/models/test',
data,
);
}
@@ -0,0 +1,81 @@
import { requestClient } from '#/api/request';
/**
* 认证接口。
*
* vben 默认对接的是它自带的 mock 后端(`/auth/login` 返回 `{accessToken}`、
* `/auth/codes` 单独取权限码)。我们的后端是 `admin_api.py`,形状不一样,而且
* **登录时就把权限码一起返回了**——少一次往返,也避免"已登录但权限还没到"
* 这个中间态导致路由守卫误判。
*
* 适配放在这一层,不去改后端:接口契约不该被某一个客户端的习惯带偏。
*/
export namespace AuthApi {
export interface LoginParams {
password?: string;
username?: string;
}
export interface LoginResult {
accessToken: string;
}
/** 后端 /api/v2/auth/login 的真实返回 */
export interface BackendLoginResult {
access_token: string;
expires_in: number;
user: {
id: number;
must_change_password: boolean;
permissions: string[];
role: string;
username: string;
};
}
}
/** 登录后暂存的权限码。getAccessCodesApi 直接用它,省掉一次请求。 */
let cachedCodes: string[] = [];
export function takeCachedAccessCodes(): string[] {
return cachedCodes;
}
export async function loginApi(data: AuthApi.LoginParams) {
const resp = await requestClient.post<AuthApi.BackendLoginResult>(
'/auth/login',
{ username: data.username, password: data.password, device_name: 'web' },
);
cachedCodes = resp.user?.permissions ?? [];
return { accessToken: resp.access_token } as AuthApi.LoginResult;
}
/**
* 刷新 accessToken。
*
* 后端目前发的是 30 天有效的长令牌,没有 refresh token 机制,所以这里必须明确
* 失败——返回一个假 token 会让请求拦截器以为续期成功,接着用一个无效令牌无限
* 重试,表现是页面卡死而不是跳登录页。
*/
export async function refreshTokenApi(): Promise<never> {
throw new Error('后端未启用令牌续期,请重新登录');
}
export async function logoutApi() {
cachedCodes = [];
return requestClient.post('/auth/logout', {});
}
/**
* 取当前用户的权限码。
*
* 优先用登录时带回来的那份;刷新页面后缓存是空的,再去 /me 拿一次。
*/
export async function getAccessCodesApi(): Promise<string[]> {
if (cachedCodes.length > 0) {
return cachedCodes;
}
const me = await requestClient.get<{ permissions: string[] }>('/me');
cachedCodes = me.permissions ?? [];
return cachedCodes;
}
@@ -0,0 +1,3 @@
export * from './auth';
export * from './menu';
export * from './user';

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