feat: 账单导入与账单核对
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CI / CI OK (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Deploy Website on push / Rerun on failure (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CI / CI OK (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Deploy Website on push / Rerun on failure (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
This commit is contained in:
4
.browserslistrc
Normal file
4
.browserslistrc
Normal file
@@ -0,0 +1,4 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
||||
not ie 11
|
||||
5
.changeset/README.md
Normal file
5
.changeset/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with multi-package repos, or single-package repos to help you version and publish your code. You can find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||
18
.changeset/config.json
Normal file
18
.changeset/config.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
|
||||
"changelog": [
|
||||
"@changesets/changelog-github",
|
||||
{ "repo": "vbenjs/vue-vben-admin" }
|
||||
],
|
||||
"commit": false,
|
||||
"fixed": [["@vben-core/*", "@vben/*"]],
|
||||
"snapshot": {
|
||||
"prereleaseTemplate": "{tag}-{datetime}"
|
||||
},
|
||||
"privatePackages": { "version": true, "tag": true },
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
1
.commitlintrc.js
Normal file
1
.commitlintrc.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@vben/commitlint-config';
|
||||
18
.editorconfig
Normal file
18
.editorconfig
Normal file
@@ -0,0 +1,18 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset=utf-8
|
||||
end_of_line=lf
|
||||
insert_final_newline=true
|
||||
indent_style=space
|
||||
indent_size=2
|
||||
max_line_length = 100
|
||||
trim_trailing_whitespace = true
|
||||
quote_type = single
|
||||
|
||||
[*.{yml,yaml,json}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
11
.gitattributes
vendored
Normal file
11
.gitattributes
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# https://docs.github.com/cn/get-started/getting-started-with-git/configuring-git-to-handle-line-endings
|
||||
|
||||
# Automatically normalize line endings (to LF) for all text-based files.
|
||||
* text=auto eol=lf
|
||||
|
||||
# Declare files that will always have CRLF line endings on checkout.
|
||||
*.{cmd,[cC][mM][dD]} text eol=crlf
|
||||
*.{bat,[bB][aA][tT]} text eol=crlf
|
||||
|
||||
# Denote all files that are truly binary and should not be modified.
|
||||
*.{ico,png,jpg,jpeg,gif,webp,svg,woff,woff2} binary
|
||||
2
.gitconfig
Normal file
2
.gitconfig
Normal file
@@ -0,0 +1,2 @@
|
||||
[core]
|
||||
ignorecase = false
|
||||
14
.github/CODEOWNERS
vendored
Normal file
14
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# default onwer
|
||||
* anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
|
||||
|
||||
# vben core onwer
|
||||
/.github/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
|
||||
/.vscode/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
|
||||
/packages/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
|
||||
/packages/@core/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
|
||||
/internal/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
|
||||
/scripts/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
|
||||
|
||||
# vben team onwer
|
||||
apps/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com @vbenjs/team-v5 jinmao88@qq.com
|
||||
docs/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com @vbenjs/team-v5 jinmao88@qq.com
|
||||
74
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
74
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
name: 🐞 Bug Report
|
||||
description: Report an issue with Vben Admin to help us make it better.
|
||||
title: 'Bug: '
|
||||
labels: ['bug: pending triage']
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
- type: dropdown
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version of our software are you running?
|
||||
options:
|
||||
- Vben Admin V5
|
||||
- Vben Admin V2
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: bug-desc
|
||||
attributes:
|
||||
label: Describe the bug?
|
||||
description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, tell us in the description. Thanks!
|
||||
placeholder: Bug Description
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Reproduction
|
||||
description: Please provide a link to [StackBlitz](https://stackblitz.com/fork/github/vitest-dev/vitest/tree/main/examples/basic?initialPath=__vitest__/) (you can also use [examples](https://github.com/vitest-dev/vitest/tree/main/examples)) or a github repo that can reproduce the problem you ran into. A [minimal reproduction](https://stackoverflow.com/help/minimal-reproducible-example) is required unless you are absolutely sure that the issue is obvious and the provided information is enough to understand the problem. If a report is vague (e.g. just a generic error message) and has no reproduction, it will receive a "needs reproduction" label. If no reproduction is provided after 3 days, it will be auto-closed.
|
||||
placeholder: Reproduction
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: system-info
|
||||
attributes:
|
||||
label: System Info
|
||||
description: Output of `npx envinfo --system --npmPackages '{vue}' --binaries --browsers`
|
||||
render: shell
|
||||
placeholder: System, Binaries, Browsers
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Validations
|
||||
description: Before submitting the issue, please make sure you do the following
|
||||
# description: By submitting this issue, you agree to follow our [Code of Conduct](https://example.com).
|
||||
options:
|
||||
- label: Read the [docs](https://doc.vben.pro/)
|
||||
required: true
|
||||
- label: Ensure the code is up to date. (Some issues have been fixed in the latest version)
|
||||
required: true
|
||||
- label: I have searched the [existing issues](https://github.com/vbenjs/vue-vben-admin/issues) and checked that my issue does not duplicate any existing issues.
|
||||
required: true
|
||||
- label: Check that this is a concrete bug. For Q&A open a [GitHub Discussion](https://github.com/vbenjs/vue-vben-admin/discussions) or join our [Discord Chat Server](https://discord.gg/8GuAdwDhj6).
|
||||
required: true
|
||||
- label: The provided reproduction is a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) of the bug.
|
||||
required: true
|
||||
38
.github/ISSUE_TEMPLATE/docs.yml
vendored
Normal file
38
.github/ISSUE_TEMPLATE/docs.yml
vendored
Normal 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
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
70
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
name: ✨ New Feature Proposal
|
||||
description: Propose a new feature to be added to Vben Admin
|
||||
title: 'FEATURE: '
|
||||
labels: ['enhancement: pending triage']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for suggesting a feature for our project! Please fill out the information below to help us understand and implement your request!
|
||||
- type: dropdown
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version of our software are you running?
|
||||
options:
|
||||
- Vben Admin V5
|
||||
- Vben Admin V2
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: A detailed description of the feature request.
|
||||
placeholder: Please describe the feature you would like to see, and why it would be useful.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: proposed-solution
|
||||
attributes:
|
||||
label: Proposed Solution
|
||||
description: A clear and concise description of what you want to happen.
|
||||
placeholder: Describe the solution you'd like to see
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives Considered
|
||||
description: |
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
placeholder: Describe any alternative solutions or features you've considered
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
placeholder: Any additional information
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
id: checkboxes
|
||||
attributes:
|
||||
label: Validations
|
||||
description: Before submitting the issue, please make sure you do the following
|
||||
options:
|
||||
- label: Read the [docs](https://doc.vben.pro/)
|
||||
required: true
|
||||
- label: Ensure the code is up to date. (Some issues have been fixed in the latest version)
|
||||
required: true
|
||||
- label: I have searched the [existing issues](https://github.com/vbenjs/vue-vben-admin/issues) and checked that my issue does not duplicate any existing issues.
|
||||
required: true
|
||||
40
.github/actions/setup-node/action.yml
vendored
Normal file
40
.github/actions/setup-node/action.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: 'Setup Node'
|
||||
|
||||
description: 'Setup node and pnpm'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/cache@v4
|
||||
name: Setup pnpm cache
|
||||
if: ${{ github.ref_name == 'main' }}
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- uses: actions/cache/restore@v4
|
||||
if: ${{ github.ref_name != 'main' }}
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: pnpm install --frozen-lockfile
|
||||
89
.github/commit-convention.md
vendored
Normal file
89
.github/commit-convention.md
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
## Git Commit Message Convention
|
||||
|
||||
> This is adapted from [Angular's commit convention](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular).
|
||||
|
||||
#### TL;DR:
|
||||
|
||||
Messages must be matched by the following regex:
|
||||
|
||||
```js
|
||||
/^(revert: )?(feat|fix|docs|style|refactor|perf|test|workflow|build|ci|chore|types|wip): .{1,50}/;
|
||||
```
|
||||
|
||||
#### Examples
|
||||
|
||||
Appears under "Features" header, `dev` subheader:
|
||||
|
||||
```
|
||||
feat(dev): add 'comments' option
|
||||
```
|
||||
|
||||
Appears under "Bug Fixes" header, `dev` subheader, with a link to issue #28:
|
||||
|
||||
```
|
||||
fix(dev): fix dev error
|
||||
|
||||
close #28
|
||||
```
|
||||
|
||||
Appears under "Performance Improvements" header, and under "Breaking Changes" with the breaking change explanation:
|
||||
|
||||
```
|
||||
perf(build): remove 'foo' option
|
||||
|
||||
BREAKING CHANGE: The 'foo' option has been removed.
|
||||
```
|
||||
|
||||
The following commit and commit `667ecc1` do not appear in the changelog if they are under the same release. If not, the revert commit appears under the "Reverts" header.
|
||||
|
||||
```
|
||||
revert: feat(compiler): add 'comments' option
|
||||
|
||||
This reverts commit 667ecc1654a317a13331b17617d973392f415f02.
|
||||
```
|
||||
|
||||
### Full Message Format
|
||||
|
||||
A commit message consists of a **header**, **body** and **footer**. The header has a **type**, **scope** and **subject**:
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
<BLANK LINE>
|
||||
<body>
|
||||
<BLANK LINE>
|
||||
<footer>
|
||||
```
|
||||
|
||||
The **header** is mandatory and the **scope** of the header is optional.
|
||||
|
||||
### Revert
|
||||
|
||||
If the commit reverts a previous commit, it should begin with `revert: `, followed by the header of the reverted commit. In the body, it should say: `This reverts commit <hash>.`, where the hash is the SHA of the commit being reverted.
|
||||
|
||||
### Type
|
||||
|
||||
If the prefix is `feat`, `fix` or `perf`, it will appear in the changelog. However, if there is any [BREAKING CHANGE](#footer), the commit will always appear in the changelog.
|
||||
|
||||
Other prefixes are up to your discretion. Suggested prefixes are `docs`, `chore`, `style`, `refactor`, and `test` for non-changelog related tasks.
|
||||
|
||||
### Scope
|
||||
|
||||
The scope could be anything specifying the place of the commit change. For example `dev`, `build`, `workflow`, `cli` etc...
|
||||
|
||||
### Subject
|
||||
|
||||
The subject contains a succinct description of the change:
|
||||
|
||||
- use the imperative, present tense: "change" not "changed" nor "changes"
|
||||
- don't capitalize the first letter
|
||||
- no dot (.) at the end
|
||||
|
||||
### Body
|
||||
|
||||
Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes". The body should include the motivation for the change and contrast this with previous behavior.
|
||||
|
||||
### Footer
|
||||
|
||||
The footer should contain any information about **Breaking Changes** and is also the place to reference GitHub issues that this commit **Closes**.
|
||||
|
||||
**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this.
|
||||
39
.github/config.yml
vendored
Normal file
39
.github/config.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
# Prevent issues being created without using the template
|
||||
blank_issues_enabled: false
|
||||
checkIssueTemplate: true
|
||||
checkPullRequestTemplate: true
|
||||
|
||||
contact_links:
|
||||
- name: 💬 Discord Chat
|
||||
url: https://discord.gg/8GuAdwDhj6
|
||||
about: Ask questions and discuss with other Vben users in real time.
|
||||
|
||||
- name: ❓ Questions & Discussions
|
||||
url: https://github.com/@vbenjs/vue-vben-admin/discussions
|
||||
about: Use GitHub discussions for message-board style questions and discussions.
|
||||
|
||||
# Comment to be posted to on PRs from first time contributors in your repository
|
||||
newPRWelcomeComment: |
|
||||
💖 Thanks for opening this pull request! 💖
|
||||
Please be patient and we will get back to you as soon as we can.
|
||||
|
||||
# Comment to be posted to on pull requests merged by a first time user
|
||||
firstPRMergeComment: >
|
||||
Thanks for your contribution! 🎉🎉🎉
|
||||
|
||||
|
||||
# Comment to be posted to on first time issues
|
||||
newIssueWelcomeComment: >
|
||||
Thanks for opening your first issue! Be sure to follow the issue template and provide every bit of information to help the developers!
|
||||
|
||||
|
||||
# *OPTIONAL* default titles to check against for lack of descriptiveness
|
||||
# MUST BE ALL LOWERCASE
|
||||
requestInfoDefaultTitles:
|
||||
- update readme.md
|
||||
- updates
|
||||
|
||||
# *Required* Comment to reply with
|
||||
requestInfoReplyComment: >
|
||||
Thanks for filing this issue/PR! It would be much appreciated if you could provide us with more information so we can effectively analyze the situation in context.
|
||||
|
||||
40
.github/contributing.md
vendored
Normal file
40
.github/contributing.md
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
# Vben Admin Contributing Guide
|
||||
|
||||
Hi! We're really excited that you are interested in contributing to Vben Admin. Before submitting your contribution, please make sure to take a moment and read through the following guidelines:
|
||||
|
||||
- [Pull Request Guidelines](#pull-request-guidelines)
|
||||
|
||||
## Contributor Code of Conduct
|
||||
|
||||
As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
|
||||
|
||||
We are committed to making participation in this project a harassment-free experience for everyone, regardless of the level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion.
|
||||
|
||||
Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
- Checkout a topic branch from the relevant branch, e.g. main, and merge back against that branch.
|
||||
|
||||
- If adding a new feature:
|
||||
- Provide a convincing reason to add this feature. Ideally, you should open a suggestion issue first and have it approved before working on it.
|
||||
|
||||
- If fixing bug:
|
||||
- Provide a detailed description of the bug in the PR. Live demo preferred.
|
||||
|
||||
- It's OK to have multiple small commits as you work on the PR - GitHub can automatically squash them before merging.
|
||||
|
||||
## Development Setup
|
||||
|
||||
You will need [pnpm](https://pnpm.io/)
|
||||
|
||||
After cloning the repo, run:
|
||||
|
||||
```bash
|
||||
# install the dependencies of the project
|
||||
$ pnpm install
|
||||
# start the project
|
||||
$ pnpm run dev
|
||||
```
|
||||
17
.github/dependabot.yml
vendored
Normal file
17
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: daily
|
||||
groups:
|
||||
non-breaking-changes:
|
||||
update-types: [minor, patch]
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: weekly
|
||||
groups:
|
||||
non-breaking-changes:
|
||||
update-types: [minor, patch]
|
||||
33
.github/pull_request_template.md
vendored
Normal file
33
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
## Description
|
||||
|
||||
<!-- Please describe the change as necessary. If it's a feature or enhancement please be as detailed as possible. If it's a bug fix, please link the issue that it fixes or describe the bug in as much detail.
|
||||
|
||||
-->
|
||||
|
||||
<!-- You can also add additional context here -->
|
||||
|
||||
## Type of change
|
||||
|
||||
Please delete options that are not relevant.
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] This change requires a documentation update
|
||||
- [ ] Please, don't make changes to `pnpm-lock.yaml` unless you introduce a new test example.
|
||||
|
||||
## Checklist
|
||||
|
||||
> ℹ️ Check all checkboxes - this will indicate that you have done everything in accordance with the rules in [CONTRIBUTING](contributing.md).
|
||||
|
||||
- [ ] If you introduce new functionality, document it. You can run documentation with `pnpm run docs:dev` command.
|
||||
- [ ] Run the tests with `pnpm test`.
|
||||
- [ ] Changes in changelog are generated from PR name. Please, make sure that it explains your changes in an understandable manner. Please, prefix changeset messages with `feat:`, `fix:`, `perf:`, `docs:`, or `chore:`.
|
||||
- [ ] My code follows the style guidelines of this project
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||
- [ ] I have made corresponding changes to the documentation
|
||||
- [ ] My changes generate no new warnings
|
||||
- [ ] I have added tests that prove my fix is effective or that my feature works
|
||||
- [ ] New and existing unit tests pass locally with my changes
|
||||
- [ ] Any dependent changes have been merged and published in downstream modules
|
||||
61
.github/release-drafter.yml
vendored
Normal file
61
.github/release-drafter.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
name-template: 'v$RESOLVED_VERSION'
|
||||
tag-template: 'v$RESOLVED_VERSION'
|
||||
version-template: $MAJOR.$MINOR.$PATCH
|
||||
change-template: '* $TITLE (#$NUMBER) @$AUTHOR'
|
||||
template: |
|
||||
# What's Changed
|
||||
|
||||
$CHANGES
|
||||
|
||||
**Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION
|
||||
|
||||
categories:
|
||||
- title: '🚀 Features'
|
||||
labels:
|
||||
- 'feature'
|
||||
- title: '🐞 Bug Fixes'
|
||||
labels:
|
||||
- 'bug'
|
||||
- title: '📈 Performance & Enhancement'
|
||||
labels:
|
||||
- 'perf'
|
||||
- 'enhancement'
|
||||
- title: 📝 Documentation
|
||||
labels:
|
||||
- 'documentation'
|
||||
- title: 👻 Maintenance
|
||||
labels:
|
||||
- 'chore'
|
||||
- 'dependencies'
|
||||
# collapse-after: 12
|
||||
- title: 🚦 Tests
|
||||
labels:
|
||||
- 'tests'
|
||||
- title: 'Breaking'
|
||||
label: 'breaking'
|
||||
|
||||
version-resolver:
|
||||
major:
|
||||
labels:
|
||||
- 'major'
|
||||
- 'breaking'
|
||||
minor:
|
||||
labels:
|
||||
- 'minor'
|
||||
patch:
|
||||
labels:
|
||||
- 'feature'
|
||||
- 'patch'
|
||||
- 'bug'
|
||||
- 'maintenance'
|
||||
- 'docs'
|
||||
- 'dependencies'
|
||||
- 'security'
|
||||
|
||||
exclude-labels:
|
||||
- 'skip-changelog'
|
||||
- 'no-changelog'
|
||||
- 'changelog'
|
||||
- 'bump versions'
|
||||
- 'reverted'
|
||||
- 'invalid'
|
||||
13
.github/semantic.yml
vendored
Normal file
13
.github/semantic.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
titleAndCommits: true
|
||||
types:
|
||||
- feat
|
||||
- fix
|
||||
- docs
|
||||
- chore
|
||||
- style
|
||||
- refactor
|
||||
- perf
|
||||
- test
|
||||
- build
|
||||
- ci
|
||||
- revert
|
||||
48
.github/workflows/build.yml
vendored
Normal file
48
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
# name: Dependabot post-update
|
||||
name: Build detection
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
HUSKY: '0'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
post-update:
|
||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||
# if: ${{ github.actor == 'dependabot[bot]' }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
# - macos-latest
|
||||
- windows-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Checkout out pull request
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh pr checkout ${{ github.event.pull_request.number }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
pnpm run build
|
||||
42
.github/workflows/changeset-version.yml
vendored
Normal file
42
.github/workflows/changeset-version.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
# https://github.com/changesets/action
|
||||
name: Changeset version
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
|
||||
env:
|
||||
CI: true
|
||||
|
||||
jobs:
|
||||
version:
|
||||
if: (github.event.pull_request.merged || github.event_name == 'workflow_dispatch') && github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]') && github.repository == 'vbenjs/vue-vben-admin'
|
||||
# if: github.repository == 'vbenjs/vue-vben-admin'
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
|
||||
- name: Create Release Pull Request
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
version: pnpm run version
|
||||
commit: 'chore: bump versions'
|
||||
title: 'chore: bump versions'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
125
.github/workflows/ci.yml
vendored
Normal file
125
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- 'releases/*'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
CI: true
|
||||
TZ: Asia/Shanghai
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
# - macos-latest
|
||||
- windows-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
|
||||
# - name: Check Git version
|
||||
# run: git --version
|
||||
|
||||
# - name: Setup mock Git user
|
||||
# run: git config --global user.email "you@example.com" && git config --global user.name "Your Name"
|
||||
|
||||
- name: Vitest tests
|
||||
run: pnpm run test:unit
|
||||
|
||||
# - name: Upload coverage
|
||||
# uses: codecov/codecov-action@v4
|
||||
# with:
|
||||
# token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
# - macos-latest
|
||||
- windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
|
||||
- name: Lint
|
||||
run: pnpm run lint
|
||||
|
||||
check:
|
||||
name: Check
|
||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
# - macos-latest
|
||||
- windows-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm check:type
|
||||
|
||||
# From https://github.com/rhysd/actionlint/blob/main/docs/usage.md#use-actionlint-on-github-actions
|
||||
- name: Check workflow files
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
|
||||
./actionlint -color -shellcheck=""
|
||||
|
||||
ci-ok:
|
||||
name: CI OK
|
||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test, check, lint]
|
||||
env:
|
||||
FAILURE: ${{ contains(join(needs.*.result, ','), 'failure') }}
|
||||
steps:
|
||||
- name: Check for failure
|
||||
run: |
|
||||
echo $FAILURE
|
||||
if [ "$FAILURE" = "false" ]; then
|
||||
exit 0
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
94
.github/workflows/codeql.yml
vendored
Normal file
94
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: 'CodeQL'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['main']
|
||||
pull_request:
|
||||
branches: ['main']
|
||||
schedule:
|
||||
- cron: '35 0 * * 0'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||
# Runner size impacts CodeQL analysis time. To learn more, please see:
|
||||
# - https://gh.io/recommended-hardware-resources-for-running-codeql
|
||||
# - https://gh.io/supported-runners-and-hardware-resources
|
||||
# - https://gh.io/using-larger-runners (GitHub.com only)
|
||||
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
|
||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
|
||||
permissions:
|
||||
# required for all workflows
|
||||
security-events: write
|
||||
|
||||
# required to fetch internal or private CodeQL packs
|
||||
packages: read
|
||||
|
||||
# only required for workflows in private repositories
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: javascript-typescript
|
||||
build-mode: none
|
||||
# CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
|
||||
# Use `c-cpp` to analyze code written in C, C++ or both
|
||||
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
|
||||
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
|
||||
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
|
||||
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
|
||||
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
|
||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
# If the analyze step fails for one of the languages you are analyzing with
|
||||
# "We were unable to automatically build your code", modify the matrix above
|
||||
# to set the build mode to "manual" for that language. Then modify this step
|
||||
# to build your code.
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
- if: matrix.build-mode == 'manual'
|
||||
shell: bash
|
||||
run: |
|
||||
echo 'If you are using a "manual" build mode for one or more of the' \
|
||||
'languages you are analyzing, replace this with the commands to build' \
|
||||
'your code, for example:'
|
||||
echo ' make bootstrap'
|
||||
echo ' make release'
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: '/language:${{matrix.language}}'
|
||||
172
.github/workflows/deploy.yml
vendored
Normal file
172
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,172 @@
|
||||
name: Deploy Website on push
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy-playground-ftp:
|
||||
name: Deploy Push Playground Ftp
|
||||
if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]') && github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Sed Config Base
|
||||
shell: bash
|
||||
run: |
|
||||
sed -i "s#VITE_COMPRESS\s*=.*#VITE_COMPRESS = gzip#g" ./playground/.env.production
|
||||
sed -i "s#VITE_PWA\s*=.*#VITE_PWA = true#g" ./playground/.env.production
|
||||
cat ./playground/.env.production
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
|
||||
- name: Build
|
||||
run: pnpm build:play
|
||||
|
||||
- name: Sync Playground files
|
||||
uses: SamKirkland/FTP-Deploy-Action@v4.3.6
|
||||
with:
|
||||
server: ${{ secrets.PRO_FTP_HOST }}
|
||||
username: ${{ secrets.WEB_PLAYGROUND_FTP_ACCOUNT }}
|
||||
password: ${{ secrets.WEB_PLAYGROUND_FTP_PWSSWORD }}
|
||||
local-dir: ./playground/dist/
|
||||
|
||||
deploy-docs-ftp:
|
||||
name: Deploy Push Docs Ftp
|
||||
if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]') && github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
|
||||
- name: Build
|
||||
run: pnpm build:docs
|
||||
|
||||
- name: Sync Docs files
|
||||
uses: SamKirkland/FTP-Deploy-Action@v4.3.6
|
||||
with:
|
||||
server: ${{ secrets.PRO_FTP_HOST }}
|
||||
username: ${{ secrets.WEBSITE_FTP_ACCOUNT }}
|
||||
password: ${{ secrets.WEBSITE_FTP_PASSWORD }}
|
||||
local-dir: ./docs/.vitepress/dist/
|
||||
|
||||
deploy-antd-ftp:
|
||||
name: Deploy Push Antd Ftp
|
||||
if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]') && github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Sed Config Base
|
||||
shell: bash
|
||||
run: |
|
||||
sed -i "s#VITE_COMPRESS\s*=.*#VITE_COMPRESS = gzip#g" ./apps/web-antd/.env.production
|
||||
sed -i "s#VITE_PWA\s*=.*#VITE_PWA = true#g" ./apps/web-antd/.env.production
|
||||
cat ./apps/web-antd/.env.production
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build:antd
|
||||
|
||||
- name: Sync files
|
||||
uses: SamKirkland/FTP-Deploy-Action@v4.3.6
|
||||
with:
|
||||
server: ${{ secrets.PRO_FTP_HOST }}
|
||||
username: ${{ secrets.WEB_ANTD_FTP_ACCOUNT }}
|
||||
password: ${{ secrets.WEB_ANTD_FTP_PASSWORD }}
|
||||
local-dir: ./apps/web-antd/dist/
|
||||
|
||||
deploy-ele-ftp:
|
||||
name: Deploy Push Element Ftp
|
||||
if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]') && github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Sed Config Base
|
||||
shell: bash
|
||||
run: |
|
||||
sed -i "s#VITE_COMPRESS\s*=.*#VITE_COMPRESS = gzip#g" ./apps/web-ele/.env.production
|
||||
sed -i "s#VITE_PWA\s*=.*#VITE_PWA = true#g" ./apps/web-ele/.env.production
|
||||
cat ./apps/web-ele/.env.production
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build:ele
|
||||
|
||||
- name: Sync files
|
||||
uses: SamKirkland/FTP-Deploy-Action@v4.3.6
|
||||
with:
|
||||
server: ${{ secrets.PRO_FTP_HOST }}
|
||||
username: ${{ secrets.WEB_ELE_FTP_ACCOUNT }}
|
||||
password: ${{ secrets.WEB_ELE_FTP_PASSWORD }}
|
||||
local-dir: ./apps/web-ele/dist/
|
||||
|
||||
deploy-naive-ftp:
|
||||
name: Deploy Push Naive Ftp
|
||||
if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]') && github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Sed Config Base
|
||||
shell: bash
|
||||
run: |
|
||||
sed -i "s#VITE_COMPRESS\s*=.*#VITE_COMPRESS = gzip#g" ./apps/web-naive/.env.production
|
||||
sed -i "s#VITE_PWA\s*=.*#VITE_PWA = true#g" ./apps/web-naive/.env.production
|
||||
cat ./apps/web-naive/.env.production
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build:naive
|
||||
|
||||
- name: Sync files
|
||||
uses: SamKirkland/FTP-Deploy-Action@v4.3.6
|
||||
with:
|
||||
server: ${{ secrets.PRO_FTP_HOST }}
|
||||
username: ${{ secrets.WEB_NAIVE_FTP_ACCOUNT }}
|
||||
password: ${{ secrets.WEB_NAIVE_FTP_PASSWORD }}
|
||||
local-dir: ./apps/web-naive/dist/
|
||||
|
||||
rerun-on-failure:
|
||||
name: Rerun on failure
|
||||
needs:
|
||||
- deploy-playground-ftp
|
||||
- deploy-docs-ftp
|
||||
- deploy-antd-ftp
|
||||
- deploy-ele-ftp
|
||||
- deploy-naive-ftp
|
||||
if: failure() && fromJSON(github.run_attempt) < 10
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Retry ${{ fromJSON(github.run_attempt) }} of 10
|
||||
env:
|
||||
GH_REPO: ${{ github.repository }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: gh workflow run rerun.yml -F run_id=${{ github.run_id }}
|
||||
25
.github/workflows/draft.yml
vendored
Normal file
25
.github/workflows/draft.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Release Drafter
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
update_release_draft:
|
||||
permissions:
|
||||
# write permission is required to create a github release
|
||||
contents: write
|
||||
# write permission is required for autolabeler
|
||||
# otherwise, read permission is required at least
|
||||
pull-requests: write
|
||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter@v6
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
31
.github/workflows/issue-close-require.yml
vendored
Normal file
31
.github/workflows/issue-close-require.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
# 每天零点运行一次,它会检查所有带有 "need reproduction" 标签的 Issues。如果这些 Issues 在过去的 3 天内没有任何活动,它们将会被自动关闭。这有助于保持 Issue 列表的整洁,并且提醒用户在必要时提供更多的信息。
|
||||
name: Issue Close Require
|
||||
|
||||
# 触发条件:每天零点
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
close-issues:
|
||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# 关闭未活动的 Issues
|
||||
- name: Close Inactive Issues
|
||||
uses: actions/stale@v9
|
||||
with:
|
||||
days-before-stale: -1 # Issues and PR will never be flagged stale automatically.
|
||||
stale-issue-label: needs-reproduction # Label that flags an issue as stale.
|
||||
only-labels: needs-reproduction # Only process these issues
|
||||
days-before-issue-close: 3
|
||||
ignore-updates: true
|
||||
remove-stale-when-updated: false
|
||||
close-issue-message: This issue was closed because it was open for 3 days without a valid reproduction.
|
||||
close-issue-label: closed-by-action
|
||||
46
.github/workflows/issue-labeled.yml
vendored
Normal file
46
.github/workflows/issue-labeled.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Label Based Actions
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
# pull_request:
|
||||
# types: [labeled]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
reply-labeled:
|
||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: remove enhancement pending
|
||||
if: github.event.label.name == 'enhancement'
|
||||
uses: actions-cool/issues-helper@v3
|
||||
with:
|
||||
actions: 'remove-labels'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
labels: 'enhancement: pending triage'
|
||||
|
||||
- name: remove bug pending
|
||||
if: github.event.label.name == 'bug'
|
||||
uses: actions-cool/issues-helper@v3
|
||||
with:
|
||||
actions: 'remove-labels'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
labels: 'bug: pending triage'
|
||||
|
||||
- name: needs reproduction
|
||||
if: github.event.label.name == 'needs reproduction'
|
||||
uses: actions-cool/issues-helper@v3
|
||||
with:
|
||||
actions: 'create-comment, remove-labels'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: |
|
||||
Hello @${{ github.event.issue.user.login }}. Please provide the complete reproduction steps and code. Issues labeled by `needs reproduction` will be closed if no activities in 3 days.
|
||||
labels: 'bug: pending triage'
|
||||
24
.github/workflows/lock.yml
vendored
Normal file
24
.github/workflows/lock.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Lock Threads
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
action:
|
||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v5
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-inactive-days: '14'
|
||||
issue-lock-reason: ''
|
||||
pr-inactive-days: '30'
|
||||
pr-lock-reason: ''
|
||||
process-only: 'issues, prs'
|
||||
80
.github/workflows/release-tag.yml
vendored
Normal file
80
.github/workflows/release-tag.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
name: Create Release Tag
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||
|
||||
env:
|
||||
HUSKY: '0'
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Create Release
|
||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# - name: Checkout code
|
||||
# uses: actions/checkout@v4
|
||||
# with:
|
||||
# fetch-depth: 0
|
||||
|
||||
# - name: Install pnpm
|
||||
# uses: pnpm/action-setup@v4
|
||||
|
||||
# - name: Use Node.js ${{ matrix.node-version }}
|
||||
# uses: actions/setup-node@v4
|
||||
# with:
|
||||
# node-version: ${{ matrix.node-version }}
|
||||
# cache: "pnpm"
|
||||
|
||||
# - name: Install dependencies
|
||||
# run: pnpm install --frozen-lockfile
|
||||
|
||||
# - name: Test and Build
|
||||
# run: |
|
||||
# pnpm run test
|
||||
# pnpm run build
|
||||
|
||||
- name: version
|
||||
id: version
|
||||
run: |
|
||||
tag=${GITHUB_REF/refs\/tags\//}
|
||||
version=${tag#v}
|
||||
major=${version%%.*}
|
||||
echo "tag=${tag}" >> $GITHUB_OUTPUT
|
||||
echo "version=${version}" >> $GITHUB_OUTPUT
|
||||
echo "major=${major}" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: release-drafter/release-drafter@v6
|
||||
with:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
publish: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# - name: force update major tag
|
||||
# run: |
|
||||
# git tag v${{ steps.version.outputs.major }} ${{ steps.version.outputs.tag }} -f
|
||||
# git push origin refs/tags/v${{ steps.version.outputs.major }} -f
|
||||
|
||||
# - name: Create Release for Tag
|
||||
# id: release_tag
|
||||
# uses: ncipollo/release-action@v1
|
||||
# with:
|
||||
# token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# generateReleaseNotes: "true"
|
||||
# body: |
|
||||
# > Please refer to [CHANGELOG.md](https://github.com/vbenjs/vue-vben-admin/blob/main/CHANGELOG.md) for details.
|
||||
19
.github/workflows/rerun.yml
vendored
Normal file
19
.github/workflows/rerun.yml
vendored
Normal 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
.github/workflows/semantic-pull-request.yml
vendored
Normal file
41
.github/workflows/semantic-pull-request.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: Semantic Pull Request
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- synchronize
|
||||
|
||||
jobs:
|
||||
main:
|
||||
name: Semantic Pull Request
|
||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Validate PR title
|
||||
uses: amannn/action-semantic-pull-request@v5
|
||||
with:
|
||||
wip: true
|
||||
subjectPattern: ^(?![A-Z]).+$
|
||||
subjectPatternError: |
|
||||
The subject "{subject}" found in the pull request title "{title}"
|
||||
didn't match the configured pattern. Please ensure that the subject
|
||||
doesn't start with an uppercase character.
|
||||
requireScope: false
|
||||
types: |
|
||||
fix
|
||||
feat
|
||||
docs
|
||||
style
|
||||
refactor
|
||||
perf
|
||||
test
|
||||
build
|
||||
ci
|
||||
chore
|
||||
revert
|
||||
types
|
||||
release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
19
.github/workflows/stale.yml
vendored
Normal file
19
.github/workflows/stale.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: 'Close stale issues'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 1 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days'
|
||||
stale-pr-message: 'This PR is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days'
|
||||
exempt-issue-labels: 'bug,enhancement'
|
||||
days-before-stale: 60
|
||||
days-before-close: 7
|
||||
52
.gitignore
vendored
Normal file
52
.gitignore
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
dist.zip
|
||||
dist.tar
|
||||
dist.war
|
||||
.nitro
|
||||
.output
|
||||
*-dist.zip
|
||||
*-dist.tar
|
||||
*-dist.war
|
||||
coverage
|
||||
*.local
|
||||
**/.vitepress/cache
|
||||
.cache
|
||||
.turbo
|
||||
.temp
|
||||
dev-dist
|
||||
.stylelintcache
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
.VSCodeCounter
|
||||
**/backend-mock/data
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
.eslintcache
|
||||
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
vite.config.mts.*
|
||||
vite.config.mjs.*
|
||||
vite.config.js.*
|
||||
vite.config.ts.*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
# .vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.history
|
||||
.cursor
|
||||
6
.gitpod.yml
Normal file
6
.gitpod.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
ports:
|
||||
- port: 9000
|
||||
onOpen: open-preview
|
||||
tasks:
|
||||
- init: npm i -g corepack && pnpm install
|
||||
command: pnpm run dev:play
|
||||
1
.node-version
Normal file
1
.node-version
Normal file
@@ -0,0 +1 @@
|
||||
22.1.0
|
||||
13
.npmrc
Normal file
13
.npmrc
Normal file
@@ -0,0 +1,13 @@
|
||||
registry=https://registry.npmmirror.com
|
||||
public-hoist-pattern[]=lefthook
|
||||
public-hoist-pattern[]=eslint
|
||||
public-hoist-pattern[]=prettier
|
||||
public-hoist-pattern[]=prettier-plugin-tailwindcss
|
||||
public-hoist-pattern[]=stylelint
|
||||
public-hoist-pattern[]=*postcss*
|
||||
public-hoist-pattern[]=@commitlint/*
|
||||
public-hoist-pattern[]=czg
|
||||
|
||||
strict-peer-dependencies=false
|
||||
auto-install-peers=true
|
||||
dedupe-peer-dependents=true
|
||||
18
.prettierignore
Normal file
18
.prettierignore
Normal file
@@ -0,0 +1,18 @@
|
||||
dist
|
||||
dev-dist
|
||||
.local
|
||||
.output.js
|
||||
node_modules
|
||||
.nvmrc
|
||||
coverage
|
||||
CODEOWNERS
|
||||
.nitro
|
||||
.output
|
||||
|
||||
|
||||
**/*.svg
|
||||
**/*.sh
|
||||
|
||||
public
|
||||
.npmrc
|
||||
*-lock.yaml
|
||||
1
.prettierrc.mjs
Normal file
1
.prettierrc.mjs
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@vben/prettier-config';
|
||||
4
.stylelintignore
Normal file
4
.stylelintignore
Normal file
@@ -0,0 +1,4 @@
|
||||
dist
|
||||
public
|
||||
__tests__
|
||||
coverage
|
||||
2
.trae/rules/project_rules.md
Normal file
2
.trae/rules/project_rules.md
Normal file
@@ -0,0 +1,2 @@
|
||||
1. 这是一个企业级项目,结构复杂,你需要深度检测代码后才能理解其业务逻辑,不能简单的只根据当前文件的代码来理解。
|
||||
2. 代码修改完成后,需检查文件内引用是否正确
|
||||
30
.vscode/extensions.json
vendored
Normal file
30
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"recommendations": [
|
||||
// Vue 3 的语言支持
|
||||
"Vue.volar",
|
||||
// 将 ESLint JavaScript 集成到 VS Code 中。
|
||||
"dbaeumer.vscode-eslint",
|
||||
// Visual Studio Code 的官方 Stylelint 扩展
|
||||
"stylelint.vscode-stylelint",
|
||||
// 使用 Prettier 的代码格式化程序
|
||||
"esbenp.prettier-vscode",
|
||||
// 支持 dotenv 文件语法
|
||||
"mikestead.dotenv",
|
||||
// 源代码的拼写检查器
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
// Tailwind CSS 的官方 VS Code 插件
|
||||
"bradlc.vscode-tailwindcss",
|
||||
// iconify 图标插件
|
||||
"antfu.iconify",
|
||||
// i18n 插件
|
||||
"Lokalise.i18n-ally",
|
||||
// CSS 变量提示
|
||||
"vunguyentuan.vscode-css-variables",
|
||||
// 在 package.json 中显示 PNPM catalog 的版本
|
||||
"antfu.pnpm-catalog-lens"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
// 和 volar 冲突
|
||||
"octref.vetur"
|
||||
]
|
||||
}
|
||||
37
.vscode/global.code-snippets
vendored
Normal file
37
.vscode/global.code-snippets
vendored
Normal 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 })"],
|
||||
},
|
||||
}
|
||||
15
.vscode/launch.json
vendored
Normal file
15
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "chrome",
|
||||
"name": "finance admin dev",
|
||||
"request": "launch",
|
||||
"url": "http://localhost:9000",
|
||||
"env": { "NODE_ENV": "development" },
|
||||
"sourceMaps": true,
|
||||
"webRoot": "${workspaceFolder}/apps/finance"
|
||||
}
|
||||
]
|
||||
}
|
||||
241
.vscode/settings.json
vendored
Normal file
241
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,241 @@
|
||||
{
|
||||
"tailwindCSS.experimental.configFile": "internal/tailwind-config/src/index.ts",
|
||||
// workbench
|
||||
"workbench.list.smoothScrolling": true,
|
||||
"workbench.startupEditor": "newUntitledFile",
|
||||
"workbench.tree.indent": 10,
|
||||
"workbench.editor.highlightModifiedTabs": true,
|
||||
"workbench.editor.closeOnFileDelete": true,
|
||||
"workbench.editor.limit.enabled": true,
|
||||
"workbench.editor.limit.perEditorGroup": true,
|
||||
"workbench.editor.limit.value": 8,
|
||||
|
||||
// editor
|
||||
"editor.tabSize": 2,
|
||||
"editor.detectIndentation": false,
|
||||
"editor.cursorBlinking": "expand",
|
||||
"editor.largeFileOptimizations": true,
|
||||
"editor.accessibilitySupport": "off",
|
||||
"editor.cursorSmoothCaretAnimation": "on",
|
||||
"editor.guides.bracketPairs": "active",
|
||||
"editor.inlineSuggest.enabled": true,
|
||||
"editor.suggestSelection": "recentlyUsedByPrefix",
|
||||
"editor.acceptSuggestionOnEnter": "smart",
|
||||
"editor.suggest.snippetsPreventQuickSuggestions": false,
|
||||
"editor.stickyScroll.enabled": true,
|
||||
"editor.hover.sticky": true,
|
||||
"editor.suggest.insertMode": "replace",
|
||||
"editor.bracketPairColorization.enabled": true,
|
||||
"editor.autoClosingBrackets": "beforeWhitespace",
|
||||
"editor.autoClosingDelete": "always",
|
||||
"editor.autoClosingOvertype": "always",
|
||||
"editor.autoClosingQuotes": "beforeWhitespace",
|
||||
"editor.wordSeparators": "`~!@#%^&*()=+[{]}\\|;:'\",.<>/?",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.fixAll.stylelint": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[scss]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[markdown]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[vue]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
// extensions
|
||||
"extensions.ignoreRecommendations": true,
|
||||
|
||||
// terminal
|
||||
"terminal.integrated.cursorBlinking": true,
|
||||
"terminal.integrated.persistentSessionReviveProcess": "never",
|
||||
"terminal.integrated.tabs.enabled": true,
|
||||
"terminal.integrated.scrollback": 10000,
|
||||
"terminal.integrated.stickyScroll.enabled": true,
|
||||
|
||||
// files
|
||||
"files.eol": "\n",
|
||||
"files.insertFinalNewline": true,
|
||||
"files.simpleDialog.enable": true,
|
||||
"files.associations": {
|
||||
"*.ejs": "html",
|
||||
"*.art": "html",
|
||||
"**/tsconfig.json": "jsonc",
|
||||
"*.json": "jsonc",
|
||||
"package.json": "json"
|
||||
},
|
||||
|
||||
"files.exclude": {
|
||||
"**/.eslintcache": true,
|
||||
"**/bower_components": true,
|
||||
"**/.turbo": true,
|
||||
"**/.idea": true,
|
||||
"**/.vitepress": true,
|
||||
"**/tmp": true,
|
||||
"**/.git": true,
|
||||
"**/.svn": true,
|
||||
"**/.hg": true,
|
||||
"**/CVS": true,
|
||||
"**/.stylelintcache": true,
|
||||
"**/.DS_Store": true,
|
||||
"**/vite.config.mts.*": true,
|
||||
"**/tea.yaml": true
|
||||
},
|
||||
"files.watcherExclude": {
|
||||
"**/.git/objects/**": true,
|
||||
"**/.git/subtree-cache/**": true,
|
||||
"**/.vscode/**": true,
|
||||
"**/node_modules/**": true,
|
||||
"**/tmp/**": true,
|
||||
"**/bower_components/**": true,
|
||||
"**/dist/**": true,
|
||||
"**/yarn.lock": true
|
||||
},
|
||||
|
||||
"typescript.tsserver.exclude": ["**/node_modules", "**/dist", "**/.turbo"],
|
||||
|
||||
// search
|
||||
"search.searchEditor.singleClickBehaviour": "peekDefinition",
|
||||
"search.followSymlinks": false,
|
||||
// 在使用搜索功能时,将这些文件夹/文件排除在外
|
||||
"search.exclude": {
|
||||
"**/node_modules": true,
|
||||
"**/*.log": true,
|
||||
"**/*.log*": true,
|
||||
"**/bower_components": true,
|
||||
"**/dist": true,
|
||||
"**/elehukouben": true,
|
||||
"**/.git": true,
|
||||
"**/.github": true,
|
||||
"**/.gitignore": true,
|
||||
"**/.svn": true,
|
||||
"**/.DS_Store": true,
|
||||
"**/.vitepress/cache": true,
|
||||
"**/.idea": true,
|
||||
"**/.vscode": false,
|
||||
"**/.yarn": true,
|
||||
"**/tmp": true,
|
||||
"*.xml": true,
|
||||
"out": true,
|
||||
"dist": true,
|
||||
"node_modules": true,
|
||||
"CHANGELOG.md": true,
|
||||
"**/pnpm-lock.yaml": true,
|
||||
"**/yarn.lock": true
|
||||
},
|
||||
|
||||
"debug.onTaskErrors": "debugAnyway",
|
||||
"diffEditor.ignoreTrimWhitespace": false,
|
||||
"npm.packageManager": "pnpm",
|
||||
|
||||
"css.validate": false,
|
||||
"less.validate": false,
|
||||
"scss.validate": false,
|
||||
|
||||
// extension
|
||||
"emmet.showSuggestionsAsSnippets": true,
|
||||
"emmet.triggerExpansionOnTab": false,
|
||||
|
||||
"errorLens.enabledDiagnosticLevels": ["warning", "error"],
|
||||
"errorLens.excludeBySource": ["cSpell", "Grammarly", "eslint"],
|
||||
|
||||
"stylelint.enable": true,
|
||||
"stylelint.packageManager": "pnpm",
|
||||
"stylelint.validate": ["css", "less", "postcss", "scss", "vue"],
|
||||
"stylelint.customSyntax": "postcss-html",
|
||||
"stylelint.snippet": ["css", "less", "postcss", "scss", "vue"],
|
||||
|
||||
"typescript.inlayHints.enumMemberValues.enabled": true,
|
||||
"typescript.preferences.preferTypeOnlyAutoImports": true,
|
||||
"typescript.preferences.includePackageJsonAutoImports": "on",
|
||||
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"typescript",
|
||||
"javascriptreact",
|
||||
"typescriptreact",
|
||||
"vue",
|
||||
"html",
|
||||
"markdown",
|
||||
"json",
|
||||
"jsonc",
|
||||
"json5"
|
||||
],
|
||||
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
|
||||
],
|
||||
|
||||
"github.copilot.enable": {
|
||||
"*": true,
|
||||
"markdown": true,
|
||||
"plaintext": false,
|
||||
"yaml": false
|
||||
},
|
||||
|
||||
"cssVariables.lookupFiles": ["packages/core/base/design/src/**/*.css"],
|
||||
|
||||
"i18n-ally.localesPaths": [
|
||||
"packages/locales/src/langs",
|
||||
"playground/src/locales/langs",
|
||||
"apps/*/src/locales/langs"
|
||||
],
|
||||
"i18n-ally.pathMatcher": "{locale}/{namespace}.{ext}",
|
||||
"i18n-ally.enabledParsers": ["json"],
|
||||
"i18n-ally.sourceLanguage": "en",
|
||||
"i18n-ally.displayLanguage": "zh-CN",
|
||||
"i18n-ally.enabledFrameworks": ["vue", "react"],
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.sortKeys": true,
|
||||
"i18n-ally.namespace": true,
|
||||
|
||||
// 控制相关文件嵌套展示
|
||||
"explorer.fileNesting.enabled": true,
|
||||
"explorer.fileNesting.expand": false,
|
||||
"explorer.fileNesting.patterns": {
|
||||
"*.ts": "$(capture).test.ts, $(capture).test.tsx, $(capture).spec.ts, $(capture).spec.tsx, $(capture).d.ts",
|
||||
"*.tsx": "$(capture).test.ts, $(capture).test.tsx, $(capture).spec.ts, $(capture).spec.tsx,$(capture).d.ts",
|
||||
"*.env": "$(capture).env.*",
|
||||
"README.md": "README*,CHANGELOG*,LICENSE,CNAME",
|
||||
"package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,.gitattributes,.gitignore,.gitpod.yml,.npmrc,.browserslistrc,.node-version,.git*,.tazerc.json",
|
||||
"eslint.config.mjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json,lefthook.yml",
|
||||
"tailwind.config.mjs": "postcss.*"
|
||||
},
|
||||
"commentTranslate.hover.enabled": false,
|
||||
"commentTranslate.multiLineMerge": true,
|
||||
"vue.server.hybridMode": true,
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"oxc.enable": false,
|
||||
"cSpell.words": [
|
||||
"archiver",
|
||||
"axios",
|
||||
"dotenv",
|
||||
"isequal",
|
||||
"jspm",
|
||||
"napi",
|
||||
"nolebase",
|
||||
"rollup",
|
||||
"vitest"
|
||||
]
|
||||
}
|
||||
72
README.md
Normal file
72
README.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Finance Master
|
||||
|
||||
## 简介
|
||||
|
||||
财务系统前端项目
|
||||
|
||||
## 特性
|
||||
|
||||
- **最新技术栈**:使用 Vue3/vite 等前端前沿技术开发
|
||||
- **TypeScript**:应用程序级 JavaScript 的语言
|
||||
- **主题**:提供多套主题色彩,可配置自定义主题
|
||||
- **国际化**:内置完善的国际化方案
|
||||
- **权限**:内置完善的动态路由权限生成方案
|
||||
|
||||
## 预览
|
||||
|
||||
管理账号:admin/123456
|
||||
|
||||
## 安装使用
|
||||
|
||||
1. 获取项目代码
|
||||
|
||||
```bash
|
||||
git clone https://git.nuttyreading.com/zm/finance-master.git
|
||||
```
|
||||
|
||||
2. 安装依赖
|
||||
|
||||
```bash
|
||||
cd finance-master
|
||||
npm i -g corepack
|
||||
pnpm install
|
||||
```
|
||||
|
||||
3. 运行
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
4. 打包
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## Git 提交规范
|
||||
|
||||
参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 规范 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
|
||||
|
||||
- `feat` 增加新功能
|
||||
- `fix` 修复问题/BUG
|
||||
- `style` 代码风格相关无影响运行结果的
|
||||
- `perf` 优化/性能提升
|
||||
- `refactor` 重构
|
||||
- `revert` 撤销修改
|
||||
- `test` 测试相关
|
||||
- `docs` 文档/注释
|
||||
- `chore` 依赖更新/脚手架配置修改等
|
||||
- `ci` 持续集成
|
||||
- `types` 类型定义文件更改
|
||||
|
||||
## 浏览器支持
|
||||
|
||||
本地开发推荐使用 `Chrome 80+` 浏览器
|
||||
|
||||
支持现代浏览器,不支持 IE
|
||||
|
||||
|
||||
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
|
||||
| :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| last 2 versions | last 2 versions | last 2 versions | last 2 versions |
|
||||
8
apps/finance/.env
Normal file
8
apps/finance/.env
Normal file
@@ -0,0 +1,8 @@
|
||||
# 应用标题
|
||||
VITE_APP_TITLE=财务系统
|
||||
|
||||
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离
|
||||
VITE_APP_NAMESPACE=finance
|
||||
|
||||
# 对store进行加密的密钥,在将store持久化到localStorage时会使用该密钥进行加密
|
||||
VITE_APP_STORE_SECURE_KEY=finance
|
||||
7
apps/finance/.env.analyze
Normal file
7
apps/finance/.env.analyze
Normal file
@@ -0,0 +1,7 @@
|
||||
# public path
|
||||
VITE_BASE=/
|
||||
|
||||
# Basic interface address SPA
|
||||
VITE_GLOB_API_URL=/api
|
||||
|
||||
VITE_VISUALIZER=true
|
||||
16
apps/finance/.env.development
Normal file
16
apps/finance/.env.development
Normal file
@@ -0,0 +1,16 @@
|
||||
# 端口号
|
||||
VITE_PORT=9000
|
||||
|
||||
VITE_BASE=/
|
||||
|
||||
# 接口地址
|
||||
VITE_GLOB_API_URL=http://192.168.110.100:9000/finance
|
||||
|
||||
# 是否开启 Nitro Mock服务,true 为开启,false 为关闭
|
||||
VITE_NITRO_MOCK=false
|
||||
|
||||
# 是否打开 devtools,true 为打开,false 为关闭
|
||||
VITE_DEVTOOLS=false
|
||||
|
||||
# 是否注入全局loading
|
||||
VITE_INJECT_APP_LOADING=true
|
||||
19
apps/finance/.env.production
Normal file
19
apps/finance/.env.production
Normal file
@@ -0,0 +1,19 @@
|
||||
VITE_BASE=/
|
||||
|
||||
# 接口地址
|
||||
VITE_GLOB_API_URL=http://192.168.110.100:9000/finance
|
||||
|
||||
# 是否开启压缩,可以设置为 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
apps/finance/index.html
Normal file
35
apps/finance/index.html
Normal 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>
|
||||
47
apps/finance/package.json
Normal file
47
apps/finance/package.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "@zmzm/finance",
|
||||
"version": "0.0.1",
|
||||
"homepage": "",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://git.nuttyreading.com/zm/finance-master.git",
|
||||
"directory": "apps/finance"
|
||||
},
|
||||
"license": "MIT",
|
||||
"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-core/shadcn-ui": "workspace:*",
|
||||
"@vben-core/shared": "workspace:*",
|
||||
"@vben-core/typings": "workspace:*",
|
||||
"@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:"
|
||||
}
|
||||
}
|
||||
1
apps/finance/postcss.config.mjs
Normal file
1
apps/finance/postcss.config.mjs
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@vben/tailwind-config/postcss';
|
||||
383
apps/finance/src/adapter/component/index.ts
Normal file
383
apps/finance/src/adapter/component/index.ts
Normal file
@@ -0,0 +1,383 @@
|
||||
/**
|
||||
* 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
|
||||
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
|
||||
*/
|
||||
|
||||
import type { UploadChangeParam, UploadFile, UploadProps } from 'ant-design-vue';
|
||||
|
||||
import type { Component, Ref } from 'vue';
|
||||
|
||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { defineAsyncComponent, defineComponent, h, ref, render, unref, watch } from 'vue';
|
||||
|
||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { notification } from 'ant-design-vue';
|
||||
|
||||
const AutoComplete = defineAsyncComponent(() => import('ant-design-vue/es/auto-complete'));
|
||||
const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
|
||||
const Checkbox = defineAsyncComponent(() => import('ant-design-vue/es/checkbox'));
|
||||
const CheckboxGroup = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup),
|
||||
);
|
||||
const DatePicker = defineAsyncComponent(() => import('ant-design-vue/es/date-picker'));
|
||||
const Divider = defineAsyncComponent(() => import('ant-design-vue/es/divider'));
|
||||
const Input = defineAsyncComponent(() => import('ant-design-vue/es/input'));
|
||||
const InputNumber = defineAsyncComponent(() => import('ant-design-vue/es/input-number'));
|
||||
const InputPassword = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/input').then((res) => res.InputPassword),
|
||||
);
|
||||
const Mentions = defineAsyncComponent(() => import('ant-design-vue/es/mentions'));
|
||||
const Radio = defineAsyncComponent(() => import('ant-design-vue/es/radio'));
|
||||
const RadioGroup = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/radio').then((res) => res.RadioGroup),
|
||||
);
|
||||
const RangePicker = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/date-picker').then((res) => res.RangePicker),
|
||||
);
|
||||
const Rate = defineAsyncComponent(() => import('ant-design-vue/es/rate'));
|
||||
const Select = defineAsyncComponent(() => import('ant-design-vue/es/select'));
|
||||
const Space = defineAsyncComponent(() => import('ant-design-vue/es/space'));
|
||||
const Switch = defineAsyncComponent(() => import('ant-design-vue/es/switch'));
|
||||
const Textarea = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/input').then((res) => res.Textarea),
|
||||
);
|
||||
const TimePicker = defineAsyncComponent(() => import('ant-design-vue/es/time-picker'));
|
||||
const TreeSelect = defineAsyncComponent(() => import('ant-design-vue/es/tree-select'));
|
||||
const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));
|
||||
const Image = defineAsyncComponent(() => import('ant-design-vue/es/image'));
|
||||
const PreviewGroup = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/image').then((res) => res.ImagePreviewGroup),
|
||||
);
|
||||
|
||||
const withDefaultPlaceholder = <T extends Component>(
|
||||
component: T,
|
||||
type: 'input' | 'select',
|
||||
componentProps: Recordable<any> = {},
|
||||
) => {
|
||||
return defineComponent({
|
||||
name: component.name,
|
||||
inheritAttrs: false,
|
||||
setup: (props: any, { attrs, expose, slots }) => {
|
||||
const placeholder = props?.placeholder || attrs?.placeholder || $t(`ui.placeholder.${type}`);
|
||||
// 透传组件暴露的方法
|
||||
const innerRef = ref();
|
||||
expose(
|
||||
new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, key) => innerRef.value?.[key],
|
||||
has: (_target, key) => key in (innerRef.value || {}),
|
||||
},
|
||||
),
|
||||
);
|
||||
return () =>
|
||||
h(component, { ...componentProps, placeholder, ...props, ...attrs, ref: innerRef }, slots);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const withPreviewUpload = () => {
|
||||
return defineComponent({
|
||||
name: Upload.name,
|
||||
emits: ['change', 'update:modelValue'],
|
||||
setup: (props: any, { attrs, slots, emit }: { attrs: any; emit: any; slots: any }) => {
|
||||
const previewVisible = ref<boolean>(false);
|
||||
|
||||
const placeholder = attrs?.placeholder || $t(`ui.placeholder.upload`);
|
||||
|
||||
const listType = attrs?.listType || attrs?.['list-type'] || 'text';
|
||||
|
||||
const fileList = ref<UploadProps['fileList']>(attrs?.fileList || attrs?.['file-list'] || []);
|
||||
|
||||
const handleChange = async (event: UploadChangeParam) => {
|
||||
fileList.value = event.fileList;
|
||||
emit('change', event);
|
||||
emit('update:modelValue', event.fileList?.length ? fileList.value : undefined);
|
||||
};
|
||||
|
||||
const handlePreview = async (file: UploadFile) => {
|
||||
previewVisible.value = true;
|
||||
await previewImage(file, previewVisible, fileList);
|
||||
};
|
||||
|
||||
const renderUploadButton = (): any => {
|
||||
const isDisabled = attrs.disabled;
|
||||
|
||||
// 如果禁用,不渲染上传按钮
|
||||
if (isDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 否则渲染默认上传按钮
|
||||
return isEmpty(slots) ? createDefaultSlotsWithUpload(listType, placeholder) : slots;
|
||||
};
|
||||
|
||||
// 可以监听到表单API设置的值
|
||||
watch(
|
||||
() => attrs.modelValue,
|
||||
(res) => {
|
||||
fileList.value = res;
|
||||
},
|
||||
);
|
||||
|
||||
return () =>
|
||||
h(
|
||||
Upload,
|
||||
{
|
||||
...props,
|
||||
...attrs,
|
||||
fileList: fileList.value,
|
||||
onChange: handleChange,
|
||||
onPreview: handlePreview,
|
||||
},
|
||||
renderUploadButton(),
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const createDefaultSlotsWithUpload = (listType: string, placeholder: string) => {
|
||||
switch (listType) {
|
||||
case 'picture-card': {
|
||||
return {
|
||||
default: () => placeholder,
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return {
|
||||
default: () =>
|
||||
h(
|
||||
Button,
|
||||
{
|
||||
icon: h(IconifyIcon, {
|
||||
icon: 'ant-design:upload-outlined',
|
||||
class: 'mb-1 size-4',
|
||||
}),
|
||||
},
|
||||
() => placeholder,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const previewImage = async (
|
||||
file: UploadFile,
|
||||
visible: Ref<boolean>,
|
||||
fileList: Ref<UploadProps['fileList']>,
|
||||
) => {
|
||||
// 检查是否为图片文件的辅助函数
|
||||
const isImageFile = (file: UploadFile): boolean => {
|
||||
const imageExtensions = new Set(['bmp', 'gif', 'jpeg', 'jpg', 'png', 'webp']);
|
||||
if (file.url) {
|
||||
const ext = file.url?.split('.').pop()?.toLowerCase();
|
||||
return ext ? imageExtensions.has(ext) : false;
|
||||
}
|
||||
if (!file.type) {
|
||||
const ext = file.name?.split('.').pop()?.toLowerCase();
|
||||
return ext ? imageExtensions.has(ext) : false;
|
||||
}
|
||||
return file.type.startsWith('image/');
|
||||
};
|
||||
|
||||
// 如果当前文件不是图片,直接打开
|
||||
if (!isImageFile(file)) {
|
||||
if (file.url) {
|
||||
window.open(file.url, '_blank');
|
||||
} else if (file.preview) {
|
||||
window.open(file.preview, '_blank');
|
||||
} else {
|
||||
console.warn('无法打开文件,没有可用的URL或预览地址');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 对于图片文件,继续使用预览组
|
||||
const [ImageComponent, PreviewGroupComponent] = await Promise.all([Image, PreviewGroup]);
|
||||
|
||||
const getBase64 = (file: File) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.addEventListener('load', () => resolve(reader.result));
|
||||
reader.addEventListener('error', (error) => reject(error));
|
||||
});
|
||||
};
|
||||
// 从fileList中过滤出所有图片文件
|
||||
const imageFiles = (unref(fileList) || []).filter((element) => isImageFile(element));
|
||||
|
||||
// 为所有没有预览地址的图片生成预览
|
||||
for (const imgFile of imageFiles) {
|
||||
if (!imgFile.url && !imgFile.preview && imgFile.originFileObj) {
|
||||
imgFile.preview = (await getBase64(imgFile.originFileObj)) as string;
|
||||
}
|
||||
}
|
||||
const container: HTMLElement | null = document.createElement('div');
|
||||
document.body.append(container);
|
||||
|
||||
// 用于追踪组件是否已卸载
|
||||
let isUnmounted = false;
|
||||
|
||||
const PreviewWrapper = {
|
||||
setup() {
|
||||
return () => {
|
||||
if (isUnmounted) return null;
|
||||
return h(
|
||||
PreviewGroupComponent,
|
||||
{
|
||||
class: 'hidden',
|
||||
preview: {
|
||||
visible: visible.value,
|
||||
// 设置初始显示的图片索引
|
||||
current: imageFiles.findIndex((f) => f.uid === file.uid),
|
||||
onVisibleChange: (value: boolean) => {
|
||||
visible.value = value;
|
||||
if (!value) {
|
||||
// 延迟清理,确保动画完成
|
||||
setTimeout(() => {
|
||||
if (!isUnmounted && container) {
|
||||
isUnmounted = true;
|
||||
render(null, container);
|
||||
container.remove();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
() =>
|
||||
// 渲染所有图片文件
|
||||
imageFiles.map((imgFile) =>
|
||||
h(ImageComponent, {
|
||||
key: imgFile.uid,
|
||||
src: imgFile.url || imgFile.preview,
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
render(h(PreviewWrapper), container);
|
||||
};
|
||||
|
||||
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
||||
export type ComponentType =
|
||||
| 'ApiSelect'
|
||||
| 'ApiTreeSelect'
|
||||
| 'AutoComplete'
|
||||
| 'Checkbox'
|
||||
| 'CheckboxGroup'
|
||||
| 'DatePicker'
|
||||
| 'DefaultButton'
|
||||
| 'Divider'
|
||||
| 'IconPicker'
|
||||
| 'Input'
|
||||
| 'InputNumber'
|
||||
| 'InputPassword'
|
||||
| 'Mentions'
|
||||
| 'PrimaryButton'
|
||||
| 'Radio'
|
||||
| 'RadioGroup'
|
||||
| 'RangePicker'
|
||||
| 'Rate'
|
||||
| 'Select'
|
||||
| 'Space'
|
||||
| 'Switch'
|
||||
| 'Textarea'
|
||||
| 'TimePicker'
|
||||
| 'TreeSelect'
|
||||
| 'Upload'
|
||||
| BaseFormComponentType;
|
||||
|
||||
async function initComponentAdapter() {
|
||||
const components: Partial<Record<ComponentType, Component>> = {
|
||||
// 如果你的组件体积比较大,可以使用异步加载
|
||||
// Button: () =>
|
||||
// import('xxx').then((res) => res.Button),
|
||||
ApiSelect: withDefaultPlaceholder(
|
||||
{
|
||||
...ApiComponent,
|
||||
name: 'ApiSelect',
|
||||
},
|
||||
'select',
|
||||
{
|
||||
component: Select,
|
||||
loadingSlot: 'suffixIcon',
|
||||
visibleEvent: 'onDropdownVisibleChange',
|
||||
modelPropName: 'value',
|
||||
},
|
||||
),
|
||||
ApiTreeSelect: withDefaultPlaceholder(
|
||||
{
|
||||
...ApiComponent,
|
||||
name: 'ApiTreeSelect',
|
||||
},
|
||||
'select',
|
||||
{
|
||||
component: TreeSelect,
|
||||
fieldNames: { label: 'label', value: 'value', children: 'children' },
|
||||
loadingSlot: 'suffixIcon',
|
||||
modelPropName: 'value',
|
||||
optionsPropName: 'treeData',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
},
|
||||
),
|
||||
AutoComplete,
|
||||
Checkbox,
|
||||
CheckboxGroup,
|
||||
DatePicker,
|
||||
// 自定义默认按钮
|
||||
DefaultButton: (props, { attrs, slots }) => {
|
||||
return h(Button, { ...props, attrs, type: 'default' }, slots);
|
||||
},
|
||||
Divider,
|
||||
IconPicker: withDefaultPlaceholder(IconPicker, 'select', {
|
||||
iconSlot: 'addonAfter',
|
||||
inputComponent: Input,
|
||||
modelValueProp: 'value',
|
||||
}),
|
||||
Input: withDefaultPlaceholder(Input, 'input'),
|
||||
InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
|
||||
InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
|
||||
Mentions: withDefaultPlaceholder(Mentions, 'input'),
|
||||
// 自定义主要按钮
|
||||
PrimaryButton: (props, { attrs, slots }) => {
|
||||
return h(Button, { ...props, attrs, type: 'primary' }, slots);
|
||||
},
|
||||
Radio,
|
||||
RadioGroup,
|
||||
RangePicker,
|
||||
Rate,
|
||||
Select: withDefaultPlaceholder(Select, 'select'),
|
||||
Space,
|
||||
Switch,
|
||||
Textarea: withDefaultPlaceholder(Textarea, 'input'),
|
||||
TimePicker,
|
||||
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
|
||||
Upload: withPreviewUpload(),
|
||||
};
|
||||
|
||||
// 将组件注册到全局共享状态中
|
||||
globalShareState.setComponents(components);
|
||||
|
||||
// 定义全局共享状态中的消息提示
|
||||
globalShareState.defineMessage({
|
||||
// 复制成功消息提示
|
||||
copyPreferencesSuccess: (title, content) => {
|
||||
notification.success({
|
||||
description: content,
|
||||
message: title,
|
||||
placement: 'bottomRight',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export { initComponentAdapter };
|
||||
46
apps/finance/src/adapter/form.ts
Normal file
46
apps/finance/src/adapter/form.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { VbenFormSchema as FormSchema, VbenFormProps } from '@vben/common-ui';
|
||||
|
||||
import type { ComponentType } from './component';
|
||||
|
||||
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
async function initSetupVbenForm() {
|
||||
setupVbenForm<ComponentType>({
|
||||
config: {
|
||||
// ant design vue组件库默认都是 v-model:value
|
||||
baseModelPropName: 'value',
|
||||
|
||||
// 一些组件是 v-model:checked 或者 v-model:fileList
|
||||
modelPropNameMap: {
|
||||
Checkbox: 'checked',
|
||||
Radio: 'checked',
|
||||
Switch: 'checked',
|
||||
Upload: 'fileList',
|
||||
},
|
||||
},
|
||||
defineRules: {
|
||||
// 输入项目必填国际化适配
|
||||
required: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null || value.length === 0) {
|
||||
return $t('ui.formRules.required', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// 选择项目必填国际化适配
|
||||
selectRequired: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null) {
|
||||
return $t('ui.formRules.selectRequired', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const useVbenForm = useForm<ComponentType>;
|
||||
|
||||
export { initSetupVbenForm, useVbenForm, z };
|
||||
|
||||
export type VbenFormSchema = FormSchema<ComponentType>;
|
||||
export type { VbenFormProps };
|
||||
66
apps/finance/src/adapter/vxe-table.ts
Normal file
66
apps/finance/src/adapter/vxe-table.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
|
||||
|
||||
import { Button, Image } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from './form';
|
||||
|
||||
setupVbenVxeTable({
|
||||
configVxeTable: (vxeUI) => {
|
||||
vxeUI.setConfig({
|
||||
grid: {
|
||||
align: 'center',
|
||||
border: false,
|
||||
columnConfig: {
|
||||
resizable: true,
|
||||
},
|
||||
minHeight: 180,
|
||||
formConfig: {
|
||||
// 全局禁用vxe-table的表单配置,使用formOptions
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
autoLoad: true,
|
||||
response: {
|
||||
result: 'items',
|
||||
total: 'total',
|
||||
list: 'items',
|
||||
},
|
||||
showActiveMsg: true,
|
||||
showResponseMsg: false,
|
||||
},
|
||||
round: true,
|
||||
showOverflow: true,
|
||||
size: 'small',
|
||||
} as VxeTableGridOptions,
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
||||
vxeUI.renderer.add('CellImage', {
|
||||
renderTableDefault(renderOpts, params) {
|
||||
const { 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 { useVbenVxeGrid };
|
||||
|
||||
export type * from '@vben/plugins/vxe-table';
|
||||
69
apps/finance/src/api/core/auth.ts
Normal file
69
apps/finance/src/api/core/auth.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { BasicUserInfo } from '@vben-core/typings';
|
||||
|
||||
import { baseRequestClient, requestClient } from '#/api/request';
|
||||
|
||||
export namespace AuthApi {
|
||||
/** 登录接口参数 */
|
||||
export interface LoginParams {
|
||||
password?: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
export interface UserInfo extends BasicUserInfo {
|
||||
/**
|
||||
* 最后登录时间
|
||||
*/
|
||||
lastTime: string;
|
||||
}
|
||||
|
||||
/** 登录接口返回值 */
|
||||
export interface LoginResult<T, U> {
|
||||
userToken: T;
|
||||
userEntity: U;
|
||||
}
|
||||
|
||||
export interface UserToken {
|
||||
token: string;
|
||||
userId: string;
|
||||
expireTime: string;
|
||||
updateTime: string;
|
||||
}
|
||||
|
||||
export interface RefreshTokenResult {
|
||||
data: string;
|
||||
status: number;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*/
|
||||
export async function loginApi(data: AuthApi.LoginParams) {
|
||||
return requestClient.post<AuthApi.LoginResult<AuthApi.UserToken, AuthApi.UserInfo>>('/auth/login', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新accessToken
|
||||
*/
|
||||
export async function refreshTokenApi() {
|
||||
return baseRequestClient.post<AuthApi.RefreshTokenResult>('/auth/refresh', {
|
||||
withCredentials: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
export async function logoutApi() {
|
||||
return baseRequestClient.post('/auth/logout', {
|
||||
withCredentials: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户权限码
|
||||
*/
|
||||
export async function getAccessCodesApi() {
|
||||
// return requestClient.get<string[]>('/auth/codes');
|
||||
return [];
|
||||
}
|
||||
3
apps/finance/src/api/core/index.ts
Normal file
3
apps/finance/src/api/core/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './auth';
|
||||
export * from './menu';
|
||||
export * from './user';
|
||||
10
apps/finance/src/api/core/menu.ts
Normal file
10
apps/finance/src/api/core/menu.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { RouteRecordStringComponent } from '@vben/types';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 获取用户所有菜单
|
||||
*/
|
||||
export async function getAllMenusApi() {
|
||||
return requestClient.get<RouteRecordStringComponent[]>('/menu/all');
|
||||
}
|
||||
10
apps/finance/src/api/core/user.ts
Normal file
10
apps/finance/src/api/core/user.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { UserInfo } from '@vben/types';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
export async function getUserInfoApi() {
|
||||
return requestClient.get<UserInfo>('/user/info');
|
||||
}
|
||||
1
apps/finance/src/api/index.ts
Normal file
1
apps/finance/src/api/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './core';
|
||||
24
apps/finance/src/api/permission/user.ts
Normal file
24
apps/finance/src/api/permission/user.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export const userApi = {
|
||||
/**
|
||||
* 获取文件列表
|
||||
*/
|
||||
getUserList: (data: any) => {
|
||||
return requestClient.post('common/sysUser/getSysUserList', data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建用户
|
||||
*/
|
||||
createUser: (data: any) => {
|
||||
return requestClient.post('common/sysUser/addSysUser', data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 修改用户
|
||||
*/
|
||||
updateUser: (data: any) => {
|
||||
return requestClient.post('common/user/addUser', data);
|
||||
},
|
||||
};
|
||||
17
apps/finance/src/api/posting/import.ts
Normal file
17
apps/finance/src/api/posting/import.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export const importBillsApi = {
|
||||
/**
|
||||
* 获取文件列表
|
||||
*/
|
||||
getImportFileList: (data: { month?: number; year: number }) => {
|
||||
return requestClient.post('/common/import/getImportList', data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*/
|
||||
uploadFile: (data: any) => {
|
||||
return requestClient.upload('/common/import/importData', data);
|
||||
},
|
||||
};
|
||||
2
apps/finance/src/api/posting/index.ts
Normal file
2
apps/finance/src/api/posting/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './import';
|
||||
export * from './reconciliate';
|
||||
115
apps/finance/src/api/posting/reconciliate.ts
Normal file
115
apps/finance/src/api/posting/reconciliate.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export const reconciliateBillsApi = {
|
||||
/**
|
||||
* 获取账单列表
|
||||
*/
|
||||
getReconciliationList: (data: any) => {
|
||||
return requestClient.post('/common/payment/getPaymentList', data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 人工对账--订单对账
|
||||
*/
|
||||
manualCheckOrder: (data: any) => {
|
||||
return requestClient.post('/common/payment/manualCheckoff', data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 人工对账--添加订单后对账
|
||||
*/
|
||||
manualCheckCreated: (data: any) => {
|
||||
return requestClient.post('/common/payment/checkoffByAddOrder', { list: data });
|
||||
},
|
||||
|
||||
/**
|
||||
* 自动对账
|
||||
*/
|
||||
autoCheck: (data: any) => {
|
||||
return requestClient.post('/common/payment/autoCheckoff', data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取订单列表
|
||||
*/
|
||||
getOrderList: (data: any) => {
|
||||
return requestClient.post('/common/payment/manualList', data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取吴门医述开课记录
|
||||
*/
|
||||
getWumenRecommendCourseBuyList: (data: any) => {
|
||||
return requestClient.post('/common/payment/recommendWumenCourseBuyList', data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取吴门医述课程商品列表
|
||||
*/
|
||||
getWumenCourseBuyList: (data: any) => {
|
||||
return requestClient.post('/common/payment/wumenCourseBuyList', data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取吴门医述充值记录列表
|
||||
*/
|
||||
getWumenPointBuyList: (data: any) => {
|
||||
return requestClient.post('/common/payment/wumenPointBuyList', data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取吴门医述实物商品列表
|
||||
*/
|
||||
getWumenPhysicalBuyList: (data: any) => {
|
||||
return requestClient.post('/common/payment/wumenPhysicalBuyList', data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取一路健康开课记录列表
|
||||
*/
|
||||
getYiluRecommendCourseBuyList: (data: any) => {
|
||||
return requestClient.post('/common/payment/recommendCcourseBuyList', data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取一路健康课程商品列表
|
||||
*/
|
||||
getYiluCourseBuyList: (data: any) => {
|
||||
return requestClient.post('/common/payment/courseBuyList', data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取一路健康充值记录列表
|
||||
*/
|
||||
getYiluPointBuyList: (data: any) => {
|
||||
return requestClient.post('/common/payment/yljkPointBuyList', data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取一路健康实物商品列表
|
||||
*/
|
||||
getPhysicalBuyList: (data: any) => {
|
||||
return requestClient.post('/common/payment/physicalBuyList', data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取VIP列表
|
||||
*/
|
||||
getVipList: (data: any) => {
|
||||
return requestClient.post('/common/payment/vipList', data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取报名培训班列表
|
||||
*/
|
||||
getTrainingClassList: (data: any) => {
|
||||
return requestClient.post('/common/payment/trainingClassList', data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取推荐用户
|
||||
*/
|
||||
getRecommendUser: (data: any) => {
|
||||
return requestClient.post('/common/payment/recommendUser', data);
|
||||
},
|
||||
};
|
||||
223
apps/finance/src/api/request.ts
Normal file
223
apps/finance/src/api/request.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* 该文件可自行根据业务逻辑进行调整
|
||||
*/
|
||||
import type { RequestClientOptions } from '@vben/request';
|
||||
|
||||
import { useAppConfig } from '@vben/hooks';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import {
|
||||
authenticateResponseInterceptor,
|
||||
defaultResponseInterceptor,
|
||||
errorMessageResponseInterceptor,
|
||||
RequestClient,
|
||||
} from '@vben/request';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
import { refreshTokenApi } from './core';
|
||||
|
||||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||
|
||||
function createRequestClient(baseURL: string, options?: RequestClientOptions) {
|
||||
const client = new RequestClient({
|
||||
...options,
|
||||
baseURL,
|
||||
});
|
||||
|
||||
/**
|
||||
* 重新认证逻辑
|
||||
*/
|
||||
async function doReAuthenticate() {
|
||||
console.warn('Access token or refresh token is invalid or expired. ');
|
||||
const accessStore = useAccessStore();
|
||||
const authStore = useAuthStore();
|
||||
accessStore.setAccessToken(null);
|
||||
|
||||
if (preferences.app.loginExpiredMode === 'modal' && accessStore.isAccessChecked) {
|
||||
accessStore.setLoginExpired(true);
|
||||
} else {
|
||||
// 显示登录过期提示
|
||||
message.error({
|
||||
content: '您的登录状态已过期,请重新登录',
|
||||
duration: 3,
|
||||
});
|
||||
// 短暂延迟后跳转,让用户看到提示
|
||||
setTimeout(() => {
|
||||
authStore.logout();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新token逻辑
|
||||
*/
|
||||
async function doRefreshToken() {
|
||||
const accessStore = useAccessStore();
|
||||
const resp = await refreshTokenApi();
|
||||
const newToken = resp.data;
|
||||
accessStore.setAccessToken(newToken);
|
||||
return newToken;
|
||||
}
|
||||
|
||||
function formatToken(token: null | string) {
|
||||
return token ? `${token}` : null;
|
||||
}
|
||||
|
||||
// 处理请求参数中的undefined值,将其替换为空字符串
|
||||
function handleUndefinedParams(params: any): any {
|
||||
if (!params || typeof params !== 'object') {
|
||||
return params;
|
||||
}
|
||||
|
||||
const handleUndefined = (obj: any): any => {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => handleUndefined(item));
|
||||
}
|
||||
|
||||
if (obj !== null && typeof obj === 'object') {
|
||||
const result: any = {};
|
||||
for (const key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
const value = obj[key];
|
||||
if (value === undefined) {
|
||||
result[key] = '';
|
||||
} else if (value !== null && typeof value === 'object') {
|
||||
result[key] = handleUndefined(value);
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
return handleUndefined(params);
|
||||
}
|
||||
|
||||
// 请求头处理
|
||||
client.addRequestInterceptor({
|
||||
fulfilled: async (config) => {
|
||||
const accessStore = useAccessStore();
|
||||
|
||||
config.headers.token = formatToken(accessStore.accessToken);
|
||||
config.headers['Accept-Language'] = preferences.app.locale;
|
||||
|
||||
// 处理请求参数中的undefined值
|
||||
if (config.params) {
|
||||
config.params = handleUndefinedParams(config.params);
|
||||
}
|
||||
|
||||
// 处理请求体中的undefined值
|
||||
if (config.data && !(config.data instanceof FormData)) {
|
||||
// 如果是FormData,不进行处理,避免丢失参数
|
||||
// FormData不需要处理undefined值,因为浏览器会自动处理
|
||||
config.data = handleUndefinedParams(config.data);
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
});
|
||||
|
||||
// 自定义拦截器,处理后端返回的status为200但code不为0的情况
|
||||
client.addResponseInterceptor({
|
||||
fulfilled: (response) => {
|
||||
const { data: responseData, status } = response;
|
||||
|
||||
// 检查HTTP状态码为200但响应数据中code不为0的情况
|
||||
if (status === 200 && responseData?.code !== 0) {
|
||||
// 创建一个错误对象,模拟HTTP错误响应
|
||||
const error = new Error(`Request failed with code ${responseData.code}`);
|
||||
Object.assign(error, {
|
||||
response: {
|
||||
...response,
|
||||
status: responseData.code, // 将后端返回的code作为HTTP状态码
|
||||
data: responseData,
|
||||
},
|
||||
});
|
||||
|
||||
// 对于特定的错误码,执行特殊处理
|
||||
if (responseData.code === 401) {
|
||||
// 登录失效的情况,触发重新认证逻辑
|
||||
// 不要等待doReAuthenticate完成,因为它会跳转页面,不需要等待
|
||||
doReAuthenticate().catch((error_) => {
|
||||
console.error('doReAuthenticate error:', error_);
|
||||
});
|
||||
// 对于401错误,不抛出错误,而是返回一个特殊标记的响应
|
||||
// 这样可以避免控制台显示未捕获的错误
|
||||
return {
|
||||
...response,
|
||||
__isLoginExpired: true,
|
||||
} as any;
|
||||
}
|
||||
|
||||
// 抛出错误,让后续的错误处理拦截器处理
|
||||
throw error;
|
||||
}
|
||||
|
||||
// 对于其他情况,正常返回响应
|
||||
return response;
|
||||
},
|
||||
});
|
||||
|
||||
// 处理返回的响应数据格式
|
||||
client.addResponseInterceptor(
|
||||
defaultResponseInterceptor({
|
||||
codeField: 'code',
|
||||
dataField: 'data',
|
||||
successCode: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
// token过期的处理
|
||||
client.addResponseInterceptor(
|
||||
authenticateResponseInterceptor({
|
||||
client,
|
||||
doReAuthenticate,
|
||||
doRefreshToken,
|
||||
enableRefreshToken: preferences.app.enableRefreshToken,
|
||||
formatToken,
|
||||
}),
|
||||
);
|
||||
|
||||
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
|
||||
client.addResponseInterceptor(
|
||||
errorMessageResponseInterceptor((msg: string, error) => {
|
||||
// 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
|
||||
// 当前接口返回的错误字段是 error 或者 message
|
||||
const responseData = error?.response?.data ?? {};
|
||||
const errorMessage = responseData?.error ?? responseData?.message ?? responseData?.msg ?? '';
|
||||
const responseCode = error?.response?.status ?? responseData?.code;
|
||||
|
||||
// 如果是401错误,已经在自定义拦截器中处理了,这里不需要额外提示
|
||||
if (responseCode === 401) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// 如果有后端返回的错误信息,优先显示
|
||||
if (errorMessage) {
|
||||
message.error(errorMessage);
|
||||
} else if (msg) {
|
||||
// 否则显示默认的错误信息
|
||||
message.error(msg);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
export const requestClient = createRequestClient(apiURL, {
|
||||
responseReturn: 'data',
|
||||
timeout: 0, // 设置为0表示没有超时时间限制
|
||||
});
|
||||
|
||||
export const baseRequestClient = new RequestClient({
|
||||
baseURL: apiURL,
|
||||
timeout: 0, // 设置为0表示没有超时时间限制
|
||||
});
|
||||
39
apps/finance/src/app.vue
Normal file
39
apps/finance/src/app.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { useAntdDesignTokens } from '@vben/hooks';
|
||||
import { preferences, usePreferences } from '@vben/preferences';
|
||||
|
||||
import { App, ConfigProvider, theme } from 'ant-design-vue';
|
||||
|
||||
import { antdLocale } from '#/locales';
|
||||
|
||||
defineOptions({ name: 'App' });
|
||||
|
||||
const { isDark } = usePreferences();
|
||||
const { tokens } = useAntdDesignTokens();
|
||||
|
||||
const tokenTheme = computed(() => {
|
||||
const algorithm = isDark.value
|
||||
? [theme.darkAlgorithm]
|
||||
: [theme.defaultAlgorithm];
|
||||
|
||||
// antd 紧凑模式算法
|
||||
if (preferences.app.compact) {
|
||||
algorithm.push(theme.compactAlgorithm);
|
||||
}
|
||||
|
||||
return {
|
||||
algorithm,
|
||||
token: tokens,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfigProvider :locale="antdLocale" :theme="tokenTheme">
|
||||
<App>
|
||||
<RouterView />
|
||||
</App>
|
||||
</ConfigProvider>
|
||||
</template>
|
||||
75
apps/finance/src/bootstrap.ts
Normal file
75
apps/finance/src/bootstrap.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { createApp, watchEffect } from 'vue';
|
||||
|
||||
import { registerAccessDirective } from '@vben/access';
|
||||
import { registerLoadingDirective } from '@vben/common-ui/es/loading';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { initStores } from '@vben/stores';
|
||||
import '@vben/styles';
|
||||
import '@vben/styles/antd';
|
||||
|
||||
import { useTitle } from '@vueuse/core';
|
||||
|
||||
import { $t, setupI18n } from '#/locales';
|
||||
|
||||
import { initComponentAdapter } from './adapter/component';
|
||||
import { initSetupVbenForm } from './adapter/form';
|
||||
import App from './app.vue';
|
||||
import { router } from './router';
|
||||
|
||||
async function bootstrap(namespace: string) {
|
||||
// 初始化组件适配器
|
||||
await initComponentAdapter();
|
||||
|
||||
// 初始化表单组件
|
||||
await initSetupVbenForm();
|
||||
|
||||
// // 设置弹窗的默认配置
|
||||
// setDefaultModalProps({
|
||||
// fullscreenButton: false,
|
||||
// });
|
||||
// // 设置抽屉的默认配置
|
||||
// setDefaultDrawerProps({
|
||||
// zIndex: 1020,
|
||||
// });
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
// 注册v-loading指令
|
||||
registerLoadingDirective(app, {
|
||||
loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令
|
||||
spinning: 'spinning',
|
||||
});
|
||||
|
||||
// 国际化 i18n 配置
|
||||
await setupI18n(app);
|
||||
|
||||
// 配置 pinia-tore
|
||||
await initStores(app, { namespace });
|
||||
|
||||
// 安装权限指令
|
||||
registerAccessDirective(app);
|
||||
|
||||
// 初始化 tippy
|
||||
const { initTippy } = await import('@vben/common-ui/es/tippy');
|
||||
initTippy(app);
|
||||
|
||||
// 配置路由及路由守卫
|
||||
app.use(router);
|
||||
|
||||
// 配置Motion插件
|
||||
const { MotionPlugin } = await import('@vben/plugins/motion');
|
||||
app.use(MotionPlugin);
|
||||
|
||||
// 动态更新标题
|
||||
watchEffect(() => {
|
||||
if (preferences.app.dynamicTitle) {
|
||||
const routeTitle = router.currentRoute.value.meta?.title;
|
||||
const pageTitle = (routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name;
|
||||
useTitle(pageTitle);
|
||||
}
|
||||
});
|
||||
|
||||
app.mount('#app');
|
||||
}
|
||||
|
||||
export { bootstrap };
|
||||
336
apps/finance/src/components/CardListDemo.vue
Normal file
336
apps/finance/src/components/CardListDemo.vue
Normal file
@@ -0,0 +1,336 @@
|
||||
<script lang="ts" setup>
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Badge, Button, Space, Tag } from 'ant-design-vue';
|
||||
|
||||
import { useCardList } from './card-list/index';
|
||||
|
||||
interface UserData {
|
||||
id: number;
|
||||
name: string;
|
||||
age: number;
|
||||
email: string;
|
||||
department: string;
|
||||
status: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
// 示例数据
|
||||
const mockData: UserData[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: '张三',
|
||||
age: 25,
|
||||
email: 'zhangsan@example.com',
|
||||
department: '技术部',
|
||||
status: 'active',
|
||||
role: '开发工程师',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '李四',
|
||||
age: 30,
|
||||
email: 'lisi@example.com',
|
||||
department: '市场部',
|
||||
status: 'inactive',
|
||||
role: '市场经理',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '王五',
|
||||
age: 28,
|
||||
email: 'wangwu@example.com',
|
||||
department: '销售部',
|
||||
status: 'active',
|
||||
role: '销售代表',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '赵六',
|
||||
age: 32,
|
||||
email: 'zhaoliu@example.com',
|
||||
department: '人事部',
|
||||
status: 'active',
|
||||
role: '人事专员',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '钱七',
|
||||
age: 27,
|
||||
email: 'qianqi@example.com',
|
||||
department: '财务部',
|
||||
status: 'inactive',
|
||||
role: '会计',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: '孙八',
|
||||
age: 29,
|
||||
email: 'sunba@example.com',
|
||||
department: '技术部',
|
||||
status: 'active',
|
||||
role: '前端工程师',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: '周九',
|
||||
age: 26,
|
||||
email: 'zhoujiu@example.com',
|
||||
department: '市场部',
|
||||
status: 'active',
|
||||
role: '市场专员',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: '吴十',
|
||||
age: 31,
|
||||
email: 'wushi@example.com',
|
||||
department: '销售部',
|
||||
status: 'inactive',
|
||||
role: '销售经理',
|
||||
},
|
||||
];
|
||||
|
||||
// 基本用法
|
||||
const [BasicCardList] = useCardList<UserData>({
|
||||
title: '基本用法',
|
||||
gridOptions: {
|
||||
data: mockData,
|
||||
columns: [
|
||||
{ field: 'name', title: '姓名' },
|
||||
{ field: 'age', title: '年龄' },
|
||||
{ field: 'email', title: '邮箱' },
|
||||
{ field: 'department', title: '部门' },
|
||||
],
|
||||
gridColumns: 3,
|
||||
cardGap: '16px',
|
||||
},
|
||||
});
|
||||
|
||||
// 带分页的卡片列表
|
||||
const [PaginatedCardList] = useCardList<UserData>({
|
||||
title: '带分页的卡片列表',
|
||||
titleHelp: '展示分页功能',
|
||||
gridOptions: {
|
||||
data: mockData,
|
||||
columns: [
|
||||
{ field: 'name', title: '姓名' },
|
||||
{ field: 'age', title: '年龄' },
|
||||
{ field: 'email', title: '邮箱' },
|
||||
{ field: 'department', title: '部门' },
|
||||
],
|
||||
pagerConfig: {
|
||||
current: 1,
|
||||
pageSize: 4,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number, range: [number, number]) =>
|
||||
`第 ${range[0]}-${range[1]} 条,共 ${total} 条`,
|
||||
},
|
||||
gridColumns: 2,
|
||||
cardGap: '16px',
|
||||
},
|
||||
gridEvents: {
|
||||
pageChange: (page: number, pageSize: number) => {
|
||||
console.warn('页码变化:', page, pageSize);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 自定义插槽
|
||||
const [CustomSlotsCardList] = useCardList<UserData>({
|
||||
title: '自定义插槽',
|
||||
gridOptions: {
|
||||
data: mockData,
|
||||
columns: [
|
||||
{ field: 'name', title: '姓名' },
|
||||
{ field: 'age', title: '年龄' },
|
||||
{ field: 'department', title: '部门' },
|
||||
{ field: 'status', title: '状态', slots: { default: 'field-status' } },
|
||||
],
|
||||
gridColumns: 3,
|
||||
cardGap: '16px',
|
||||
},
|
||||
gridEvents: {
|
||||
cardClick: (row: UserData) => {
|
||||
console.warn('点击卡片:', row);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 完整功能示例
|
||||
const [FullFeaturedCardList, fullFeaturedApi] = useCardList<UserData>({
|
||||
title: '完整功能示例',
|
||||
titleHelp: '展示所有功能',
|
||||
class: 'full-featured-wrapper',
|
||||
gridClass: 'full-featured-list',
|
||||
gridOptions: {
|
||||
data: mockData,
|
||||
columns: [
|
||||
{
|
||||
field: 'name',
|
||||
title: '姓名',
|
||||
},
|
||||
{
|
||||
field: 'age',
|
||||
title: '年龄',
|
||||
formatter: (value: number) => `${value} 岁`,
|
||||
},
|
||||
{
|
||||
field: 'role',
|
||||
title: '职位',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
slots: { default: 'field-status-full' },
|
||||
},
|
||||
],
|
||||
showTitle: true,
|
||||
titleField: 'name',
|
||||
cardClass: 'custom-card',
|
||||
gridColumns: 3,
|
||||
cardGap: '20px',
|
||||
cardHeight: '200px',
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
current: 1,
|
||||
pageSize: 6,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: true,
|
||||
},
|
||||
loading: false,
|
||||
emptyText: '暂无用户数据',
|
||||
},
|
||||
gridEvents: {
|
||||
cardClick: (row: UserData) => {
|
||||
console.warn('卡片点击:', row);
|
||||
},
|
||||
pageChange: (page: number, pageSize: number) => {
|
||||
console.warn('页码变化:', page, pageSize);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 事件处理函数
|
||||
const handleEdit = (row: UserData) => {
|
||||
console.warn('编辑用户:', row);
|
||||
};
|
||||
|
||||
const handleDelete = (row: UserData) => {
|
||||
console.warn('删除用户:', row);
|
||||
};
|
||||
|
||||
const handleView = (row: UserData) => {
|
||||
console.warn('查看用户:', row);
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
fullFeaturedApi.setLoading(true);
|
||||
setTimeout(() => {
|
||||
fullFeaturedApi.setLoading(false);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const handleSetData = () => {
|
||||
fullFeaturedApi.setData(mockData.slice(0, 3));
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
fullFeaturedApi.reset();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height title="CardList 组件使用示例">
|
||||
<div class="card-list-demo">
|
||||
<h3 class="!mt-0">1. 基本用法</h3>
|
||||
<p class="demo-desc">最简单的卡片列表,展示数据的基本功能</p>
|
||||
<BasicCardList />
|
||||
|
||||
<h3>2. 带分页的卡片列表</h3>
|
||||
<p class="demo-desc">支持分页功能,可以切换页码和每页条数</p>
|
||||
<PaginatedCardList />
|
||||
|
||||
<h3>3. 自定义插槽</h3>
|
||||
<p class="demo-desc">使用自定义插槽来定制卡片内容和样式</p>
|
||||
<CustomSlotsCardList>
|
||||
<template #field-status="{ value }">
|
||||
<Tag :color="value === 'active' ? 'green' : 'red'">
|
||||
{{ value === 'active' ? '激活' : '停用' }}
|
||||
</Tag>
|
||||
</template>
|
||||
</CustomSlotsCardList>
|
||||
|
||||
<h3>4. 完整功能示例</h3>
|
||||
<p class="demo-desc">展示所有功能:标题、帮助提示、自定义插槽、分页、事件等</p>
|
||||
<div class="action-buttons">
|
||||
<Space>
|
||||
<Button type="primary" @click="handleRefresh">刷新</Button>
|
||||
<Button @click="handleSetData">设置数据</Button>
|
||||
<Button @click="handleReset">重置</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<FullFeaturedCardList>
|
||||
<template #card-extra="{ row }">
|
||||
<Space>
|
||||
<Button type="link" size="small" @click.stop="handleEdit(row)"> 编辑 </Button>
|
||||
<Button type="link" size="small" danger @click.stop="handleDelete(row)"> 删除 </Button>
|
||||
</Space>
|
||||
</template>
|
||||
|
||||
<template #card-actions="{ row }">
|
||||
<Button type="primary" size="small" @click.stop="handleView(row)"> 查看详情 </Button>
|
||||
</template>
|
||||
|
||||
<template #field-status-full="{ value }">
|
||||
<Badge
|
||||
:status="value === 'active' ? 'success' : 'error'"
|
||||
:text="value === 'active' ? '激活' : '停用'"
|
||||
/>
|
||||
</template>
|
||||
</FullFeaturedCardList>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card-list-demo {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 24px;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.demo-desc {
|
||||
margin-bottom: 16px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.full-featured-wrapper {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.full-featured-list {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
84
apps/finance/src/components/SelectDropdownRender/index.vue
Normal file
84
apps/finance/src/components/SelectDropdownRender/index.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<script lang="ts" setup>
|
||||
import { defineComponent, ref, watch } from 'vue';
|
||||
|
||||
import { Button, Divider, Input, Select, Space } from 'ant-design-vue';
|
||||
|
||||
export interface RemoteOption {
|
||||
label: string;
|
||||
value: string;
|
||||
/** 是否为用户自由输入生成的兜底项 */
|
||||
__custom?: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
/** 选项数据 */
|
||||
options?: any | { value: string }[];
|
||||
/** 占位符 */
|
||||
placeholder?: string;
|
||||
/** v-model */
|
||||
value?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:value']);
|
||||
|
||||
const VNodes = defineComponent({
|
||||
props: {
|
||||
vnodes: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
render() {
|
||||
return this.vnodes;
|
||||
},
|
||||
});
|
||||
|
||||
const items = ref<{ value: string }[]>(props.options || []);
|
||||
const value = ref(props.value);
|
||||
const inputRef = ref();
|
||||
const inputValue = ref('');
|
||||
|
||||
// 监听 props.value 变化,更新本地 value
|
||||
watch(
|
||||
() => props.value,
|
||||
(newValue) => {
|
||||
value.value = newValue;
|
||||
},
|
||||
);
|
||||
|
||||
// 监听本地 value 变化,触发 update:value 事件
|
||||
watch(value, (newValue) => {
|
||||
emit('update:value', newValue);
|
||||
});
|
||||
|
||||
const addItem = (e: Event) => {
|
||||
e.preventDefault();
|
||||
const newItemValue = inputValue.value;
|
||||
items.value.push({
|
||||
value: newItemValue,
|
||||
});
|
||||
// 将新添加的选项设为选中值
|
||||
value.value = newItemValue;
|
||||
inputValue.value = '';
|
||||
setTimeout(() => {
|
||||
inputRef.value?.focus();
|
||||
}, 0);
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<Select v-model:value="value" placeholder="请选择或新增" :options="items">
|
||||
<template #dropdownRender="{ menuNode: menu }">
|
||||
<VNodes :vnodes="menu" />
|
||||
<Divider style="margin: 4px 0" />
|
||||
<Space style="padding: 4px 8px">
|
||||
<Input ref="inputRef" v-model:value="inputValue" placeholder="在此手动输入" />
|
||||
<Button type="text" @click="addItem">
|
||||
<template #icon>
|
||||
<!-- <PlusOutlined /> -->
|
||||
</template>
|
||||
新增
|
||||
</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</Select>
|
||||
</template>
|
||||
197
apps/finance/src/components/card-list/api.ts
Normal file
197
apps/finance/src/components/card-list/api.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import type { CardListApi, CardListPagination, CardListProps } from './types';
|
||||
|
||||
import { bindMethods, isFunction, mergeWithArrayOverride, StateHandler } from '@vben/utils';
|
||||
|
||||
import { Store } from '@vben-core/shared/store';
|
||||
|
||||
function getDefaultState(): CardListProps {
|
||||
return {
|
||||
class: '',
|
||||
gridClass: '',
|
||||
gridOptions: {
|
||||
columns: [],
|
||||
data: [],
|
||||
showTitle: true,
|
||||
gridColumns: 3,
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number) => `共 ${total} 条`,
|
||||
},
|
||||
cardHeight: 'auto',
|
||||
cardWidth: '100%',
|
||||
cardGap: '10px',
|
||||
loading: false,
|
||||
emptyText: '暂无数据',
|
||||
},
|
||||
gridEvents: {},
|
||||
};
|
||||
}
|
||||
|
||||
export class CardListApiInstance<T extends Record<string, any> = any> implements CardListApi<T> {
|
||||
public state: CardListProps<T> | null = null;
|
||||
public store: Store<CardListProps<T>>;
|
||||
|
||||
private isMounted = false;
|
||||
private stateHandler: StateHandler;
|
||||
|
||||
constructor(options: CardListProps = {}) {
|
||||
const storeState = { ...options };
|
||||
|
||||
const defaultState = getDefaultState();
|
||||
this.store = new Store<CardListProps>(mergeWithArrayOverride(storeState, defaultState), {
|
||||
onUpdate: () => {
|
||||
this.state = this.store.state;
|
||||
},
|
||||
});
|
||||
|
||||
this.state = this.store.state;
|
||||
this.stateHandler = new StateHandler();
|
||||
bindMethods(this);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.setData([]);
|
||||
}
|
||||
|
||||
getData() {
|
||||
return (this.state?.gridOptions?.data || []).map((item) => ({ ...item })) as T[];
|
||||
}
|
||||
|
||||
/** 检查组件是否已挂载 */
|
||||
getIsMounted() {
|
||||
return this.isMounted;
|
||||
}
|
||||
|
||||
getState() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/** 添加项目 */
|
||||
insertAt(items: T[], index?: number) {
|
||||
const data = this.getData();
|
||||
const newData = [...data];
|
||||
|
||||
if (typeof index === 'number' && index >= 0 && index <= newData.length) {
|
||||
newData.splice(index, 0, ...items);
|
||||
} else {
|
||||
// 需判断如果原数据已经存在,则不添加
|
||||
newData.push(...items);
|
||||
}
|
||||
|
||||
this.setData(newData);
|
||||
}
|
||||
|
||||
mount() {
|
||||
this.isMounted = true;
|
||||
}
|
||||
|
||||
refresh() {
|
||||
// 触发数据刷新
|
||||
const currentData = this.state?.gridOptions?.data || [];
|
||||
this.setData([...currentData] as any);
|
||||
}
|
||||
|
||||
remove(predicate: ((row: T, index: number) => boolean) | number) {
|
||||
const data = this.getData();
|
||||
|
||||
const newData =
|
||||
typeof predicate === 'number'
|
||||
? data.filter((_, index) => index !== predicate)
|
||||
: data.filter((row, index) => !predicate(row, index));
|
||||
|
||||
this.setData(newData);
|
||||
}
|
||||
|
||||
reset() {
|
||||
const defaultState = getDefaultState();
|
||||
this.setState({
|
||||
gridOptions: defaultState.gridOptions,
|
||||
});
|
||||
}
|
||||
|
||||
setData(data: T[]) {
|
||||
this.setState({
|
||||
gridOptions: {
|
||||
...this.state?.gridOptions,
|
||||
data: data as any,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setErrorItems(errorItems: any[]) {
|
||||
// 遍历data,将errorItems中的id匹配到的item添加error属性
|
||||
const newData = this.getData().map((item) => {
|
||||
const errorData = errorItems.find((error) => error.id === item.id);
|
||||
return errorData ? { ...item, error: true } : { ...item, error: false };
|
||||
});
|
||||
this.setState({
|
||||
gridOptions: {
|
||||
...this.state?.gridOptions,
|
||||
data: newData as any,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setLoading(loading: boolean) {
|
||||
this.setState({
|
||||
gridOptions: {
|
||||
...this.state?.gridOptions,
|
||||
loading,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setPagination(pagination: any) {
|
||||
// 确保分页对象符合CardListPagination类型要求
|
||||
const validPagination: CardListPagination = {
|
||||
current: pagination?.current || 1,
|
||||
pageSize: pagination?.pageSize || 10,
|
||||
total: pagination?.total,
|
||||
showSizeChanger: pagination?.showSizeChanger,
|
||||
showQuickJumper: pagination?.showQuickJumper,
|
||||
showTotal: pagination?.showTotal,
|
||||
};
|
||||
|
||||
this.setState({
|
||||
gridOptions: {
|
||||
...this.state?.gridOptions,
|
||||
pagerConfig: validPagination,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setState(
|
||||
state: ((prev: CardListProps<T>) => Partial<CardListProps<T>>) | Partial<CardListProps<T>>,
|
||||
) {
|
||||
if (isFunction(state)) {
|
||||
const stateFn = state as (prev: CardListProps<T>) => Partial<CardListProps<T>>;
|
||||
this.store.setState((prev: any) => {
|
||||
return mergeWithArrayOverride(stateFn(prev), prev);
|
||||
});
|
||||
} else {
|
||||
this.store.setState((prev: any) => mergeWithArrayOverride(state, prev));
|
||||
}
|
||||
}
|
||||
|
||||
unmount() {
|
||||
this.isMounted = false;
|
||||
this.stateHandler.reset();
|
||||
}
|
||||
|
||||
updateItem(predicate: ((row: T, index: number) => boolean) | number, newItem: Partial<T>) {
|
||||
const data = this.getData();
|
||||
|
||||
const newData = data.map((row, index) => {
|
||||
const matched = typeof predicate === 'number' ? index === predicate : predicate(row, index);
|
||||
|
||||
return matched ? { ...row, ...newItem } : row;
|
||||
});
|
||||
|
||||
this.setData(newData);
|
||||
}
|
||||
}
|
||||
393
apps/finance/src/components/card-list/card-list.vue
Normal file
393
apps/finance/src/components/card-list/card-list.vue
Normal file
@@ -0,0 +1,393 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CardListProps, ExtendedCardListApi } from './types';
|
||||
|
||||
import { computed, h, nextTick, onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
import { usePriorityValues } from '@vben/hooks';
|
||||
import { cn } from '@vben/utils';
|
||||
|
||||
import { VbenLoading } from '@vben-core/shadcn-ui';
|
||||
|
||||
import { Card, DatePicker, Empty, Input, Select } from 'ant-design-vue';
|
||||
|
||||
import SelectDropdownRender from '#/components/SelectDropdownRender/index.vue';
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {});
|
||||
|
||||
// 创建一个全局缓存对象
|
||||
const selectOptionsCache: Record<string, any[]> = {};
|
||||
|
||||
interface Props extends CardListProps {
|
||||
api: ExtendedCardListApi;
|
||||
}
|
||||
|
||||
const listData = computed(() => {
|
||||
return gridOptions.value?.data || [];
|
||||
});
|
||||
|
||||
const state = props.api?.useStore?.();
|
||||
|
||||
const { class: className, gridClass, gridOptions, gridEvents } = usePriorityValues(props, state);
|
||||
|
||||
// 计算标题字段
|
||||
const titleFieldName = computed(() => {
|
||||
const titleField = gridOptions.value?.titleField;
|
||||
if (titleField) return titleField;
|
||||
const columns = gridOptions.value?.columns || [];
|
||||
if (columns.length > 0) return columns[0]?.field || '';
|
||||
return '';
|
||||
});
|
||||
|
||||
// 卡片网格样式
|
||||
const gridStyle = computed(() => {
|
||||
const gridHeight = gridOptions.value?.gridHeight || 'auto';
|
||||
const gridColumns = gridOptions.value?.gridColumns || 3;
|
||||
const cardGap = gridOptions.value?.cardGap || '10px';
|
||||
|
||||
return {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(${gridColumns}, 1fr)`,
|
||||
gap: cardGap,
|
||||
height: gridHeight,
|
||||
overflow: 'auto',
|
||||
padding: '5px',
|
||||
};
|
||||
});
|
||||
|
||||
// 卡片样式
|
||||
const cardStyle = (item: any) => {
|
||||
const cardHeight = gridOptions.value?.cardHeight;
|
||||
const cardWidth = gridOptions.value?.cardWidth || '100%';
|
||||
|
||||
const style: any = {
|
||||
width: cardWidth,
|
||||
minWidth: 0, // 添加最小宽度为0,防止内容撑开
|
||||
overflow: 'hidden', // 添加溢出隐藏
|
||||
borderColor: item.error ? 'red' : 'inherit',
|
||||
};
|
||||
|
||||
if (cardHeight && cardHeight !== 'auto') {
|
||||
style.height = cardHeight;
|
||||
}
|
||||
|
||||
return style;
|
||||
};
|
||||
|
||||
// const errorCardStyle = (item: any) => {
|
||||
// const errorData = gridOptions.value?.errorData || [];
|
||||
// return errorData.some((errorItem) => errorItem.id === item.id)
|
||||
// ? {
|
||||
// ...cardStyle.value,
|
||||
// borderColor: 'red',
|
||||
// }
|
||||
// : false;
|
||||
// };
|
||||
|
||||
// 格式化字段值
|
||||
const formatFieldValue = (column: any, row: any) => {
|
||||
if (!column?.field) return '';
|
||||
const value = row[column.field];
|
||||
if (column?.formatter) {
|
||||
return column.formatter(value, row);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
// 渲染编辑组件
|
||||
const renderEditComponent = (column: any, row: any) => {
|
||||
const fieldName = column.field;
|
||||
const value = row[fieldName];
|
||||
const editRender = column.editRender;
|
||||
const editProps = editRender?.props || {};
|
||||
|
||||
// 创建更新函数,避免直接修改只读对象
|
||||
const updateValue = (val: any) => {
|
||||
// 获取当前数据数组
|
||||
const currentData = gridOptions.value?.data || [];
|
||||
// 找到要修改的项的索引
|
||||
const index = currentData.findIndex((item: any) => item.id === row.id);
|
||||
if (index !== -1) {
|
||||
// 创建新的数据数组,避免直接修改原数组
|
||||
const newData = [...currentData];
|
||||
// 创建新的对象,避免直接修改原对象
|
||||
newData[index] = { ...newData[index], [fieldName]: val };
|
||||
// 更新数据
|
||||
props.api.setData(newData);
|
||||
}
|
||||
};
|
||||
|
||||
switch (editRender.name) {
|
||||
case 'date-picker':
|
||||
case 'DatePicker': {
|
||||
return h(DatePicker, {
|
||||
value,
|
||||
valueFormat: 'YYYY-MM-DD',
|
||||
...editProps,
|
||||
'onUpdate:value': updateValue,
|
||||
});
|
||||
}
|
||||
case 'Input':
|
||||
case 'input': {
|
||||
return h(Input, {
|
||||
value,
|
||||
...editProps,
|
||||
'onUpdate:value': updateValue,
|
||||
});
|
||||
}
|
||||
case 'input-number':
|
||||
case 'InputNumber': {
|
||||
return h(Input, {
|
||||
value,
|
||||
type: 'number',
|
||||
...editProps,
|
||||
'onUpdate:value': updateValue,
|
||||
});
|
||||
}
|
||||
case 'Select':
|
||||
case 'select': {
|
||||
return h(Select, {
|
||||
value,
|
||||
...editProps,
|
||||
'onUpdate:value': updateValue,
|
||||
});
|
||||
}
|
||||
case 'SelectDropdownRender':
|
||||
case 'selectDropdownRender': {
|
||||
// 创建一个响应式的选项数组
|
||||
const options = ref<any[]>([]);
|
||||
|
||||
// 创建一个唯一的缓存键,基于 row 的关键属性
|
||||
const cacheKey = `${row.paymentId}_${row.come}_${row.orderType}_${row.courseId}`;
|
||||
|
||||
// 检查缓存中是否已有数据
|
||||
if (selectOptionsCache[cacheKey]) {
|
||||
options.value = selectOptionsCache[cacheKey];
|
||||
} else if (editProps?.fetchOptions) {
|
||||
// 如果缓存中没有,则获取数据并存储到缓存
|
||||
editProps.fetchOptions(row).then((res: any) => {
|
||||
options.value = res;
|
||||
// 存储到缓存
|
||||
selectOptionsCache[cacheKey] = res;
|
||||
});
|
||||
}
|
||||
|
||||
return h(SelectDropdownRender, {
|
||||
value,
|
||||
...editProps,
|
||||
options,
|
||||
'onUpdate:value': updateValue,
|
||||
});
|
||||
}
|
||||
default: {
|
||||
return formatFieldValue(column, row);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 处理卡片点击
|
||||
const handleCardClick = (row: any) => {
|
||||
gridEvents.value?.cardClick?.(row);
|
||||
};
|
||||
|
||||
// 初始化
|
||||
async function init() {
|
||||
await nextTick();
|
||||
if (props.api?.mount) {
|
||||
props.api.mount();
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (props.api?.unmount) {
|
||||
props.api.unmount();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('h-full overflow-auto rounded-md', className)">
|
||||
<div :class="cn('rounded-md bg-card', gridClass)">
|
||||
<!-- 加载状态 -->
|
||||
<VbenLoading v-if="gridOptions?.loading" :spinning="true" />
|
||||
|
||||
<!-- 卡片列表 -->
|
||||
<div v-else-if="listData.length > 0" :style="gridStyle">
|
||||
<Card
|
||||
v-for="(item, index) in listData"
|
||||
:key="item[titleFieldName]"
|
||||
:class="gridOptions?.cardClass"
|
||||
:style="cardStyle(item)"
|
||||
@click="handleCardClick(item)"
|
||||
>
|
||||
<!-- 自定义标题 -->
|
||||
<template #title v-if="gridOptions?.showTitle">
|
||||
<div class="custom-card-title" :title="item[titleFieldName]">
|
||||
{{ item[titleFieldName] }}
|
||||
</div>
|
||||
</template>
|
||||
<!-- 卡片右上角额外内容 -->
|
||||
<template #extra v-if="$slots['card-extra']">
|
||||
<slot name="card-extra" :row="item" :index="index"></slot>
|
||||
</template>
|
||||
|
||||
<div class="card-content">
|
||||
<div
|
||||
v-for="column in gridOptions?.columns"
|
||||
:key="column?.field || ''"
|
||||
class="card-field"
|
||||
v-show="
|
||||
!column?.show ||
|
||||
(typeof column.show === 'function' ? column.show(item) : column.show)
|
||||
"
|
||||
>
|
||||
<!-- 自定义插槽 -->
|
||||
<div v-if="column?.slots?.default" class="field-item">
|
||||
<span class="field-title">{{ column?.title }}:</span>
|
||||
<span class="field-value field-edit">
|
||||
<slot
|
||||
:name="column.slots.default"
|
||||
:row="item"
|
||||
:field="column?.field || ''"
|
||||
:value="item[column?.field || '']"
|
||||
:index="index"
|
||||
></slot>
|
||||
</span>
|
||||
</div>
|
||||
<!-- 编辑渲染组件 -->
|
||||
<div v-else-if="column?.editRender" class="field-item">
|
||||
<span class="field-title">{{ column?.title }}:</span>
|
||||
<span class="field-value field-edit">
|
||||
<component :is="renderEditComponent(column, item)" />
|
||||
</span>
|
||||
</div>
|
||||
<!-- 默认显示 -->
|
||||
<div v-else class="field-item">
|
||||
<span class="field-title">{{ column?.title }}:</span>
|
||||
<span class="field-value">{{ formatFieldValue(column, item) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片底部操作区域 -->
|
||||
<template #actions v-if="$slots['card-actions']">
|
||||
<slot name="card-actions" :row="item"></slot>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else class="flex flex-col items-center justify-center py-10">
|
||||
<Empty :description="gridOptions?.emptyText || '暂无数据'" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-content {
|
||||
.card-field {
|
||||
margin-bottom: 8px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.field-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.field-title {
|
||||
flex-shrink: 0;
|
||||
margin-right: 8px;
|
||||
font-weight: 500;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.field-value {
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
|
||||
&.field-edit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
:deep(.ant-input),
|
||||
:deep(.ant-picker),
|
||||
:deep(.ant-select) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
:deep(.ant-card) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%; // 确保卡片不超出容器宽度
|
||||
min-width: 0; // 防止内容撑开
|
||||
|
||||
.ant-card-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 12px !important;
|
||||
min-width: 0; // 防止内容撑开
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-width: 0; // 防止内容撑开
|
||||
}
|
||||
|
||||
.ant-card-head {
|
||||
min-height: auto;
|
||||
padding: 0 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
min-width: 0; // 防止内容撑开
|
||||
height: auto; // 允许头部高度自适应
|
||||
display: flex; // 使用flex布局
|
||||
|
||||
.ant-card-head-wrapper {
|
||||
align-items: flex-start; // 顶部对齐
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ant-card-head-title {
|
||||
padding: 8px 0;
|
||||
width: 100%;
|
||||
height: auto; // 允许标题高度自适应
|
||||
white-space: normal; // 允许换行
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.ant-card-extra {
|
||||
padding: 8px 0; // 给extra区域添加与标题相同的内边距
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-card-title {
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
white-space: normal; // 允许换行
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
11
apps/finance/src/components/card-list/index.ts
Normal file
11
apps/finance/src/components/card-list/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export { CardListApiInstance } from './api';
|
||||
export { default as CardList } from './card-list.vue';
|
||||
export type {
|
||||
CardListApi,
|
||||
CardListColumn,
|
||||
CardListOptions,
|
||||
CardListPagination,
|
||||
CardListProps,
|
||||
ExtendedCardListApi,
|
||||
} from './types';
|
||||
export { useCardList } from './use-card-list';
|
||||
134
apps/finance/src/components/card-list/types.ts
Normal file
134
apps/finance/src/components/card-list/types.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import type { ClassType, DeepPartial } from '@vben/types';
|
||||
|
||||
export interface CardListColumn<T = any> {
|
||||
/** 字段名 */
|
||||
field: string;
|
||||
/** 显示标题 */
|
||||
title: string;
|
||||
/** 格式化函数 */
|
||||
formatter?: (value: any, row: T) => string;
|
||||
/** 插槽配置 */
|
||||
slots?: {
|
||||
default?: string;
|
||||
};
|
||||
/** 显示条件 */
|
||||
show?: ((row: T) => boolean) | boolean;
|
||||
/** 编辑渲染组件 */
|
||||
editRender?: { name: string; props?: Record<string, any> };
|
||||
}
|
||||
|
||||
export interface CardListPagination {
|
||||
/** 是否启用分页 */
|
||||
enabled?: boolean;
|
||||
/** 当前页码 */
|
||||
current?: number;
|
||||
/** 每页条数 */
|
||||
pageSize?: number;
|
||||
/** 总条数 */
|
||||
total?: number;
|
||||
/** 显示分页大小选择器 */
|
||||
showSizeChanger?: boolean;
|
||||
/** 显示快速跳转 */
|
||||
showQuickJumper?: boolean;
|
||||
/** 显示总条数 */
|
||||
showTotal?: ((total: number, range: [number, number]) => string) | boolean;
|
||||
}
|
||||
|
||||
export interface CardListOptions<T = any> {
|
||||
/** 列配置 */
|
||||
columns?: CardListColumn<T>[];
|
||||
/** 数据源 */
|
||||
data?: T[];
|
||||
/** 是否显示卡片标题 */
|
||||
showTitle?: boolean;
|
||||
/** 标题字段名,如果不指定则使用第一个字段 */
|
||||
titleField?: string;
|
||||
/** 卡片类名 */
|
||||
cardClass?: string;
|
||||
/** 网格列数 */
|
||||
gridColumns?: number;
|
||||
/** 网格高度 */
|
||||
gridHeight?: string;
|
||||
/** 分页配置 */
|
||||
pagerConfig?: CardListPagination;
|
||||
/** 卡片高度 */
|
||||
cardHeight?: string;
|
||||
/** 卡片宽度 */
|
||||
cardWidth?: string;
|
||||
/** 卡片间距 */
|
||||
cardGap?: string;
|
||||
/** 加载状态 */
|
||||
loading?: boolean;
|
||||
/** 空状态文本 */
|
||||
emptyText?: string;
|
||||
/** 错误数据 */
|
||||
errorData?: T[];
|
||||
}
|
||||
|
||||
export interface CardListProps<T = any> {
|
||||
/** 标题 */
|
||||
title?: string;
|
||||
/** 标题帮助 */
|
||||
titleHelp?: string;
|
||||
/** 组件class */
|
||||
class?: ClassType;
|
||||
/** 网格class */
|
||||
gridClass?: ClassType;
|
||||
/** 网格配置 */
|
||||
gridOptions?: DeepPartial<CardListOptions<T>>;
|
||||
/** 网格事件 */
|
||||
gridEvents?: {
|
||||
/** 卡片点击 */
|
||||
cardClick?: (row: T) => void;
|
||||
/** 页码变化 */
|
||||
pageChange?: (page: number, pageSize: number) => void;
|
||||
/** 每页条数变化 */
|
||||
pageSizeChange?: (pageSize: number) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export type CardListApi<T = any> = {
|
||||
/** 清空数据 */
|
||||
clear: () => void;
|
||||
/** 获取数据 */
|
||||
getData: () => T[];
|
||||
/** 获取状态 */
|
||||
getState: () => CardListProps<T> | null;
|
||||
/** 添加项目 */
|
||||
insertAt: (items: T[], index?: number) => void;
|
||||
/** 挂载组件 */
|
||||
mount: () => void;
|
||||
/** 刷新数据 */
|
||||
refresh: () => void;
|
||||
/** 删除项目 */
|
||||
remove: (predicate: ((row: T, index: number) => boolean) | number) => void;
|
||||
/** 重置 */
|
||||
reset: () => void;
|
||||
/** 设置数据 */
|
||||
setData: (data: T[]) => void;
|
||||
/** 设置错误数据 */
|
||||
setErrorItems: (errorItems: { id: number | string }[]) => void;
|
||||
/** 设置加载状态 */
|
||||
setLoading: (loading: boolean) => void;
|
||||
/** 设置分页 */
|
||||
setPagination: (pagination: CardListPagination) => void;
|
||||
/** 设置状态 */
|
||||
setState: (state: Partial<CardListProps<T>>) => void;
|
||||
/** 卸载组件 */
|
||||
unmount: () => void;
|
||||
/** 更新项目 */
|
||||
updateItem: (
|
||||
predicate: ((row: T, index: number) => boolean) | number,
|
||||
newItem: Partial<T>,
|
||||
) => void;
|
||||
};
|
||||
|
||||
export type ExtendedCardListApi<T = any> = CardListApi<T> & {
|
||||
useStore: <R = CardListProps<T>>(selector?: (state: CardListProps<T>) => R) => Readonly<Ref<R>>;
|
||||
};
|
||||
|
||||
export type UseVbenCardList = <T extends Record<string, any> = any>(
|
||||
options: CardListProps<T>,
|
||||
) => readonly [any, ExtendedCardListApi<T>];
|
||||
41
apps/finance/src/components/card-list/use-card-list.ts
Normal file
41
apps/finance/src/components/card-list/use-card-list.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { SlotsType } from 'vue';
|
||||
|
||||
import type { CardListProps, ExtendedCardListApi } from './types';
|
||||
|
||||
import { defineComponent, h, onBeforeUnmount } from 'vue';
|
||||
|
||||
import { useStore } from '@vben-core/shared/store';
|
||||
|
||||
import { CardListApiInstance } from './api';
|
||||
import CardList from './card-list.vue';
|
||||
|
||||
export function useCardList<T extends Record<string, any> = any>(options: CardListProps<T>) {
|
||||
const api = new CardListApiInstance<T>(options);
|
||||
const extendedApi = api as any as ExtendedCardListApi<T>;
|
||||
|
||||
extendedApi.useStore = (selector) => {
|
||||
return useStore(api.store, selector);
|
||||
};
|
||||
|
||||
const CardListComponent = defineComponent(
|
||||
(props: CardListProps<T>, { attrs, slots }) => {
|
||||
onBeforeUnmount(() => {
|
||||
api.unmount();
|
||||
});
|
||||
|
||||
api.setState({ ...props, ...attrs });
|
||||
return () => h(CardList, { ...props, ...attrs, api: extendedApi }, slots);
|
||||
},
|
||||
{
|
||||
name: 'CardList',
|
||||
inheritAttrs: false,
|
||||
slots: Object as SlotsType<{
|
||||
[key: string]: { field: string; index: number; row: T; value: any };
|
||||
}>,
|
||||
},
|
||||
);
|
||||
|
||||
return [CardListComponent, extendedApi] as const;
|
||||
}
|
||||
|
||||
export type useCardList = typeof useCardList;
|
||||
29
apps/finance/src/layouts/auth.vue
Normal file
29
apps/finance/src/layouts/auth.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { AuthPageLayout } from '@vben/layouts';
|
||||
import { preferences } from '@vben/preferences';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const appName = computed(() => preferences.app.name);
|
||||
const logo = computed(() => {
|
||||
return preferences.logo.enable ? preferences.logo.source : '';
|
||||
});
|
||||
const logoDark = computed(() => preferences.logo.sourceDark);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthPageLayout
|
||||
:app-name="appName"
|
||||
:logo="logo"
|
||||
:logo-dark="logoDark"
|
||||
:copyright="false"
|
||||
:toolbar-list="['color', 'layout', 'theme']"
|
||||
:page-description="$t('authentication.pageDesc')"
|
||||
:page-title="$t('authentication.pageTitle')"
|
||||
>
|
||||
<!-- 自定义工具栏 -->
|
||||
<!-- <template #toolbar></template> -->
|
||||
</AuthPageLayout>
|
||||
</template>
|
||||
78
apps/finance/src/layouts/basic.vue
Normal file
78
apps/finance/src/layouts/basic.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch } from 'vue';
|
||||
|
||||
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
|
||||
import { useWatermark } from '@vben/hooks';
|
||||
import { BasicLayout, LockScreen, UserDropdown } from '@vben/layouts';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||
|
||||
// import { $t } from '#/locales';
|
||||
import { useAuthStore } from '#/store';
|
||||
import LoginForm from '#/views/_core/authentication/login.vue';
|
||||
|
||||
// const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const authStore = useAuthStore();
|
||||
const accessStore = useAccessStore();
|
||||
const { destroyWatermark, updateWatermark } = useWatermark();
|
||||
|
||||
const menus = computed(() => [
|
||||
// {
|
||||
// handler: () => {
|
||||
// router.push({ name: 'Profile' });
|
||||
// },
|
||||
// icon: 'lucide:user',
|
||||
// text: $t('page.auth.profile'),
|
||||
// },
|
||||
]);
|
||||
|
||||
const avatar = computed(() => {
|
||||
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
|
||||
});
|
||||
|
||||
async function handleLogout() {
|
||||
await authStore.logout(false);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => ({
|
||||
enable: preferences.app.watermark,
|
||||
content: preferences.app.watermarkContent,
|
||||
}),
|
||||
async ({ enable, content }) => {
|
||||
if (enable) {
|
||||
await updateWatermark({
|
||||
content: content || `${userStore.userInfo?.account} - ${userStore.userInfo?.name}`,
|
||||
});
|
||||
} else {
|
||||
destroyWatermark();
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicLayout @clear-preferences-and-logout="handleLogout">
|
||||
<template #user-dropdown>
|
||||
<UserDropdown
|
||||
:avatar
|
||||
:menus
|
||||
:text="userStore.userInfo?.name"
|
||||
description="管理员"
|
||||
@logout="handleLogout"
|
||||
/>
|
||||
</template>
|
||||
<template #extra>
|
||||
<AuthenticationLoginExpiredModal v-model:open="accessStore.loginExpired" :avatar>
|
||||
<LoginForm />
|
||||
</AuthenticationLoginExpiredModal>
|
||||
</template>
|
||||
<template #lock-screen>
|
||||
<LockScreen :avatar @to-login="handleLogout" />
|
||||
</template>
|
||||
</BasicLayout>
|
||||
</template>
|
||||
6
apps/finance/src/layouts/index.ts
Normal file
6
apps/finance/src/layouts/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
const BasicLayout = () => import('./basic.vue');
|
||||
const AuthPageLayout = () => import('./auth.vue');
|
||||
|
||||
const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);
|
||||
|
||||
export { AuthPageLayout, BasicLayout, IFrameView };
|
||||
3
apps/finance/src/locales/README.md
Normal file
3
apps/finance/src/locales/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# locale
|
||||
|
||||
每个app使用的国际化可能不同,这里用于扩展国际化的功能,例如扩展 dayjs、antd组件库的多语言切换,以及app本身的国际化文件。
|
||||
95
apps/finance/src/locales/index.ts
Normal file
95
apps/finance/src/locales/index.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import type { Locale } from 'ant-design-vue/es/locale';
|
||||
|
||||
import type { App } from 'vue';
|
||||
|
||||
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { $t, setupI18n as coreSetup, loadLocalesMapFromDir } from '@vben/locales';
|
||||
import { preferences } from '@vben/preferences';
|
||||
|
||||
import antdEnLocale from 'ant-design-vue/es/locale/en_US';
|
||||
import antdDefaultLocale from 'ant-design-vue/es/locale/zh_CN';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const antdLocale = ref<Locale>(antdDefaultLocale);
|
||||
|
||||
const modules = import.meta.glob('./langs/**/*.json');
|
||||
|
||||
const localesMap = loadLocalesMapFromDir(/\.\/langs\/([^/]+)\/(.*)\.json$/, modules);
|
||||
/**
|
||||
* 加载应用特有的语言包
|
||||
* 这里也可以改造为从服务端获取翻译数据
|
||||
* @param lang
|
||||
*/
|
||||
async function loadMessages(lang: SupportedLanguagesType) {
|
||||
const [appLocaleMessages] = await Promise.all([
|
||||
localesMap[lang]?.(),
|
||||
loadThirdPartyMessage(lang),
|
||||
]);
|
||||
return appLocaleMessages?.default;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载第三方组件库的语言包
|
||||
* @param lang
|
||||
*/
|
||||
async function loadThirdPartyMessage(lang: SupportedLanguagesType) {
|
||||
await Promise.all([loadAntdLocale(lang), loadDayjsLocale(lang)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载dayjs的语言包
|
||||
* @param lang
|
||||
*/
|
||||
async function loadDayjsLocale(lang: SupportedLanguagesType) {
|
||||
let locale;
|
||||
switch (lang) {
|
||||
case 'en-US': {
|
||||
locale = await import('dayjs/locale/en');
|
||||
break;
|
||||
}
|
||||
case 'zh-CN': {
|
||||
locale = await import('dayjs/locale/zh-cn');
|
||||
break;
|
||||
}
|
||||
// 默认使用英语
|
||||
default: {
|
||||
locale = await import('dayjs/locale/en');
|
||||
}
|
||||
}
|
||||
if (locale) {
|
||||
dayjs.locale(locale);
|
||||
} else {
|
||||
console.error(`Failed to load dayjs locale for ${lang}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载antd的语言包
|
||||
* @param lang
|
||||
*/
|
||||
async function loadAntdLocale(lang: SupportedLanguagesType) {
|
||||
switch (lang) {
|
||||
case 'en-US': {
|
||||
antdLocale.value = antdEnLocale;
|
||||
break;
|
||||
}
|
||||
case 'zh-CN': {
|
||||
antdLocale.value = antdDefaultLocale;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
|
||||
await coreSetup(app, {
|
||||
defaultLocale: preferences.app.locale,
|
||||
loadMessages,
|
||||
missingWarn: !import.meta.env.PROD,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export { $t, antdLocale, setupI18n };
|
||||
13
apps/finance/src/locales/langs/en-US/demos.json
Normal file
13
apps/finance/src/locales/langs/en-US/demos.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"title": "Demos",
|
||||
"antd": "Ant Design Vue",
|
||||
"vben": {
|
||||
"title": "Project",
|
||||
"about": "About",
|
||||
"document": "Document",
|
||||
"antdv": "Ant Design Vue Version",
|
||||
"naive-ui": "Naive UI Version",
|
||||
"element-plus": "Element Plus Version",
|
||||
"tdesign": "TDesign Vue Version"
|
||||
}
|
||||
}
|
||||
15
apps/finance/src/locales/langs/en-US/page.json
Normal file
15
apps/finance/src/locales/langs/en-US/page.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"auth": {
|
||||
"login": "Login",
|
||||
"register": "Register",
|
||||
"codeLogin": "Code Login",
|
||||
"qrcodeLogin": "Qr Code Login",
|
||||
"forgetPassword": "Forget Password",
|
||||
"profile": "Profile"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"analytics": "Analytics",
|
||||
"workspace": "Workspace"
|
||||
}
|
||||
}
|
||||
13
apps/finance/src/locales/langs/zh-CN/demos.json
Normal file
13
apps/finance/src/locales/langs/zh-CN/demos.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"title": "演示",
|
||||
"antd": "Ant Design Vue",
|
||||
"vben": {
|
||||
"title": "项目",
|
||||
"about": "关于",
|
||||
"document": "文档",
|
||||
"antdv": "Ant Design Vue 版本",
|
||||
"naive-ui": "Naive UI 版本",
|
||||
"element-plus": "Element Plus 版本",
|
||||
"tdesign": "TDesign Vue 版本"
|
||||
}
|
||||
}
|
||||
15
apps/finance/src/locales/langs/zh-CN/page.json
Normal file
15
apps/finance/src/locales/langs/zh-CN/page.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"auth": {
|
||||
"login": "登录",
|
||||
"register": "注册",
|
||||
"codeLogin": "验证码登录",
|
||||
"qrcodeLogin": "二维码登录",
|
||||
"forgetPassword": "忘记密码",
|
||||
"profile": "个人中心"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "概览",
|
||||
"analytics": "分析页",
|
||||
"workspace": "工作台"
|
||||
}
|
||||
}
|
||||
31
apps/finance/src/main.ts
Normal file
31
apps/finance/src/main.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { initPreferences } from '@vben/preferences';
|
||||
import { unmountGlobalLoading } from '@vben/utils';
|
||||
|
||||
import { overridesPreferences } from './preferences';
|
||||
|
||||
/**
|
||||
* 应用初始化完成之后再进行页面加载渲染
|
||||
*/
|
||||
async function initApplication() {
|
||||
// name用于指定项目唯一标识
|
||||
// 用于区分不同项目的偏好设置以及存储数据的key前缀以及其他一些需要隔离的数据
|
||||
const env = import.meta.env.PROD ? 'prod' : 'dev';
|
||||
const appVersion = import.meta.env.VITE_APP_VERSION;
|
||||
const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${appVersion}-${env}`;
|
||||
|
||||
// app偏好设置初始化
|
||||
await initPreferences({
|
||||
namespace,
|
||||
overrides: overridesPreferences,
|
||||
});
|
||||
|
||||
// 启动应用并挂载
|
||||
// vue应用主要逻辑及视图
|
||||
const { bootstrap } = await import('./bootstrap');
|
||||
await bootstrap(namespace);
|
||||
|
||||
// 移除并销毁loading
|
||||
unmountGlobalLoading();
|
||||
}
|
||||
|
||||
initApplication();
|
||||
20
apps/finance/src/preferences.ts
Normal file
20
apps/finance/src/preferences.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { defineOverridesPreferences } from '@vben/preferences';
|
||||
|
||||
/**
|
||||
* @description 项目配置文件
|
||||
* 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
|
||||
* !!! 更改配置后请清空缓存,否则可能不生效
|
||||
*/
|
||||
export const overridesPreferences = defineOverridesPreferences({
|
||||
// overrides
|
||||
app: {
|
||||
enableCheckUpdates: false,
|
||||
name: import.meta.env.VITE_APP_TITLE,
|
||||
},
|
||||
widget: {
|
||||
languageToggle: false,
|
||||
notification: false,
|
||||
timezone: false,
|
||||
showCopyPreferences: false,
|
||||
},
|
||||
});
|
||||
42
apps/finance/src/router/access.ts
Normal file
42
apps/finance/src/router/access.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type {
|
||||
ComponentRecordType,
|
||||
GenerateMenuAndRoutesOptions,
|
||||
} from '@vben/types';
|
||||
|
||||
import { generateAccessible } from '@vben/access';
|
||||
import { preferences } from '@vben/preferences';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { getAllMenusApi } from '#/api';
|
||||
import { BasicLayout, IFrameView } from '#/layouts';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
|
||||
|
||||
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
||||
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
|
||||
|
||||
const layoutMap: ComponentRecordType = {
|
||||
BasicLayout,
|
||||
IFrameView,
|
||||
};
|
||||
|
||||
return await generateAccessible(preferences.app.accessMode, {
|
||||
...options,
|
||||
fetchMenuListAsync: async () => {
|
||||
message.loading({
|
||||
content: `${$t('common.loadingMenu')}...`,
|
||||
duration: 1.5,
|
||||
});
|
||||
return await getAllMenusApi();
|
||||
},
|
||||
// 可以指定没有权限跳转403页面
|
||||
forbiddenComponent,
|
||||
// 如果 route.meta.menuVisibleWithForbidden = true
|
||||
layoutMap,
|
||||
pageMap,
|
||||
});
|
||||
}
|
||||
|
||||
export { generateAccess };
|
||||
133
apps/finance/src/router/guard.ts
Normal file
133
apps/finance/src/router/guard.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import type { Router } from 'vue-router';
|
||||
|
||||
import { LOGIN_PATH } from '@vben/constants';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||
import { startProgress, stopProgress } from '@vben/utils';
|
||||
|
||||
import { accessRoutes, coreRouteNames } from '#/router/routes';
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
import { generateAccess } from './access';
|
||||
|
||||
/**
|
||||
* 通用守卫配置
|
||||
* @param router
|
||||
*/
|
||||
function setupCommonGuard(router: Router) {
|
||||
// 记录已经加载的页面
|
||||
const loadedPaths = new Set<string>();
|
||||
|
||||
router.beforeEach((to) => {
|
||||
to.meta.loaded = loadedPaths.has(to.path);
|
||||
|
||||
// 页面加载进度条
|
||||
if (!to.meta.loaded && preferences.transition.progress) {
|
||||
startProgress();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
router.afterEach((to) => {
|
||||
// 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
|
||||
|
||||
loadedPaths.add(to.path);
|
||||
|
||||
// 关闭页面加载进度条
|
||||
if (preferences.transition.progress) {
|
||||
stopProgress();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限访问守卫配置
|
||||
* @param router
|
||||
*/
|
||||
function setupAccessGuard(router: Router) {
|
||||
router.beforeEach(async (to, from) => {
|
||||
const accessStore = useAccessStore();
|
||||
const userStore = useUserStore();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
// 基本路由,这些路由不需要进入权限拦截
|
||||
if (coreRouteNames.includes(to.name as string)) {
|
||||
if (to.path === LOGIN_PATH && accessStore.accessToken) {
|
||||
return decodeURIComponent(
|
||||
(to.query?.redirect as string) ||
|
||||
userStore.userInfo?.homePath ||
|
||||
preferences.app.defaultHomePath,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// accessToken 检查
|
||||
if (!accessStore.accessToken) {
|
||||
// 明确声明忽略权限访问权限,则可以访问
|
||||
if (to.meta.ignoreAccess) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 没有访问权限,跳转登录页面
|
||||
if (to.fullPath !== LOGIN_PATH) {
|
||||
return {
|
||||
path: LOGIN_PATH,
|
||||
// 如不需要,直接删除 query
|
||||
query:
|
||||
to.fullPath === preferences.app.defaultHomePath
|
||||
? {}
|
||||
: { redirect: encodeURIComponent(to.fullPath) },
|
||||
// 携带当前跳转的页面,登录后重新跳转该页面
|
||||
replace: true,
|
||||
};
|
||||
}
|
||||
return to;
|
||||
}
|
||||
|
||||
// 是否已经生成过动态路由
|
||||
if (accessStore.isAccessChecked) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 生成路由表
|
||||
// 当前登录用户拥有的角色标识列表
|
||||
const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
|
||||
const userRoles = userInfo.roles ?? [];
|
||||
|
||||
// 生成菜单和路由
|
||||
const { accessibleMenus, accessibleRoutes } = await generateAccess({
|
||||
roles: userRoles,
|
||||
router,
|
||||
// 则会在菜单中显示,但是访问会被重定向到403
|
||||
routes: accessRoutes,
|
||||
});
|
||||
|
||||
// 保存菜单信息和路由信息
|
||||
accessStore.setAccessMenus(accessibleMenus);
|
||||
accessStore.setAccessRoutes(accessibleRoutes);
|
||||
accessStore.setIsAccessChecked(true);
|
||||
const redirectPath = (from.query.redirect ??
|
||||
(to.path === preferences.app.defaultHomePath
|
||||
? userInfo.homePath || preferences.app.defaultHomePath
|
||||
: to.fullPath)) as string;
|
||||
|
||||
return {
|
||||
...router.resolve(decodeURIComponent(redirectPath)),
|
||||
replace: true,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 项目守卫配置
|
||||
* @param router
|
||||
*/
|
||||
function createRouterGuard(router: Router) {
|
||||
/** 通用 */
|
||||
setupCommonGuard(router);
|
||||
/** 权限访问 */
|
||||
setupAccessGuard(router);
|
||||
}
|
||||
|
||||
export { createRouterGuard };
|
||||
33
apps/finance/src/router/index.ts
Normal file
33
apps/finance/src/router/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router';
|
||||
|
||||
import { resetStaticRoutes } from '@vben/utils';
|
||||
|
||||
import { createRouterGuard } from './guard';
|
||||
import { routes } from './routes';
|
||||
|
||||
/**
|
||||
* @zh_CN 创建vue-router实例
|
||||
*/
|
||||
const router = createRouter({
|
||||
history:
|
||||
import.meta.env.VITE_ROUTER_HISTORY === 'hash'
|
||||
? createWebHashHistory(import.meta.env.VITE_BASE)
|
||||
: createWebHistory(import.meta.env.VITE_BASE),
|
||||
// 应该添加到路由的初始路由列表。
|
||||
routes,
|
||||
scrollBehavior: (to, _from, savedPosition) => {
|
||||
if (savedPosition) {
|
||||
return savedPosition;
|
||||
}
|
||||
return to.hash ? { behavior: 'smooth', el: to.hash } : { left: 0, top: 0 };
|
||||
},
|
||||
// 是否应该禁止尾部斜杠。
|
||||
// strict: true,
|
||||
});
|
||||
|
||||
const resetRoutes = () => resetStaticRoutes(router, routes);
|
||||
|
||||
// 创建路由守卫
|
||||
createRouterGuard(router);
|
||||
|
||||
export { resetRoutes, router };
|
||||
95
apps/finance/src/router/routes/core.ts
Normal file
95
apps/finance/src/router/routes/core.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { LOGIN_PATH } from '@vben/constants';
|
||||
import { preferences } from '@vben/preferences';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const BasicLayout = () => import('#/layouts/basic.vue');
|
||||
const AuthPageLayout = () => import('#/layouts/auth.vue');
|
||||
/** 全局404页面 */
|
||||
const fallbackNotFoundRoute: RouteRecordRaw = {
|
||||
component: () => import('#/views/_core/fallback/not-found.vue'),
|
||||
meta: {
|
||||
hideInBreadcrumb: true,
|
||||
hideInMenu: true,
|
||||
hideInTab: true,
|
||||
title: '404',
|
||||
},
|
||||
name: 'FallbackNotFound',
|
||||
path: '/:path(.*)*',
|
||||
};
|
||||
|
||||
/** 基本路由,这些路由是必须存在的 */
|
||||
const coreRoutes: RouteRecordRaw[] = [
|
||||
/**
|
||||
* 根路由
|
||||
* 使用基础布局,作为所有页面的父级容器,子级就不必配置BasicLayout。
|
||||
* 此路由必须存在,且不应修改
|
||||
*/
|
||||
{
|
||||
component: BasicLayout,
|
||||
meta: {
|
||||
hideInBreadcrumb: true,
|
||||
title: 'Root',
|
||||
},
|
||||
name: 'Root',
|
||||
path: '/',
|
||||
redirect: preferences.app.defaultHomePath,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
component: AuthPageLayout,
|
||||
meta: {
|
||||
hideInTab: true,
|
||||
title: 'Authentication',
|
||||
},
|
||||
name: 'Authentication',
|
||||
path: '/auth',
|
||||
redirect: LOGIN_PATH,
|
||||
children: [
|
||||
{
|
||||
name: 'Login',
|
||||
path: 'login',
|
||||
component: () => import('#/views/_core/authentication/login.vue'),
|
||||
meta: {
|
||||
title: $t('page.auth.login'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'CodeLogin',
|
||||
path: 'code-login',
|
||||
component: () => import('#/views/_core/authentication/code-login.vue'),
|
||||
meta: {
|
||||
title: $t('page.auth.codeLogin'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'QrCodeLogin',
|
||||
path: 'qrcode-login',
|
||||
component: () => import('#/views/_core/authentication/qrcode-login.vue'),
|
||||
meta: {
|
||||
title: $t('page.auth.qrcodeLogin'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ForgetPassword',
|
||||
path: 'forget-password',
|
||||
component: () => import('#/views/_core/authentication/forget-password.vue'),
|
||||
meta: {
|
||||
title: $t('page.auth.forgetPassword'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Register',
|
||||
path: 'register',
|
||||
component: () => import('#/views/_core/authentication/register.vue'),
|
||||
meta: {
|
||||
title: $t('page.auth.register'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export { coreRoutes, fallbackNotFoundRoute };
|
||||
33
apps/finance/src/router/routes/index.ts
Normal file
33
apps/finance/src/router/routes/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { mergeRouteModules, traverseTreeValues } from '@vben/utils';
|
||||
|
||||
import { coreRoutes, fallbackNotFoundRoute } from './core';
|
||||
|
||||
const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
|
||||
eager: true,
|
||||
});
|
||||
|
||||
// 有需要可以自行打开注释,并创建文件夹
|
||||
// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
|
||||
// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
|
||||
|
||||
/** 动态路由 */
|
||||
const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
|
||||
|
||||
/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
|
||||
// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
|
||||
// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
|
||||
const staticRoutes: RouteRecordRaw[] = [];
|
||||
const externalRoutes: RouteRecordRaw[] = [];
|
||||
|
||||
/** 路由列表,由基本路由、外部路由和404兜底路由组成
|
||||
* 无需走权限验证(会一直显示在菜单中) */
|
||||
const routes: RouteRecordRaw[] = [...coreRoutes, ...externalRoutes, fallbackNotFoundRoute];
|
||||
|
||||
/** 基本路由列表,这些路由不需要进入权限拦截 */
|
||||
const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
|
||||
|
||||
/** 有权限校验的路由列表,包含动态路由和静态路由 */
|
||||
const accessRoutes = [...dynamicRoutes, ...staticRoutes];
|
||||
export { accessRoutes, coreRouteNames, routes };
|
||||
18
apps/finance/src/router/routes/modules/dashboard.ts
Normal file
18
apps/finance/src/router/routes/modules/dashboard.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
meta: {
|
||||
icon: 'lucide:layout-dashboard',
|
||||
order: -1,
|
||||
title: $t('page.dashboard.title'),
|
||||
},
|
||||
name: 'Dashboard',
|
||||
path: '/dashboard',
|
||||
component: () => import('#/views/dashboard/analytics/index.vue'),
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
36
apps/finance/src/router/routes/modules/posting.ts
Normal file
36
apps/finance/src/router/routes/modules/posting.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
meta: {
|
||||
icon: 'ic:baseline-view-in-ar',
|
||||
keepAlive: true,
|
||||
order: 1000,
|
||||
title: '入账管理',
|
||||
},
|
||||
name: 'Posting',
|
||||
path: '/posting',
|
||||
children: [
|
||||
{
|
||||
meta: {
|
||||
title: '账单导入',
|
||||
keepAlive: true,
|
||||
},
|
||||
name: 'ImportBills',
|
||||
path: '/posting/import-bills',
|
||||
component: () => import('#/views/posting/import/index.vue'),
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
title: '账单核对',
|
||||
keepAlive: true,
|
||||
},
|
||||
name: 'ReconciliateBills',
|
||||
path: '/posting/reconciliate-bills',
|
||||
component: () => import('#/views/posting/reconciliate/index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
34
apps/finance/src/router/routes/modules/system.ts
Normal file
34
apps/finance/src/router/routes/modules/system.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
meta: {
|
||||
icon: 'ic:baseline-view-in-ar',
|
||||
keepAlive: true,
|
||||
order: 1000,
|
||||
title: '系统管理',
|
||||
},
|
||||
name: 'System',
|
||||
path: '/system',
|
||||
children: [
|
||||
{
|
||||
meta: {
|
||||
title: '用户管理',
|
||||
},
|
||||
name: 'Users',
|
||||
path: '/system/users',
|
||||
component: () => import('#/views/permission/users/index.vue'),
|
||||
},
|
||||
// {
|
||||
// meta: {
|
||||
// title: '用户管理2',
|
||||
// },
|
||||
// name: 'Users2',
|
||||
// path: '/system/users2',
|
||||
// component: () => import('#/components/CardListDemo.vue'),
|
||||
// },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
112
apps/finance/src/store/auth.ts
Normal file
112
apps/finance/src/store/auth.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import type { Recordable, UserInfo } from '@vben/types';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { LOGIN_PATH } from '@vben/constants';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
|
||||
|
||||
import { notification } from 'ant-design-vue';
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import { loginApi, logoutApi } from '#/api';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const accessStore = useAccessStore();
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
|
||||
const loginLoading = ref(false);
|
||||
|
||||
/**
|
||||
* 异步处理登录操作
|
||||
* Asynchronously handle the login process
|
||||
* @param params 登录表单数据
|
||||
*/
|
||||
async function authLogin(params: Recordable<any>, onSuccess?: () => Promise<void> | void) {
|
||||
// 异步处理用户登录操作并获取 accessToken
|
||||
let userInfo: null | UserInfo = null;
|
||||
try {
|
||||
loginLoading.value = true;
|
||||
const { userToken, userEntity } = await loginApi(params);
|
||||
|
||||
// 如果成功获取到 accessToken
|
||||
if (userToken) {
|
||||
accessStore.setAccessToken(userToken.token);
|
||||
|
||||
// 获取用户信息并存储到 accessStore 中
|
||||
// const [fetchUserInfoResult, accessCodes] = await Promise.all([
|
||||
// fetchUserInfo(),
|
||||
// getAccessCodesApi(),
|
||||
// ]);
|
||||
|
||||
userInfo = userEntity;
|
||||
|
||||
userStore.setUserInfo(userInfo);
|
||||
// accessStore.setAccessCodes(accessCodes);
|
||||
|
||||
if (accessStore.loginExpired) {
|
||||
accessStore.setLoginExpired(false);
|
||||
} else {
|
||||
onSuccess
|
||||
? await onSuccess?.()
|
||||
: await router.push(userInfo?.homePath || preferences.app.defaultHomePath);
|
||||
}
|
||||
|
||||
if (userInfo?.name) {
|
||||
notification.success({
|
||||
description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.name}`,
|
||||
duration: 3,
|
||||
message: $t('authentication.loginSuccess'),
|
||||
});
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
loginLoading.value = false;
|
||||
}
|
||||
|
||||
return {
|
||||
userInfo,
|
||||
};
|
||||
}
|
||||
|
||||
async function logout(redirect: boolean = true) {
|
||||
try {
|
||||
await logoutApi();
|
||||
} catch {
|
||||
// 不做任何处理
|
||||
}
|
||||
resetAllStores();
|
||||
accessStore.setLoginExpired(false);
|
||||
|
||||
// 回登录页带上当前路由地址
|
||||
await router.replace({
|
||||
path: LOGIN_PATH,
|
||||
query: redirect
|
||||
? {
|
||||
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
|
||||
}
|
||||
: {},
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchUserInfo() {
|
||||
let userInfo: null | UserInfo = null;
|
||||
userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
|
||||
return userInfo;
|
||||
}
|
||||
|
||||
function $reset() {
|
||||
loginLoading.value = false;
|
||||
}
|
||||
|
||||
return {
|
||||
$reset,
|
||||
authLogin,
|
||||
fetchUserInfo,
|
||||
loginLoading,
|
||||
logout,
|
||||
};
|
||||
});
|
||||
2
apps/finance/src/store/index.ts
Normal file
2
apps/finance/src/store/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './auth';
|
||||
export * from './sys';
|
||||
92
apps/finance/src/store/sys.ts
Normal file
92
apps/finance/src/store/sys.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
export const useSysStore = defineStore('sys', () => {
|
||||
const staticDictMap = {
|
||||
payment: {
|
||||
'0': '微信',
|
||||
'1': '支付宝',
|
||||
'2': '银行',
|
||||
},
|
||||
checkoff: {
|
||||
'0': '未对账',
|
||||
'1': '对账失败',
|
||||
'2': '对账成功',
|
||||
},
|
||||
} as const;
|
||||
|
||||
// 自动推断字典类型
|
||||
type DictType = keyof typeof staticDictMap;
|
||||
|
||||
const getDictMap = (dictType: DictType, dictValue: number | string) => {
|
||||
const dict = staticDictMap[dictType];
|
||||
const key = String(dictValue);
|
||||
|
||||
// 使用 Record 类型来避免索引签名问题
|
||||
const record = dict as Record<string, string>;
|
||||
return record[key] ?? '';
|
||||
};
|
||||
|
||||
// 定义字典项类型
|
||||
type DictItem = {
|
||||
color?: string;
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
const staticDictList: Record<string, Array<DictItem>> = {
|
||||
payment: [
|
||||
{
|
||||
label: '微信',
|
||||
value: '0',
|
||||
},
|
||||
{
|
||||
label: '支付宝',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
label: '银行',
|
||||
value: '2',
|
||||
},
|
||||
],
|
||||
checkoff: [
|
||||
{
|
||||
label: '未对账',
|
||||
value: '0',
|
||||
color: 'warning',
|
||||
},
|
||||
{
|
||||
label: '对账失败',
|
||||
value: '1',
|
||||
color: 'error',
|
||||
},
|
||||
{
|
||||
label: '对账成功',
|
||||
value: '2',
|
||||
color: 'success',
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
// 自动推断字典列表类型
|
||||
type DictListType = keyof typeof staticDictList;
|
||||
|
||||
const getDictList = (dictType: DictListType) => {
|
||||
return staticDictList[dictType] ?? [];
|
||||
};
|
||||
|
||||
const getDictByList = (dictType: DictListType, dictValue: number | string) => {
|
||||
const dictList = getDictList(dictType);
|
||||
const key = String(dictValue);
|
||||
|
||||
// 确保返回的对象始终包含 color 属性
|
||||
return dictList.find((item: DictItem) => item.value === key) ?? { label: '', value: '' };
|
||||
};
|
||||
|
||||
return {
|
||||
staticDictMap,
|
||||
staticDictList,
|
||||
getDictMap,
|
||||
getDictList,
|
||||
getDictByList,
|
||||
$reset: () => {}, // sys store 只包含静态数据,不需要重置
|
||||
};
|
||||
});
|
||||
3
apps/finance/src/views/_core/README.md
Normal file
3
apps/finance/src/views/_core/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# \_core
|
||||
|
||||
此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。
|
||||
69
apps/finance/src/views/_core/authentication/code-login.vue
Normal file
69
apps/finance/src/views/_core/authentication/code-login.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { AuthenticationCodeLogin, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
defineOptions({ name: 'CodeLogin' });
|
||||
|
||||
const loading = ref(false);
|
||||
const CODE_LENGTH = 6;
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.mobile'),
|
||||
},
|
||||
fieldName: 'phoneNumber',
|
||||
label: $t('authentication.mobile'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, { message: $t('authentication.mobileTip') })
|
||||
.refine((v) => /^\d{11}$/.test(v), {
|
||||
message: $t('authentication.mobileErrortip'),
|
||||
}),
|
||||
},
|
||||
{
|
||||
component: 'VbenPinInput',
|
||||
componentProps: {
|
||||
codeLength: CODE_LENGTH,
|
||||
createText: (countdown: number) => {
|
||||
const text =
|
||||
countdown > 0
|
||||
? $t('authentication.sendText', [countdown])
|
||||
: $t('authentication.sendCode');
|
||||
return text;
|
||||
},
|
||||
placeholder: $t('authentication.code'),
|
||||
},
|
||||
fieldName: 'code',
|
||||
label: $t('authentication.code'),
|
||||
rules: z.string().length(CODE_LENGTH, {
|
||||
message: $t('authentication.codeTip', [CODE_LENGTH]),
|
||||
}),
|
||||
},
|
||||
];
|
||||
});
|
||||
/**
|
||||
* 异步处理登录操作
|
||||
* Asynchronously handle the login process
|
||||
* @param values 登录表单数据
|
||||
*/
|
||||
async function handleLogin(values: Recordable<any>) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(values);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationCodeLogin
|
||||
:form-schema="formSchema"
|
||||
:loading="loading"
|
||||
@submit="handleLogin"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { AuthenticationForgetPassword, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
defineOptions({ name: 'ForgetPassword' });
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: 'example@example.com',
|
||||
},
|
||||
fieldName: 'email',
|
||||
label: $t('authentication.email'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, { message: $t('authentication.emailTip') })
|
||||
.email($t('authentication.emailValidErrorTip')),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
function handleSubmit(value: Recordable<any>) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('reset email:', value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationForgetPassword
|
||||
:form-schema="formSchema"
|
||||
:loading="loading"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
50
apps/finance/src/views/_core/authentication/login.vue
Normal file
50
apps/finance/src/views/_core/authentication/login.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { AuthenticationLogin, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
defineOptions({ name: 'Login' });
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.usernameTip'),
|
||||
},
|
||||
fieldName: 'account',
|
||||
label: $t('authentication.username'),
|
||||
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
|
||||
},
|
||||
{
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.password'),
|
||||
},
|
||||
fieldName: 'password',
|
||||
label: $t('authentication.password'),
|
||||
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationLogin
|
||||
:form-schema="formSchema"
|
||||
:loading="authStore.loginLoading"
|
||||
:show-code-login="false"
|
||||
:show-forget-password="false"
|
||||
:show-qrcode-login="false"
|
||||
:show-third-party-login="false"
|
||||
:show-register="false"
|
||||
@submit="authStore.authLogin"
|
||||
/>
|
||||
</template>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user