commit 829edfa82460e35725f363814a3cd0b8733e2d7a Author: web@ppanel Date: Thu Nov 14 01:22:43 2024 +0700 🎉 chore(init): project initialization diff --git a/.commitlintrc.js b/.commitlintrc.js new file mode 100644 index 0000000..ea6f53a --- /dev/null +++ b/.commitlintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ['@repo/commitlint-config'], +}; diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..dc3c978 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,10 @@ +// This configuration only applies to the package manager root. +/** @type {import("eslint").Linter.Config} */ +module.exports = { + ignorePatterns: ['apps/**', 'packages/**'], + extends: ['@repo/eslint-config/library.js'], + parser: '@typescript-eslint/parser', + parserOptions: { + project: true, + }, +}; diff --git a/.github/ISSUE_TEMPLATE/1_bug_report.yml b/.github/ISSUE_TEMPLATE/1_bug_report.yml new file mode 100644 index 0000000..d181c38 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1_bug_report.yml @@ -0,0 +1,45 @@ +name: '🐛 反馈缺陷 Bug Report' +description: '反馈一个问题缺陷 | Report an bug' +title: '[Bug] ' +labels: '🐛 Bug' +body: + - type: dropdown + attributes: + label: '💻 系统环境 | Operating System' + options: + - Windows + - macOS + - Ubuntu + - Other Linux + - Other + validations: + required: true + - type: dropdown + attributes: + label: '🌐 浏览器 | Browser' + options: + - Chrome + - Edge + - Safari + - Firefox + - Other + validations: + required: true + - type: textarea + attributes: + label: '🐛 问题描述 | Bug Description' + description: A clear and concise description of the bug. + validations: + required: true + - type: textarea + attributes: + label: '🚦 期望结果 | Expected Behavior' + description: A clear and concise description of what you expected to happen. + - type: textarea + attributes: + label: '📷 复现步骤 | Recurrence Steps' + description: A clear and concise description of how to recurrence. + - type: textarea + attributes: + label: '📝 补充信息 | Additional Information' + description: If your problem needs further explanation, or if the issue you're seeing cannot be reproduced in a gist, please add more information here. diff --git a/.github/ISSUE_TEMPLATE/2_feature_request.yml b/.github/ISSUE_TEMPLATE/2_feature_request.yml new file mode 100644 index 0000000..edcf7d0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2_feature_request.yml @@ -0,0 +1,21 @@ +name: '🌠 功能需求 Feature Request' +description: '需求或建议 | Suggest an idea' +title: '[Request] ' +labels: '🌠 Feature Request' +body: + - type: textarea + attributes: + label: '🥰 需求描述 | Feature Description' + description: Please add a clear and concise description of the problem you are seeking to solve with this feature request. + validations: + required: true + - type: textarea + attributes: + label: '🧐 解决方案 | Proposed Solution' + description: Describe the solution you'd like in a clear and concise manner. + validations: + required: true + - type: textarea + attributes: + label: '📝 补充信息 | Additional Information' + description: Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/3_question.yml b/.github/ISSUE_TEMPLATE/3_question.yml new file mode 100644 index 0000000..f989f7d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/3_question.yml @@ -0,0 +1,15 @@ +name: '😇 疑问或帮助 Help Wanted' +description: '疑问或需要帮助 | Need help' +title: '[Question] ' +labels: '😇 Help Wanted' +body: + - type: textarea + attributes: + label: '🧐 问题描述 | Proposed Solution' + description: A clear and concise description of the proplem. + validations: + required: true + - type: textarea + attributes: + label: '📝 补充信息 | Additional Information' + description: Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/4_other.md b/.github/ISSUE_TEMPLATE/4_other.md new file mode 100644 index 0000000..215dd1f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/4_other.md @@ -0,0 +1,7 @@ +--- +name: '📝 其他 Other' +about: '其他问题 | Other issues' +title: '' +labels: '' +assignees: '' +--- diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..a73b6cf --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,17 @@ +#### 💻 变更类型 | Change Type + + + +- \[ ] ✨ feat +- \[ ] 🐛 fix +- \[ ] 💄 style +- \[ ] 🔨 chore +- \[ ] 📝 docs + +#### 🔀 变更说明 | Description of Change + + + +#### 📝 补充信息 | Additional Information + + diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml new file mode 100644 index 0000000..eea0d46 --- /dev/null +++ b/.github/workflows/auto-merge.yml @@ -0,0 +1,30 @@ +name: Dependabot Auto Merge +on: + pull_request_target: + types: [labeled, edited] + +jobs: + merge: + if: contains(github.event.pull_request.labels.*.name, 'dependencies') + name: Dependabot Auto Merge + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js environment + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install deps + run: pnpm install + + - name: Merge + uses: ahmadnassri/action-dependabot-auto-merge@v2 + with: + command: merge + target: minor + github-token: ${{ secrets.GH_TOKEN }} diff --git a/.github/workflows/issue-check-inactive.yml b/.github/workflows/issue-check-inactive.yml new file mode 100644 index 0000000..d37c4c3 --- /dev/null +++ b/.github/workflows/issue-check-inactive.yml @@ -0,0 +1,22 @@ +name: Issue Check Inactive + +on: + schedule: + - cron: '0 0 */15 * *' + +permissions: + contents: read + +jobs: + issue-check-inactive: + permissions: + issues: write # for actions-cool/issues-helper to update issues + pull-requests: write # for actions-cool/issues-helper to update PRs + runs-on: ubuntu-latest + steps: + - name: check-inactive + uses: actions-cool/issues-helper@v3 + with: + actions: 'check-inactive' + inactive-label: 'Inactive' + inactive-day: 30 diff --git a/.github/workflows/issue-close-require.yml b/.github/workflows/issue-close-require.yml new file mode 100644 index 0000000..68d6b6c --- /dev/null +++ b/.github/workflows/issue-close-require.yml @@ -0,0 +1,46 @@ +name: Issue Close Require + +on: + schedule: + - cron: '0 0 * * *' + +permissions: + contents: read + +jobs: + issue-close-require: + permissions: + issues: write # for actions-cool/issues-helper to update issues + pull-requests: write # for actions-cool/issues-helper to update PRs + runs-on: ubuntu-latest + steps: + - name: need reproduce + uses: actions-cool/issues-helper@v3 + with: + actions: 'close-issues' + labels: '✅ Fixed' + inactive-day: 3 + body: | + Since the issue was labeled with `✅ Fixed`, but no response in 3 days. This issue will be closed. If you have any questions, you can comment and reply. + + 由于该 issue 被标记为已修复,同时 3 天未收到回应。现关闭 issue,若有任何问题,可评论回复。 + - name: need reproduce + uses: actions-cool/issues-helper@v3 + with: + actions: 'close-issues' + labels: '🤔 Need Reproduce' + inactive-day: 3 + body: | + Since the issue was labeled with `🤔 Need Reproduce`, but no response in 3 days. This issue will be closed. If you have any questions, you can comment and reply. + + 由于该 issue 被标记为需要更多信息,却 3 天未收到回应。现关闭 issue,若有任何问题,可评论回复。 + - name: need reproduce + uses: actions-cool/issues-helper@v3 + with: + actions: 'close-issues' + labels: "🙅🏻‍♀️ WON'T DO" + inactive-day: 3 + body: | + Since the issue was labeled with `🙅🏻‍♀️ WON'T DO`, and no response in 3 days. This issue will be closed. If you have any questions, you can comment and reply. + + 由于该 issue 被标记为暂不处理,同时 3 天未收到回应。现关闭 issue,若有任何问题,可评论回复。 diff --git a/.github/workflows/issue-remove-inactive.yml b/.github/workflows/issue-remove-inactive.yml new file mode 100644 index 0000000..dbe42dd --- /dev/null +++ b/.github/workflows/issue-remove-inactive.yml @@ -0,0 +1,25 @@ +name: Issue Remove Inactive + +on: + issues: + types: [edited] + issue_comment: + types: [created, edited] + +permissions: + contents: read + +jobs: + issue-remove-inactive: + permissions: + issues: write # for actions-cool/issues-helper to update issues + pull-requests: write # for actions-cool/issues-helper to update PRs + runs-on: ubuntu-latest + steps: + - name: remove inactive + if: github.event.issue.state == 'open' && github.actor == github.event.issue.user.login + uses: actions-cool/issues-helper@v3 + with: + actions: 'remove-labels' + issue-number: ${{ github.event.issue.number }} + labels: 'Inactive' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e9c7b78 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,45 @@ +name: Build and Release + +on: + push: + branches: + - main + +jobs: + release: + name: Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js environment + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Cache pnpm store + uses: actions/cache@v3 + with: + path: ~/.pnpm-store + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install deps + run: pnpm install + + - name: Lint + run: pnpm lint + + - name: Build + run: pnpm build + + - name: Release + id: release + run: pnpm release + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a1ffe0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# Dependencies +node_modules +.pnp +.pnp.js +.husky +./bin + +# Local env files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Testing +coverage + +# Turbo +.turbo + +# Vercel +.vercel + +# Build Outputs +.next/ +out/ +build +dist + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Misc +.DS_Store +*.pem diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..16c4845 --- /dev/null +++ b/.npmrc @@ -0,0 +1,10 @@ +public-hoist-pattern[]=*typescript* +public-hoist-pattern[]=*tailwindcss* +public-hoist-pattern[]=*autoprefixer* +public-hoist-pattern[]=*postcss* +public-hoist-pattern[]=*eslint* +public-hoist-pattern[]=*prettier* +public-hoist-pattern[]=*commitlint* +public-hoist-pattern[]=*semantic-release* +public-hoist-pattern[]=*@umijs/openapi* + diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..778aa87 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,57 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# Dependencies +node_modules +.pnp +.pnp.js + +# Local env files +.env* + +# Testing +coverage + +# Turbo +.turbo + +# Vercel +.vercel + +# Build Outputs +.next/ +out/ +build +dist + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Misc +.DS_Store +*.pem + +# Fonts +*.woff + +# Images +*.svg +*.ico + +# Husky +.husky + +# Docker +Dockerfile + +# LICENSE +LICENSE + +# Ignores +.npmrc +.gitignore +.prettierignore +public + +packages/ui/src/lotties/*.json \ No newline at end of file diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..95967ee --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1 @@ +module.exports = require('@repo/prettier-config'); diff --git a/.releaserc.js b/.releaserc.js new file mode 100644 index 0000000..27fbc79 --- /dev/null +++ b/.releaserc.js @@ -0,0 +1,71 @@ +const { createConfig } = require('semantic-release-config-gitmoji/lib/createConfig'); + +const config = createConfig({ + tagFormat: 'v${version}', + changelogTitle: ` +# Changelog`, + releaseRules: [ + { + release: 'minor', + type: 'feat', + }, + { + release: 'patch', + type: 'fix', + }, + { + release: 'patch', + type: 'perf', + }, + { + release: 'patch', + type: 'style', + }, + { + release: 'patch', + type: 'refactor', + }, + { + release: 'patch', + type: 'build', + }, + { release: 'patch', scope: 'README', type: 'docs' }, + { release: 'patch', scope: 'README.md', type: 'docs' }, + { release: false, type: 'docs' }, + { + release: false, + type: 'test', + }, + { + release: false, + type: 'ci', + }, + { + release: false, + type: 'chore', + }, + { + release: false, + type: 'wip', + }, + { + release: 'major', + type: 'BREAKING CHANGE', + }, + { + release: 'major', + scope: 'BREAKING CHANGE', + }, + { + release: 'major', + subject: '*BREAKING CHANGE*', + }, + { release: 'patch', subject: '*force release*' }, + { release: 'patch', subject: '*force patch*' }, + { release: 'minor', subject: '*force minor*' }, + { release: 'major', subject: '*force major*' }, + { release: false, subject: '*skip release*' }, + ], +}); + +module.exports = config; diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ab25516 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,15 @@ +{ + "eslint.workingDirectories": [ + { + "mode": "auto" + } + ], + "explorer.fileNesting.enabled": true, + "explorer.fileNesting.patterns": { + "*.ts": "${capture}.js", + "*.js": "${capture}.js.map, ${capture}.min.js, ${capture}.d.ts", + "*.jsx": "${capture}.js", + "*.tsx": "${capture}.ts", + "package.json": "*" + } +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..42729d4 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or + advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email + address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders 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, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +For answers to common questions about this code of conduct, see the FAQ at +. Translations are available at +. + +[homepage]: https://www.contributor-covenant.org diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..649ea40 --- /dev/null +++ b/README.md @@ -0,0 +1,125 @@ + + +
+ + + +

PPanel web

+ +This is a PPanel web powered by PPanel + +English +· +[Chinese](./README.zh-CN.md) +· +[Changelog](../../CHANGELOG.md) +· +[Report Bug][issues-link] +· +[Request Feature][issues-link] + + + +[![][github-release-shield]][github-release-link] +[![][github-releasedate-shield]][github-releasedate-link] +[![][github-action-test-shield]][github-action-test-link] +[![][github-action-release-shield]][github-action-release-link]
+[![][github-contributors-shield]][github-contributors-link] +[![][github-forks-shield]][github-forks-link] +[![][github-stars-shield]][github-stars-link] +[![][github-issues-shield]][github-issues-link] +[![][github-license-shield]][github-license-link] + +![][split] + +
+ +## 📦 Application List + +| 📦 Application | 🖼️ Preview | +| :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------- | +| [**PPanel User Web**][ppanel-user-web-github]
Developed with modern frontend technologies (Next.js, TypeScript, TailwindCSS), providing basic user features with support for multiple languages and themes.
[![One-Click Deploy](https://img.shields.io/badge/Deploy%20with-Vercel-blue?style=for-the-badge)][ppanel-user-web-deploy] | [![Preview][ppanel-user-web-cover]][ppanel-user-web-github] | +| [**PPanel Admin Web**][ppanel-admin-web-github]
Developed with modern frontend technologies, this admin web provides basic data management features with support for multiple languages and themes.
[![One-Click Deploy](https://img.shields.io/badge/Deploy%20with-Vercel-blue?style=for-the-badge)][ppanel-admin-web-deploy] | [![Preview][ppanel-admin-web-cover]][ppanel-admin-web-preview] | + +## ⌨️ Local Development + +You can use Github Codespaces for online development: + +[![][codespaces-shield]][codespaces-link] + +You can use Gitpod for online development: + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][gitpod-link] + +or clone it for local development: + +```bash +git clone https://github.com/perfect-panel/ppanel-web.git +cd ppanel-web + +# Install dependencies +pnpm install +``` + +## 🤝 Contributing + +Contributions of all types are more than welcome, +if you're interested in contributing code, feel free to check out our GitHub +[Issues][github-issues-link] to get stuck in to show us what you’re made of. + +[![][pr-welcome-shield]][pr-welcome-link] + +[![][contributors-contrib]][contributors-url] + +
+ +[![][back-to-top]](#readme-top) + +
+ +--- + +## 📝 License + +Copyright © 2024 [PPanel][profile-link].
+This project is [GUN](../../LICENSE) licensed. + + + +[back-to-top]: https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square +[codespaces-link]: https://codespaces.new/perfect-panel/ppanel-web +[codespaces-shield]: https://github.com/codespaces/badge.svg +[contributors-contrib]: https://contrib.rocks/image?repo=perfect-panel/ppanel-web +[contributors-url]: https://github.com/perfect-panel/ppanel-web/graphs/contributors +[github-action-release-link]: https://github.com/perfect-panel/ppanel-web/actions/workflows/release.yml +[github-action-release-shield]: https://img.shields.io/github/actions/workflow/status/perfect-panel/ppanel-web/release.yml?label=release&labelColor=black&logo=githubactions&logoColor=white&style=flat-square +[github-action-test-link]: https://github.com/perfect-panel/ppanel-web/actions/workflows/test.yml +[github-action-test-shield]: https://img.shields.io/github/actions/workflow/status/perfect-panel/ppanel-web/test.yml?label=test&labelColor=black&logo=githubactions&logoColor=white&style=flat-square +[github-contributors-link]: https://github.com/perfect-panel/ppanel-web/graphs/contributors +[github-contributors-shield]: https://img.shields.io/github/contributors/perfect-panel/ppanel-web?color=c4f042&labelColor=black&style=flat-square +[github-forks-link]: https://github.com/perfect-panel/ppanel-web/network/members +[github-forks-shield]: https://img.shields.io/github/forks/perfect-panel/ppanel-web?color=8ae8ff&labelColor=black&style=flat-square +[github-issues-link]: https://github.com/perfect-panel/ppanel-web/issues +[github-issues-shield]: https://img.shields.io/github/issues/perfect-panel/ppanel-web?color=ff80eb&labelColor=black&style=flat-square +[github-license-link]: https://github.com/perfect-panel/ppanel-web/blob/master/LICENSE +[github-license-shield]: https://img.shields.io/github/license/perfect-panel/ppanel-web?color=white&labelColor=black&style=flat-square +[github-release-link]: https://github.com/perfect-panel/ppanel-web/releases +[github-release-shield]: https://img.shields.io/github/v/release/perfect-panel/ppanel-web?style=flat-square&sort=semver&logo=github +[github-releasedate-link]: https://github.com/perfect-panel/ppanel-web/releases +[github-releasedate-shield]: https://img.shields.io/github/release-date/perfect-panel/ppanel-web?labelColor=black&style=flat-square +[github-stars-link]: https://github.com/perfect-panel/ppanel-web/network/stargazers +[github-stars-shield]: https://img.shields.io/github/stars/perfect-panel/ppanel-web?color=ffcb47&labelColor=black&style=flat-square +[gitpod-link]: https://gitpod.io/#https://github.com/perfect-panel/ppanel-web +[issues-link]: https://github.com/perfect-panel/ppanel-web/issues/new/choose +[pr-welcome-link]: https://github.com/perfect-panel/ppanel-web/pulls +[pr-welcome-shield]: https://img.shields.io/badge/🤯_pr_welcome-%E2%86%92-ffcb47?labelColor=black&style=for-the-badge +[profile-link]: https://github.com/perfect-panel +[split]: https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png +[ppanel-user-web-github]: https://github.com/perfect-panel/ppanel-web/tree/main/apps/user +[ppanel-user-web-cover]: https://urlscan.io/liveshot/?width=1920&height=1080&url=https://user.ppanel.dev +[ppanel-user-web-preview]: https://user.ppanel.dev +[ppanel-user-web-deploy]: https://vercel.com/new/clone?demo-description=PPanel%20is%20a%20pure%2C%20professional%2C%20and%20perfect%20open-source%20proxy%20panel%20tool%2C%20designed%20to%20be%20your%20ideal%20choice%20for%20learning%20and%20practical%20use&demo-image=https%3A%2F%2Furlscan.io%2Fliveshot%2F%3Fwidth%3D1920%26height%3D1080%26url%3Dhttps%3A%2F%2Fuser.ppanel.dev&demo-title=PPanel%20User%20Web&demo-url=https%3A%2F%2Fuser.ppanel.dev%2F&from=.&project-name=ppanel-user-web&repository-name=ppanel-web&repository-url=https%3A%2F%2Fgithub.com%2Fperfect-panel%2Fppanel-web&root-directory=apps%2Fuser&skippable-integrations=1 +[ppanel-admin-web-github]: https://github.com/perfect-panel/ppanel-web/tree/main/apps/admin +[ppanel-admin-web-cover]: https://urlscan.io/liveshot/?width=1920&height=1080&url=https://admin.ppanel.dev +[ppanel-admin-web-preview]: https://admin.ppanel.dev +[ppanel-admin-web-deploy]: https://vercel.com/new/clone?demo-description=PPanel%20is%20a%20pure%2C%20professional%2C%20and%20perfect%20open-source%20proxy%20panel%20tool%2C%20designed%20to%20be%20your%20ideal%20choice%20for%20learning%20and%20practical%20use&demo-image=https%3A%2F%2Furlscan.io%2Fliveshot%2F%3Fwidth%3D1920%26height%3D1080%26url%3Dhttps%3A%2F%2Fadmin.ppanel.dev&demo-title=PPanel%20Admin%20Web&demo-url=https%3A%2F%2Fadmin.ppanel.dev%2F&from=.&project-name=ppanel-admin-web&repository-name=ppanel-web&repository-url=https%3A%2F%2Fgithub.com%2Fperfect-panel%2Fppanel-web&root-directory=apps%2Fadmin&skippable-integrations=1 diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..fb0b6d0 --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,125 @@ + + +
+ + + +

PPanel 前端

+ +这是由 PPanel 提供支持的前端 + +[英文](./README.md) +· +中文 +· +[更新日志](./CHANGELOG.md) +· +[报告问题][issues-link] +· +[请求功能][issues-link] + + + +[![][github-release-shield]][github-release-link] +[![][github-releasedate-shield]][github-releasedate-link] +[![][github-action-test-shield]][github-action-test-link] +[![][github-action-release-shield]][github-action-release-link]
+[![][github-contributors-shield]][github-contributors-link] +[![][github-forks-shield]][github-forks-link] +[![][github-stars-shield]][github-stars-link] +[![][github-issues-shield]][github-issues-link] +[![][github-license-shield]][github-license-link] + +![][split] + +
+ +## 📦 Application List + +| 📦 Application | 🖼️ Preview | +| :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------- | +| [**PPanel User Web**][ppanel-user-web-github]
Developed with modern frontend technologies (Next.js, TypeScript, TailwindCSS), providing basic user features with support for multiple languages and themes.
[![One-Click Deploy](https://img.shields.io/badge/Deploy%20with-Vercel-blue?style=for-the-badge)][ppanel-user-web-deploy] | [![Preview][ppanel-user-web-cover]][ppanel-user-web-github] | +| [**PPanel Admin Web**][ppanel-admin-web-github]
Developed with modern frontend technologies, this admin web provides basic data management features with support for multiple languages and themes.
[![One-Click Deploy](https://img.shields.io/badge/Deploy%20with-Vercel-blue?style=for-the-badge)][ppanel-admin-web-deploy] | [![Preview][ppanel-admin-web-cover]][ppanel-admin-web-preview] | + +## ⌨️ 本地开发 + +您可以使用 Github Codespaces 进行在线开发: + +[![][codespaces-shield]][codespaces-link] + +您可以使用 Gitpod 进行在线开发: + +[![在 Gitpod 中打开](https://gitpod.io/button/open-in-gitpod.svg)][gitpod-link] + +或者克隆项目进行本地开发: + +```bash +git clone https://github.com/perfect-panel/ppanel-web.git +cd ppanel-web + +# 安装依赖 +pnpm install +``` + +## 🤝 贡献 + +欢迎各种类型的贡献, +如果您有兴趣贡献代码,请随时查看我们的 GitHub +[问题][github-issues-link] 来展示您的能力。 + +[![][pr-welcome-shield]][pr-welcome-link] + +[![][contributors-contrib]][contributors-url] + +
+ +[![][back-to-top]](#readme-top) + +
+ +--- + +## 📝 许可证 + +版权所有 © 2024 [PPanel][profile-link]。
+本项目使用 [GUN](./LICENSE) 许可证。 + + + +[back-to-top]: https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square +[codespaces-link]: https://codespaces.new/perfect-panel/ppanel-web +[codespaces-shield]: https://github.com/codespaces/badge.svg +[contributors-contrib]: https://contrib.rocks/image?repo=perfect-panel/ppanel-web +[contributors-url]: https://github.com/perfect-panel/ppanel-web/graphs/contributors +[github-action-release-link]: https://github.com/perfect-panel/ppanel-web/actions/workflows/release.yml +[github-action-release-shield]: https://img.shields.io/github/actions/workflow/status/perfect-panel/ppanel-web/release.yml?label=release&labelColor=black&logo=githubactions&logoColor=white&style=flat-square +[github-action-test-link]: https://github.com/perfect-panel/ppanel-web/actions/workflows/test.yml +[github-action-test-shield]: https://img.shields.io/github/actions/workflow/status/perfect-panel/ppanel-web/test.yml?label=test&labelColor=black&logo=githubactions&logoColor=white&style=flat-square +[github-contributors-link]: https://github.com/perfect-panel/ppanel-web/graphs/contributors +[github-contributors-shield]: https://img.shields.io/github/contributors/perfect-panel/ppanel-web?color=c4f042&labelColor=black&style=flat-square +[github-forks-link]: https://github.com/perfect-panel/ppanel-web/network/members +[github-forks-shield]: https://img.shields.io/github/forks/perfect-panel/ppanel-web?color=8ae8ff&labelColor=black&style=flat-square +[github-issues-link]: https://github.com/perfect-panel/ppanel-web/issues +[github-issues-shield]: https://img.shields.io/github/issues/perfect-panel/ppanel-web?color=ff80eb&labelColor=black&style=flat-square +[github-license-link]: https://github.com/perfect-panel/ppanel-web/blob/master/LICENSE +[github-license-shield]: https://img.shields.io/github/license/perfect-panel/ppanel-web?color=white&labelColor=black&style=flat-square +[github-release-link]: https://github.com/perfect-panel/ppanel-web/releases +[github-release-shield]: https://img.shields.io/github/v/release/perfect-panel/ppanel-web?style=flat-square&sort=semver&logo=github +[github-releasedate-link]: https://github.com/perfect-panel/ppanel-web/releases +[github-releasedate-shield]: https://img.shields.io/github/release-date/perfect-panel/ppanel-web?labelColor=black&style=flat-square +[github-stars-link]: https://github.com/perfect-panel/ppanel-web/network/stargazers +[github-stars-shield]: https://img.shields.io/github/stars/perfect-panel/ppanel-web?color=ffcb47&labelColor=black&style=flat-square +[gitpod-link]: https://gitpod.io/#https://github.com/perfect-panel/ppanel-web +[issues-link]: https://github.com/perfect-panel/ppanel-web/issues/new/choose +[pr-welcome-link]: https://github.com/perfect-panel/ppanel-web/pulls +[pr-welcome-shield]: https://img.shields.io/badge/🤯_pr_welcome-%E2%86%92-ffcb47?labelColor=black&style=for-the-badge +[profile-link]: https://github.com/perfect-panel +[split]: https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png +[ppanel-user-web-github]: https://github.com/perfect-panel/ppanel-web/tree/main/apps/user +[ppanel-user-web-cover]: https://urlscan.io/liveshot/?width=1920&height=1080&url=https://user.ppanel.dev +[ppanel-user-web-preview]: https://user.ppanel.dev +[ppanel-user-web-deploy]: https://vercel.com/new/clone?demo-description=PPanel%20is%20a%20pure%2C%20professional%2C%20and%20perfect%20open-source%20proxy%20panel%20tool%2C%20designed%20to%20be%20your%20ideal%20choice%20for%20learning%20and%20practical%20use&demo-image=https%3A%2F%2Furlscan.io%2Fliveshot%2F%3Fwidth%3D1920%26height%3D1080%26url%3Dhttps%3A%2F%2Fuser.ppanel.dev&demo-title=PPanel%20User%20Web&demo-url=https%3A%2F%2Fuser.ppanel.dev%2F&from=.&project-name=ppanel-user-web&repository-name=ppanel-web&repository-url=https%3A%2F%2Fgithub.com%2Fperfect-panel%2Fppanel-web&root-directory=apps%2Fuser&skippable-integrations=1 +[ppanel-admin-web-github]: https://github.com/perfect-panel/ppanel-web/tree/main/apps/admin +[ppanel-admin-web-cover]: https://urlscan.io/liveshot/?width=1920&height=1080&url=https://admin.ppanel.dev +[ppanel-admin-web-preview]: https://admin.ppanel.dev +[ppanel-admin-web-deploy]: https://vercel.com/new/clone?demo-description=PPanel%20is%20a%20pure%2C%20professional%2C%20and%20perfect%20open-source%20proxy%20panel%20tool%2C%20designed%20to%20be%20your%20ideal%20choice%20for%20learning%20and%20practical%20use&demo-image=https%3A%2F%2Furlscan.io%2Fliveshot%2F%3Fwidth%3D1920%26height%3D1080%26url%3Dhttps%3A%2F%2Fadmin.ppanel.dev&demo-title=PPanel%20Admin%20Web&demo-url=https%3A%2F%2Fadmin.ppanel.dev%2F&from=.&project-name=ppanel-admin-web&repository-name=ppanel-web&repository-url=https%3A%2F%2Fgithub.com%2Fperfect-panel%2Fppanel-web&root-directory=apps%2Fadmin&skippable-integrations=1 diff --git a/apps/admin/.env.template b/apps/admin/.env.template new file mode 100644 index 0000000..b52d245 --- /dev/null +++ b/apps/admin/.env.template @@ -0,0 +1,17 @@ +# Default Language +NEXT_PUBLIC_DEFAULT_LANGUAGE=en-US + +# Site URL and API URL +NEXT_PUBLIC_SITE_URL=https://admin.ppanel.dev +NEXT_PUBLIC_API_URL=https://api.ppanel.dev + +# Default Login User +NEXT_PUBLIC_DEFAULT_USER_EMAIL=support@ppanel.dev +NEXT_PUBLIC_DEFAULT_USER_PASSWORD=support@ppanel.dev + +# Please put in the .env file, otherwise the i18n command will not work +# OpenAI API key and proxy URL required for i18n command (optional) +OPENAI_API_KEY= +OPENAI_PROXY_URL= + + diff --git a/apps/admin/.eslintrc.js b/apps/admin/.eslintrc.js new file mode 100644 index 0000000..a63fbc3 --- /dev/null +++ b/apps/admin/.eslintrc.js @@ -0,0 +1,14 @@ +/** @type {import("eslint").Linter.Config} */ +module.exports = { + root: true, + extends: ['@repo/eslint-config/next.js'], + parser: '@typescript-eslint/parser', + parserOptions: { + project: true, + }, + rules: { + '@typescript-eslint/no-namespace': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': 'off', + }, +}; diff --git a/apps/admin/.gitignore b/apps/admin/.gitignore new file mode 100644 index 0000000..14e8ef7 --- /dev/null +++ b/apps/admin/.gitignore @@ -0,0 +1,40 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# env files (can opt-in for committing if needed) +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/apps/admin/.i18nrc.json b/apps/admin/.i18nrc.json new file mode 100644 index 0000000..6157119 --- /dev/null +++ b/apps/admin/.i18nrc.json @@ -0,0 +1,15 @@ +{ + "entry": "./locales/zh-CN", + "entryLocale": "zh-CN", + "experimental": { + "jsonMode": true + }, + "markdown": { + "entry": ["./README.md"], + "entryLocale": "en-US", + "outputLocales": ["zh-CN"] + }, + "modelName": "gpt-3.5-turbo", + "output": "./locales", + "outputLocales": ["en-US", "zh-CN"] +} diff --git a/apps/admin/README.md b/apps/admin/README.md new file mode 100644 index 0000000..c336ce1 --- /dev/null +++ b/apps/admin/README.md @@ -0,0 +1,141 @@ + + +
+ + + +

PPanel admin web

+ +This is a PPanel admin web powered by PPanel + +English +· +[Chinese](./README.zh-CN.md) +· +[Changelog](../../CHANGELOG.md) +· +[Report Bug][issues-link] +· +[Request Feature][issues-link] + + + +[![][github-release-shield]][github-release-link] +[![][github-releasedate-shield]][github-releasedate-link] +[![][github-action-test-shield]][github-action-test-link] +[![][github-action-release-shield]][github-action-release-link]
+[![][github-contributors-shield]][github-contributors-link] +[![][github-forks-shield]][github-forks-link] +[![][github-stars-shield]][github-stars-link] +[![][github-issues-shield]][github-issues-link] +[![][github-license-shield]][github-license-link] + +![](https://urlscan.io/liveshot/?width=1920&height=1080&url=https://admin.ppanel.dev) + +
+ +
+Table of contents + +#### TOC + +- [⌨️ Local Development](#️-local-development) +- [🚀 Deploy on Vercel](#-deploy-on-vercel) +- [🤝 Contributing](#-contributing) +- [📝 License](#-license) + +#### + +
+ +## ⌨️ Local Development + +You can use Github Codespaces for online development: + +[![][codespaces-shield]][codespaces-link] + +You can use Gitpod for online development: + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][gitpod-link] + +or clone it for local development: + +```bash +git clone https://github.com/perfect-panel/ppanel-web.git +cd ppanel-web + +# Install dependencies +pnpm install + +# Run the development server +cd apps/admin +pnpm dev +``` + +Open with your browser to see the result. + +## 🚀 Deploy on Vercel + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?demo-description=PPanel%20is%20a%20pure%2C%20professional%2C%20and%20perfect%20open-source%20proxy%20panel%20tool%2C%20designed%20to%20be%20your%20ideal%20choice%20for%20learning%20and%20practical%20use&demo-image=https%3A%2F%2Furlscan.io%2Fliveshot%2F%3Fwidth%3D1920%26height%3D1080%26url%3Dhttps%3A%2F%2Fadmin.ppanel.dev&demo-title=PPanel%20Admin%20Web&demo-url=https%3A%2F%2Fadmin.ppanel.dev%2F&from=.&project-name=ppanel-admin-web&repository-name=ppanel-web&repository-url=https%3A%2F%2Fgithub.com%2Fperfect-panel%2Fppanel-web&root-directory=apps%2Fadmin&skippable-integrations=1) + +The easiest way to deploy your Next.js app is to use the +[Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) +from the creators of Next.js. + +Check out our +[Next.js deployment documentation](https://nextjs.org/docs/deployment) +for more details. + +## 🤝 Contributing + +Contributions of all types are more than welcome, +if you're interested in contributing code, feel free to check out our GitHub +[Issues][github-issues-link] to get stuck in to show us what you’re made of. + +[![][pr-welcome-shield]][pr-welcome-link] + +[![][contributors-contrib]][contributors-url] + +
+ +[![][back-to-top]](#readme-top) + +
+ +--- + +## 📝 License + +Copyright © 2024 [PPanel][profile-link].
+This project is [GUN](../../LICENSE) licensed. + + + +[back-to-top]: https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square +[codespaces-link]: https://codespaces.new/perfect-panel/ppanel-web +[codespaces-shield]: https://github.com/codespaces/badge.svg +[contributors-contrib]: https://contrib.rocks/image?repo=perfect-panel/ppanel-web +[contributors-url]: https://github.com/perfect-panel/ppanel-web/graphs/contributors +[github-action-release-link]: https://github.com/perfect-panel/ppanel-web/actions/workflows/release.yml +[github-action-release-shield]: https://img.shields.io/github/actions/workflow/status/perfect-panel/ppanel-web/release.yml?label=release&labelColor=black&logo=githubactions&logoColor=white&style=flat-square +[github-action-test-link]: https://github.com/perfect-panel/ppanel-web/actions/workflows/test.yml +[github-action-test-shield]: https://img.shields.io/github/actions/workflow/status/perfect-panel/ppanel-web/test.yml?label=test&labelColor=black&logo=githubactions&logoColor=white&style=flat-square +[github-contributors-link]: https://github.com/perfect-panel/ppanel-web/graphs/contributors +[github-contributors-shield]: https://img.shields.io/github/contributors/perfect-panel/ppanel-web?color=c4f042&labelColor=black&style=flat-square +[github-forks-link]: https://github.com/perfect-panel/ppanel-web/network/members +[github-forks-shield]: https://img.shields.io/github/forks/perfect-panel/ppanel-web?color=8ae8ff&labelColor=black&style=flat-square +[github-issues-link]: https://github.com/perfect-panel/ppanel-web/issues +[github-issues-shield]: https://img.shields.io/github/issues/perfect-panel/ppanel-web?color=ff80eb&labelColor=black&style=flat-square +[github-license-link]: https://github.com/perfect-panel/ppanel-web/blob/master/LICENSE +[github-license-shield]: https://img.shields.io/github/license/perfect-panel/ppanel-web?color=white&labelColor=black&style=flat-square +[github-release-link]: https://github.com/perfect-panel/ppanel-web/releases +[github-release-shield]: https://img.shields.io/github/v/release/perfect-panel/ppanel-web?style=flat-square&sort=semver&logo=github +[github-releasedate-link]: https://github.com/perfect-panel/ppanel-web/releases +[github-releasedate-shield]: https://img.shields.io/github/release-date/perfect-panel/ppanel-web?labelColor=black&style=flat-square +[github-stars-link]: https://github.com/perfect-panel/ppanel-web/network/stargazers +[github-stars-shield]: https://img.shields.io/github/stars/perfect-panel/ppanel-web?color=ffcb47&labelColor=black&style=flat-square +[gitpod-link]: https://gitpod.io/#https://github.com/perfect-panel/ppanel-web +[issues-link]: https://github.com/perfect-panel/ppanel-web/issues/new/choose +[pr-welcome-link]: https://github.com/perfect-panel/ppanel-web/pulls +[pr-welcome-shield]: https://img.shields.io/badge/🤯_pr_welcome-%E2%86%92-ffcb47?labelColor=black&style=for-the-badge +[profile-link]: https://github.com/perfect-panel diff --git a/apps/admin/README.zh-CN.md b/apps/admin/README.zh-CN.md new file mode 100644 index 0000000..3c440de --- /dev/null +++ b/apps/admin/README.zh-CN.md @@ -0,0 +1,141 @@ + + +
+ + + +

PPanel 管理后台

+ +这是由 PPanel 提供支持的 PPanel 管理后台 + +[英文](./README.md) +· +中文 +· +[更新日志](../../CHANGELOG.md) +· +[报告问题][issues-link] +· +[请求功能][issues-link] + + + +[![][github-release-shield]][github-release-link] +[![][github-releasedate-shield]][github-releasedate-link] +[![][github-action-test-shield]][github-action-test-link] +[![][github-action-release-shield]][github-action-release-link]
+[![][github-contributors-shield]][github-contributors-link] +[![][github-forks-shield]][github-forks-link] +[![][github-stars-shield]][github-stars-link] +[![][github-issues-shield]][github-issues-link] +[![][github-license-shield]][github-license-link] + +![](https://urlscan.io/liveshot/?width=1920&height=1080&url=https://admin.ppanel.dev) + +
+ +
+目录 + +#### 目录 + +- [⌨️ 本地开发](#️-本地开发) +- [🚀 在 Vercel 上部署](#-在-vercel-上部署) +- [🤝 贡献](#-贡献) +- [📝 许可证](#-许可证) + +#### + +
+ +## ⌨️ 本地开发 + +您可以使用 Github Codespaces 进行在线开发: + +[![][codespaces-shield]][codespaces-link] + +您可以使用 Gitpod 进行在线开发: + +[![在 Gitpod 中打开](https://gitpod.io/button/open-in-gitpod.svg)][gitpod-link] + +或者克隆项目进行本地开发: + +```bash +git clone https://github.com/perfect-panel/ppanel-web.git +cd ppanel-web + +# 安装依赖 +pnpm install + +# 运行开发服务器 +cd apps/admin +pnpm dev +``` + +在浏览器中打开 查看结果。 + +## 🚀 在 Vercel 上部署 + +[![使用 Vercel 部署](https://vercel.com/button)](https://vercel.com/new/clone?demo-description=PPanel%20is%20a%20pure%2C%20professional%2C%20and%20perfect%20open-source%20proxy%20panel%20tool%2C%20designed%20to%20be%20your%20ideal%20choice%20for%20learning%20and%20practical%20use&demo-image=https%3A%2F%2Furlscan.io%2Fliveshot%2F%3Fwidth%3D1920%26height%3D1080%26url%3Dhttps%3A%2F%2Fadmin.ppanel.dev&demo-title=PPanel%20Admin%20Web&demo-url=https%3A%2F%2Fadmin.ppanel.dev%2F&from=.&project-name=ppanel-admin-web&repository-name=ppanel-web&repository-url=https%3A%2F%2Fgithub.com%2Fperfect-panel%2Fppanel-web&root-directory=apps%2Fadmin&skippable-integrations=1) + +部署 Next.js 应用的最简单方式是使用 +[ Vercel 平台](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) +由 Next.js 的创建者提供支持。 + +查看我们的 +[Next.js 部署文档](https://nextjs.org/docs/deployment) +获取更多详情。 + +## 🤝 贡献 + +欢迎各种类型的贡献, +如果您有兴趣贡献代码,请随时查看我们的 GitHub +[问题][github-issues-link] 来展示您的能力。 + +[![][pr-welcome-shield]][pr-welcome-link] + +[![][contributors-contrib]][contributors-url] + +
+ +[![][back-to-top]](#readme-top) + +
+ +--- + +## 📝 许可证 + +版权所有 © 2024 [PPanel][profile-link]。
+本项目使用 [GUN](./LICENSE) 许可证。 + + + +[back-to-top]: https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square +[codespaces-link]: https://codespaces.new/perfect-panel/ppanel-web +[codespaces-shield]: https://github.com/codespaces/badge.svg +[contributors-contrib]: https://contrib.rocks/image?repo=perfect-panel/ppanel-web +[contributors-url]: https://github.com/perfect-panel/ppanel-web/graphs/contributors +[github-action-release-link]: https://github.com/perfect-panel/ppanel-web/actions/workflows/release.yml +[github-action-release-shield]: https://img.shields.io/github/actions/workflow/status/perfect-panel/ppanel-web/release.yml?label=release&labelColor=black&logo=githubactions&logoColor=white&style=flat-square +[github-action-test-link]: https://github.com/perfect-panel/ppanel-web/actions/workflows/test.yml +[github-action-test-shield]: https://img.shields.io/github/actions/workflow/status/perfect-panel/ppanel-web/test.yml?label=test&labelColor=black&logo=githubactions&logoColor=white&style=flat-square +[github-contributors-link]: https://github.com/perfect-panel/ppanel-web/graphs/contributors +[github-contributors-shield]: https://img.shields.io/github/contributors/perfect-panel/ppanel-web?color=c4f042&labelColor=black&style=flat-square +[github-forks-link]: https://github.com/perfect-panel/ppanel-web/network/members +[github-forks-shield]: https://img.shields.io/github/forks/perfect-panel/ppanel-web?color=8ae8ff&labelColor=black&style=flat-square +[github-issues-link]: https://github.com/perfect-panel/ppanel-web/issues +[github-issues-shield]: https://img.shields.io/github/issues/perfect-panel/ppanel-web?color=ff80eb&labelColor=black&style=flat-square +[github-license-link]: https://github.com/perfect-panel/ppanel-web/blob/master/LICENSE +[github-license-shield]: https://img.shields.io/github/license/perfect-panel/ppanel-web?color=white&labelColor=black&style=flat-square +[github-release-link]: https://github.com/perfect-panel/ppanel-web/releases +[github-release-shield]: https://img.shields.io/github/v/release/perfect-panel/ppanel-web?style=flat-square&sort=semver&logo=github +[github-releasedate-link]: https://github.com/perfect-panel/ppanel-web/releases +[github-releasedate-shield]: https://img.shields.io/github/release-date/perfect-panel/ppanel-web?labelColor=black&style=flat-square +[github-stars-link]: https://github.com/perfect-panel/ppanel-web/network/stargazers +[github-stars-shield]: https://img.shields.io/github/stars/perfect-panel/ppanel-web?color=ffcb47&labelColor=black&style=flat-square +[gitpod-link]: https://gitpod.io/#https://github.com/perfect-panel/ppanel-web +[issues-link]: https://github.com/perfect-panel/ppanel-web/issues/new/choose +[pr-welcome-link]: https://github.com/perfect-panel/ppanel-web/pulls +[pr-welcome-shield]: https://img.shields.io/badge/🤯_pr_welcome-%E2%86%92-ffcb47?labelColor=black&style=for-the-badge +[profile-link]: https://github.com/perfect-panel diff --git a/apps/admin/app/(auth)/page.tsx b/apps/admin/app/(auth)/page.tsx new file mode 100644 index 0000000..31eb3cd --- /dev/null +++ b/apps/admin/app/(auth)/page.tsx @@ -0,0 +1,54 @@ +'use client'; + +import LanguageSwitch from '@/components/language-switch'; +import useGlobalStore from '@/config/use-global'; +import { LoginIcon } from '@repo/ui/lotties'; +import { useTranslations } from 'next-intl'; +import Image from 'next/legacy/image'; +import Link from 'next/link'; + +import ThemeSwitch from '@/components/theme-switch'; +import UserAuthForm from './user-auth-form'; + +export default function Page() { + const t = useTranslations('auth'); + const { common } = useGlobalStore(); + const { site } = common; + + return ( +
+
+
+
+ + logo + {site.site_name} + + +

+ {site.site_desc} +

+
+
+
+
+
+
+ +
+
+
+ {t('tos')} +
+
+ + +
+
+
+
+
+
+
+ ); +} diff --git a/apps/admin/app/(auth)/turnstile.tsx b/apps/admin/app/(auth)/turnstile.tsx new file mode 100644 index 0000000..eea4a18 --- /dev/null +++ b/apps/admin/app/(auth)/turnstile.tsx @@ -0,0 +1,55 @@ +'use client'; + +import { useLocale } from 'next-intl'; +import { useTheme } from 'next-themes'; +import { useEffect } from 'react'; +import Turnstile, { useTurnstile } from 'react-turnstile'; + +import useGlobalStore from '@/config/use-global'; + +export default function CloudFlareTurnstile({ + id, + value, + onChange, +}: { + id?: string; + value?: null | string; + onChange: (value?: string) => void; +}) { + const { common } = useGlobalStore(); + const { verify } = common; + const { resolvedTheme } = useTheme(); + const locale = useLocale(); + const turnstile = useTurnstile(); + + useEffect(() => { + if (value === '') { + turnstile.reset(); + } + }, [turnstile, value]); + + return ( + verify.turnstile_site_key && ( + onChange(token)} + // onError={() => { + // onChange(); + // turnstile.reset(); + // }} + onExpire={() => { + onChange(); + turnstile.reset(); + }} + onTimeout={() => { + onChange(); + turnstile.reset(); + }} + /> + ) + ); +} diff --git a/apps/admin/app/(auth)/user-auth-form.tsx b/apps/admin/app/(auth)/user-auth-form.tsx new file mode 100644 index 0000000..8f6b31b --- /dev/null +++ b/apps/admin/app/(auth)/user-auth-form.tsx @@ -0,0 +1,165 @@ +'use client'; + +import { + NEXT_PUBLIC_DEFAULT_USER_EMAIL, + NEXT_PUBLIC_DEFAULT_USER_PASSWORD, +} from '@/config/constants'; +import useGlobalStore from '@/config/use-global'; +import { checkUser, resetPassword, userLogin, userRegister } from '@/services/common/auth'; +import { getRedirectUrl, setAuthorization } from '@/utils/common'; +import { Icon } from '@iconify/react'; +import { Button } from '@shadcn/ui/button'; +import { toast } from '@shadcn/ui/lib/sonner'; +import { cn } from '@shadcn/ui/lib/utils'; +import { useTranslations } from 'next-intl'; +import { useRouter } from 'next/navigation'; +import { ReactNode, useState, useTransition } from 'react'; +import UserCheckForm from './user-check-form'; +import UserLoginForm from './user-login-form'; +import UserRegisterForm from './user-register-form'; +import UserResetForm from './user-reset-form'; + +export default function UserAuthForm() { + const t = useTranslations('auth'); + const { common } = useGlobalStore(); + const { register } = common; + const router = useRouter(); + const [type, setType] = useState<'login' | 'register' | 'reset'>(); + const [loading, startTransition] = useTransition(); + const [initialValues, setInitialValues] = useState<{ + email?: string; + password?: string; + }>({ + email: NEXT_PUBLIC_DEFAULT_USER_EMAIL, + password: NEXT_PUBLIC_DEFAULT_USER_PASSWORD, + }); + + const handleFormSubmit = async (params: any) => { + const onLogin = async (token?: string) => { + if (!token) return; + setAuthorization(token); + router.replace(getRedirectUrl()); + router.refresh(); + }; + startTransition(async () => { + try { + switch (type) { + case 'login': + // eslint-disable-next-line no-case-declarations + const login = await userLogin(params); + toast.success(t('login.success')); + onLogin(login.data.data?.token); + break; + case 'register': + // eslint-disable-next-line no-case-declarations + const create = await userRegister(params); + toast.success(t('register.success')); + onLogin(create.data.data?.token); + break; + case 'reset': + await resetPassword(params); + toast.success(t('reset.success')); + setType('login'); + break; + default: + if (type === 'reset') break; + // eslint-disable-next-line no-case-declarations + const response = await checkUser(params); + setInitialValues({ + ...initialValues, + ...params, + }); + setType(response.data.data?.exist ? 'login' : 'register'); + break; + } + } catch (error) { + /* empty */ + } + }); + }; + let UserForm: ReactNode = null; + switch (type) { + case 'login': + UserForm = ( + + ); + break; + case 'register': + UserForm = ( + + ); + break; + case 'reset': + UserForm = ( + + ); + break; + default: + UserForm = ( + + ); + break; + } + + return ( + <> +
+

{t(`${type || 'check'}.title`)}

+
+ {t(`${type || 'check'}.description`)} +
+
+ {!((type === 'register' && register.stop_register) || type === 'reset') && ( + <> +
+ + + +
+
+ {t('orWithEmail')} +
+ + )} + + {UserForm} + + ); +} diff --git a/apps/admin/app/(auth)/user-check-form.tsx b/apps/admin/app/(auth)/user-check-form.tsx new file mode 100644 index 0000000..506db63 --- /dev/null +++ b/apps/admin/app/(auth)/user-check-form.tsx @@ -0,0 +1,110 @@ +import useGlobalStore from '@/config/use-global'; +import { Icon } from '@iconify/react'; +import { Button } from '@shadcn/ui/button'; +import { Form, FormControl, FormField, FormItem, FormMessage } from '@shadcn/ui/form'; +import { Input } from '@shadcn/ui/input'; +import { useForm } from '@shadcn/ui/lib/react-hook-form'; +import { z, zodResolver } from '@shadcn/ui/lib/zod'; +import { useTranslations } from 'next-intl'; +import { useEffect, useRef, useState } from 'react'; + +export default function UserCheckForm({ + loading, + onSubmit, + initialValues, +}: { + loading?: boolean; + onSubmit: (data: any) => void; + initialValues: any; +}) { + const t = useTranslations('auth.check'); + const { common } = useGlobalStore(); + const { register } = common; + const formSchema = z.object({ + email: z + .string() + .email(t('email')) + .refine( + (email) => { + if (!register.enable_email_domain_suffix) return true; + const domain = email.split('@')[1] as string; + return register.email_domain_suffix_list.split('\n').includes(domain); + }, + { + message: t('whitelist'), + }, + ), + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: initialValues, + }); + + const [isSubmitting, setIsSubmitting] = useState(false); + const typingTimeoutRef = useRef(null); + + const handleEmailChange = (value: string) => { + form.setValue('email', value); + + if (typingTimeoutRef.current) { + clearTimeout(typingTimeoutRef.current); + } + + typingTimeoutRef.current = setTimeout(async () => { + const isValid = await form.trigger('email'); + if (isValid) { + setIsSubmitting(true); + form.handleSubmit(onSubmit)(); + } else { + setIsSubmitting(false); + } + }, 500); + }; + + useEffect(() => { + if (initialValues && initialValues.email) { + handleEmailChange(initialValues.email); + } + return () => { + if (typingTimeoutRef.current) { + clearTimeout(typingTimeoutRef.current); + } + }; + }, []); + + return ( +
+ + ( + + + handleEmailChange(e.target.value)} + /> + + + + )} + /> + + + + ); +} diff --git a/apps/admin/app/(auth)/user-login-form.tsx b/apps/admin/app/(auth)/user-login-form.tsx new file mode 100644 index 0000000..2db5d98 --- /dev/null +++ b/apps/admin/app/(auth)/user-login-form.tsx @@ -0,0 +1,110 @@ +import useGlobalStore from '@/config/use-global'; +import { Icon } from '@iconify/react'; +import { Button } from '@shadcn/ui/button'; +import { Form, FormControl, FormField, FormItem, FormMessage } from '@shadcn/ui/form'; +import { Input } from '@shadcn/ui/input'; +import { useForm } from '@shadcn/ui/lib/react-hook-form'; +import { z, zodResolver } from '@shadcn/ui/lib/zod'; +import { useTranslations } from 'next-intl'; +import { Dispatch, SetStateAction } from 'react'; + +import CloudFlareTurnstile from './turnstile'; + +export default function UserLoginForm({ + loading, + onSubmit, + initialValues, + setInitialValues, + onSwitchForm, +}: { + loading?: boolean; + onSubmit: (data: any) => void; + initialValues: any; + setInitialValues: Dispatch>; + onSwitchForm: (type?: 'register' | 'reset') => void; +}) { + const t = useTranslations('auth.login'); + const { common } = useGlobalStore(); + const { verify } = common; + + const formSchema = z.object({ + email: z.string(), + password: z.string(), + cf_token: verify.enable_login_verify ? z.string() : z.string().optional(), + }); + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: initialValues, + }); + + return ( + <> +
+ + ( + + + + + + + )} + /> + ( + + + + + + + )} + /> + {verify.enable_login_verify && ( + ( + + + + + + + )} + /> + )} + + + +
+ + +
+ + ); +} diff --git a/apps/admin/app/(auth)/user-register-form.tsx b/apps/admin/app/(auth)/user-register-form.tsx new file mode 100644 index 0000000..a0d12de --- /dev/null +++ b/apps/admin/app/(auth)/user-register-form.tsx @@ -0,0 +1,205 @@ +import useGlobalStore from '@/config/use-global'; +import { sendEmailCode } from '@/services/common/common'; +import { Icon } from '@iconify/react'; +import { Markdown } from '@repo/ui/markdown'; +import { Button } from '@shadcn/ui/button'; +import { Form, FormControl, FormField, FormItem, FormMessage } from '@shadcn/ui/form'; +import { Input } from '@shadcn/ui/input'; +import { useForm } from '@shadcn/ui/lib/react-hook-form'; +import { z, zodResolver } from '@shadcn/ui/lib/zod'; +import { useCountDown } from 'ahooks'; +import { useTranslations } from 'next-intl'; +import { Dispatch, SetStateAction, useState } from 'react'; + +import CloudFlareTurnstile from './turnstile'; + +export default function UserRegisterForm({ + loading, + onSubmit, + initialValues, + setInitialValues, + onSwitchForm, +}: { + loading?: boolean; + onSubmit: (data: any) => void; + initialValues: any; + setInitialValues: Dispatch>; + onSwitchForm: (type?: 'register' | 'reset') => void; +}) { + const t = useTranslations('auth.register'); + const { common } = useGlobalStore(); + const { verify, register, invite } = common; + + const [targetDate, setTargetDate] = useState(); + const [, { seconds }] = useCountDown({ + targetDate, + onEnd: () => { + setTargetDate(undefined); + }, + }); + const handleSendCode = async () => { + await sendEmailCode({ + email: initialValues.email, + type: 1, + }); + setTargetDate(Date.now() + 60000); + }; + + const formSchema = z + .object({ + email: z.string(), + password: z.string(), + repeat_password: z.string(), + code: register.enable_email_verify ? z.string() : z.string().nullish(), + invite: invite.forced_invite ? z.string() : z.string().nullish(), + cf_token: verify.enable_register_verify ? z.string() : z.string().nullish(), + }) + .superRefine(({ password, repeat_password }, ctx) => { + if (password !== repeat_password) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: t('passwordMismatch'), + path: ['repeat_password'], + }); + } + }); + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + ...initialValues, + invite: sessionStorage.getItem('invite'), + }, + }); + + return ( + <> + {register.stop_register ? ( + {t('message')} + ) : ( +
+ + ( + + + + + + + )} + /> + ( + + + + + + + )} + /> + ( + + + + + + + )} + /> + {register.enable_email_verify && ( + ( + + +
+ + +
+
+ +
+ )} + /> + )} + ( + + + + + + + )} + /> + {verify.enable_register_verify && ( + ( + + + + + + + )} + /> + )} + + + + )} +
+ {t('existingAccount')} + +
+ + ); +} diff --git a/apps/admin/app/(auth)/user-reset-form.tsx b/apps/admin/app/(auth)/user-reset-form.tsx new file mode 100644 index 0000000..6412d92 --- /dev/null +++ b/apps/admin/app/(auth)/user-reset-form.tsx @@ -0,0 +1,153 @@ +import useGlobalStore from '@/config/use-global'; +import { sendEmailCode } from '@/services/common/common'; +import { Icon } from '@iconify/react'; +import { Button } from '@shadcn/ui/button'; +import { Form, FormControl, FormField, FormItem, FormMessage } from '@shadcn/ui/form'; +import { Input } from '@shadcn/ui/input'; +import { useForm } from '@shadcn/ui/lib/react-hook-form'; +import { z, zodResolver } from '@shadcn/ui/lib/zod'; +import { useCountDown } from 'ahooks'; +import { useTranslations } from 'next-intl'; +import { Dispatch, SetStateAction, useState } from 'react'; + +import CloudFlareTurnstile from './turnstile'; + +export default function UserResetForm({ + loading, + onSubmit, + initialValues, + setInitialValues, + onSwitchForm, +}: { + loading?: boolean; + onSubmit: (data: any) => void; + initialValues: any; + setInitialValues: Dispatch>; + onSwitchForm: (type?: 'register' | 'reset') => void; +}) { + const t = useTranslations('auth.reset'); + + const { common } = useGlobalStore(); + const { verify, register } = common; + + const [targetDate, setTargetDate] = useState(); + const [, { seconds }] = useCountDown({ + targetDate, + onEnd: () => { + setTargetDate(undefined); + }, + }); + const handleSendCode = async () => { + await sendEmailCode({ + email: initialValues.email, + type: 2, + }); + setTargetDate(Date.now() + 60000); // 60秒倒计时 + }; + + const formSchema = z.object({ + email: z.string(), + password: z.string(), + code: register.enable_email_verify ? z.string() : z.string().nullish(), + cf_token: verify.enable_register_verify ? z.string() : z.string().nullish(), + }); + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: initialValues, + }); + + return ( + <> +
+ + ( + + + + + + + )} + /> + ( + + + + + + + )} + /> + + {register.enable_email_verify && ( + ( + + +
+ + +
+
+ +
+ )} + /> + )} + {verify.enable_reset_password_verify && ( + ( + + + + + + + )} + /> + )} + + + +
+ {t('existingAccount')} + +
+ + ); +} diff --git a/apps/admin/app/dashboard/announcement/notice-form.tsx b/apps/admin/app/dashboard/announcement/notice-form.tsx new file mode 100644 index 0000000..8b8863b --- /dev/null +++ b/apps/admin/app/dashboard/announcement/notice-form.tsx @@ -0,0 +1,134 @@ +import { Icon } from '@iconify/react'; +import { MarkdownEditor } from '@repo/ui/editor'; +import { Button } from '@shadcn/ui/button'; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@shadcn/ui/form'; +import { Input } from '@shadcn/ui/input'; +import { useForm } from '@shadcn/ui/lib/react-hook-form'; +import { z, zodResolver } from '@shadcn/ui/lib/zod'; +import { ScrollArea } from '@shadcn/ui/scroll-area'; +import { + Sheet, + SheetContent, + SheetFooter, + SheetHeader, + SheetTitle, + SheetTrigger, +} from '@shadcn/ui/sheet'; +import { useTranslations } from 'next-intl'; +import { useTheme } from 'next-themes'; +import { useEffect, useState } from 'react'; + +const formSchema = z.object({ + title: z.string(), + content: z.string().optional(), +}); + +interface AnnouncementFormProps { + onSubmit: (data: T) => Promise | boolean; + initialValues?: T; + loading?: boolean; + trigger: string; + title: string; +} + +export default function AnnouncementForm>({ + onSubmit, + initialValues, + loading, + trigger, + title, +}: AnnouncementFormProps) { + const t = useTranslations('announcement'); + const { resolvedTheme } = useTheme(); + const [open, setOpen] = useState(false); + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + title: '', + content: '', + ...initialValues, + }, + }); + + useEffect(() => { + form?.reset(initialValues); + }, [form, initialValues]); + + async function handleSubmit(data: { [x: string]: any }) { + const bool = await onSubmit(data as T); + if (bool) setOpen(false); + } + + return ( + + + + + + + {title} + + +
+ + ( + + {t('form.title')} + + + + + + )} + /> + + ( + + {t('form.content')} + + { + form.setValue(field.name, value || ''); + }} + /> + + + + )} + /> + + +
+ + + + +
+
+ ); +} diff --git a/apps/admin/app/dashboard/announcement/page.tsx b/apps/admin/app/dashboard/announcement/page.tsx new file mode 100644 index 0000000..49ada2b --- /dev/null +++ b/apps/admin/app/dashboard/announcement/page.tsx @@ -0,0 +1,173 @@ +'use client'; + +import { + createAnnouncement, + deleteAnnouncement, + getAnnouncementList, + updateAnnouncement, + updateAnnouncementEnable, +} from '@/services/admin/announcement'; +import { ConfirmButton } from '@repo/ui/confirm-button'; +import { format } from '@shadcn/ui/lib/date-fns'; +import { toast } from '@shadcn/ui/lib/sonner'; +import { Switch } from '@shadcn/ui/switch'; +import { useTranslations } from 'next-intl'; +import { useRef, useState } from 'react'; + +import { ProTable, ProTableActions } from '@/components/pro-table'; +import { Button } from '@shadcn/ui/button'; +import NoticeForm from './notice-form'; + +export default function Page() { + const t = useTranslations('announcement'); + const [loading, setLoading] = useState(false); + const ref = useRef(); + + return ( + + action={ref} + header={{ + title: t('announcementList'), + toolbar: ( + + trigger={t('create')} + title={t('createAnnouncement')} + loading={loading} + onSubmit={async (values) => { + setLoading(true); + try { + await createAnnouncement(values); + toast.success(t('createSuccess')); + ref.current?.refresh(); + setLoading(false); + return true; + } catch (error) { + setLoading(false); + return false; + } + }} + /> + ), + }} + columns={[ + { + accessorKey: 'enable', + header: t('enable'), + cell: ({ row }) => { + return ( + { + await updateAnnouncementEnable({ + id: row.original.id, + enable: checked, + }); + ref.current?.refresh(); + }} + /> + ); + }, + }, + { + accessorKey: 'title', + header: t('title'), + }, + { + accessorKey: 'content', + header: t('content'), + }, + { + accessorKey: 'updated_at', + header: t('updatedAt'), + cell: ({ row }) => format(row.getValue('updated_at'), 'yyyy-MM-dd HH:mm:ss'), + }, + ]} + params={[ + { + key: 'enable', + placeholder: t('enable'), + options: [ + { label: t('show'), value: 'false' }, + { label: t('hide'), value: 'true' }, + ], + }, + { key: 'search' }, + ]} + request={async (pagination, filter) => { + const { data } = await getAnnouncementList({ + ...pagination, + ...filter, + }); + return { + list: data.data?.list || [], + total: data.data?.total || 0, + }; + }} + actions={{ + render(row) { + return [ + + key='edit' + trigger={t('edit')} + title={t('editAnnouncement')} + loading={loading} + initialValues={row} + onSubmit={async (values) => { + setLoading(true); + try { + await updateAnnouncement({ + ...row, + ...values, + }); + toast.success(t('updateSuccess')); + ref.current?.refresh(); + setLoading(false); + return true; + } catch (error) { + setLoading(false); + return false; + } + }} + />, + {t('delete')}} + title={t('confirmDelete')} + description={t('deleteDescription')} + onConfirm={async () => { + await deleteAnnouncement({ + id: row.id, + }); + toast.success(t('deleteSuccess')); + ref.current?.refresh(); + }} + cancelText={t('cancel')} + confirmText={t('confirm')} + />, + ]; + }, + batchRender(rows) { + return [ + {t('delete')}} + title={t('confirmDelete')} + description={t('deleteDescription')} + onConfirm={async () => { + for (const element of rows) { + await deleteAnnouncement({ + id: element.id!, + }); + } + toast.success(t('deleteSuccess')); + ref.current?.refresh(); + }} + cancelText={t('cancel')} + confirmText={t('confirm')} + />, + ]; + }, + }} + /> + ); +} diff --git a/apps/admin/app/dashboard/coupon/coupon-form.tsx b/apps/admin/app/dashboard/coupon/coupon-form.tsx new file mode 100644 index 0000000..57c3399 --- /dev/null +++ b/apps/admin/app/dashboard/coupon/coupon-form.tsx @@ -0,0 +1,353 @@ +'use client'; + +import { getSubscribeList } from '@/services/admin/subscribe'; +import { Icon } from '@iconify/react'; +import { Combobox } from '@repo/ui/combobox'; +import { DatePicker } from '@repo/ui/date-picker'; +import { EnhancedInput } from '@repo/ui/enhanced-input'; +import { unitConversion } from '@repo/ui/utils'; +import { Button } from '@shadcn/ui/button'; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@shadcn/ui/form'; +import { useForm } from '@shadcn/ui/lib/react-hook-form'; +import { z, zodResolver } from '@shadcn/ui/lib/zod'; +import { RadioGroup, RadioGroupItem } from '@shadcn/ui/radio-group'; +import { ScrollArea } from '@shadcn/ui/scroll-area'; +import { + Sheet, + SheetContent, + SheetFooter, + SheetHeader, + SheetTitle, + SheetTrigger, +} from '@shadcn/ui/sheet'; +import { useQuery } from '@tanstack/react-query'; +import { useTranslations } from 'next-intl'; +import { useEffect, useState } from 'react'; + +const formSchema = z.object({ + name: z.string(), + code: z.string().optional(), + count: z.number().optional(), + type: z.number().optional(), + discount: z.number().optional(), + start_time: z.number().optional(), + expire_time: z.number().optional(), + subscribe: z.array(z.number()).nullish(), + user_limit: z.number().optional(), +}); + +interface CouponFormProps { + onSubmit: (data: T) => Promise | boolean; + initialValues?: T; + loading?: boolean; + trigger: string; + title: string; +} + +export default function CouponForm>({ + onSubmit, + initialValues, + loading, + trigger, + title, +}: CouponFormProps) { + const t = useTranslations('coupon'); + + const [open, setOpen] = useState(false); + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + type: 1, + ...initialValues, + } as any, + }); + + useEffect(() => { + form?.reset(initialValues); + }, [form, initialValues]); + + async function handleSubmit(data: { [x: string]: any }) { + const bool = await onSubmit(data as T); + if (bool) setOpen(false); + } + + const type = form.watch('type'); + + const { data: subscribe } = useQuery({ + queryKey: ['getSubscribeList', 'all'], + queryFn: async () => { + const { data } = await getSubscribeList({ + page: 1, + size: 9999, + }); + return data.data?.list as API.Subscribe[]; + }, + }); + + return ( + + + + + + + {title} + + +
+ + ( + + {t('form.name')} + + { + form.setValue(field.name, value); + }} + /> + + + + )} + /> + ( + + {t('form.customCouponCode')} + + { + form.setValue(field.name, value); + }} + /> + + + + )} + /> + ( + + {t('form.type')} + + { + form.setValue(field.name, Number(value)); + form.setValue('discount', ''); + }} + className='flex gap-2' + > + + + + + + {t('form.percentageDiscount')} + + + + + + + {t('form.amountDiscount')} + + + + + + )} + /> + {type === 1 && ( + ( + + {t('form.percentageDiscount')} + + { + form.setValue(field.name, value); + }} + min={1} + max={100} + /> + + + + )} + /> + )} + {type === 2 && ( + ( + + {t('form.amountDiscount')} + + unitConversion('centsToDollars', value)} + formatOutput={(value) => unitConversion('dollarsToCents', value)} + onValueChange={(value) => { + form.setValue(field.name, value); + }} + /> + + + + )} + /> + )} + ( + + {t('form.specifiedServer')} + + + multiple + placeholder={t('form.selectServer')} + value={field.value} + onChange={(value) => { + form.setValue(field.name, value); + }} + options={subscribe?.map((item: API.Subscribe) => ({ + value: item.id, + label: item.name, + }))} + /> + + + + )} + /> + ( + + {t('form.startTime')} + + date < new Date(Date.now() - 24 * 60 * 60 * 1000)} + onChange={(value) => { + form.setValue(field.name, value); + }} + /> + + + + )} + /> + ( + + {t('form.expireTime')} + + { + form.setValue(field.name, value); + }} + /> + + + + )} + /> + + ( + + {t('form.count')} + + { + form.setValue(field.name, value); + }} + /> + + + + )} + /> + ( + + {t('form.userLimit')} + + { + form.setValue(field.name, value); + }} + /> + + + + )} + /> + + +
+ + + + +
+
+ ); +} diff --git a/apps/admin/app/dashboard/coupon/page.tsx b/apps/admin/app/dashboard/coupon/page.tsx new file mode 100644 index 0000000..673c89c --- /dev/null +++ b/apps/admin/app/dashboard/coupon/page.tsx @@ -0,0 +1,232 @@ +'use client'; + +import { Display } from '@/components/display'; +import { ProTable, ProTableActions } from '@/components/pro-table'; +import { + batchDeleteCoupon, + createCoupon, + deleteCoupon, + getCouponList, + updateCoupon, +} from '@/services/admin/coupon'; +import { getSubscribeList } from '@/services/admin/subscribe'; +import { ConfirmButton } from '@repo/ui/confirm-button'; +import { formatDate } from '@repo/ui/utils'; +import { Badge } from '@shadcn/ui/badge'; +import { Button } from '@shadcn/ui/button'; +import { toast } from '@shadcn/ui/lib/sonner'; +import { Switch } from '@shadcn/ui/switch'; +import { useQuery } from '@tanstack/react-query'; +import { useTranslations } from 'next-intl'; +import { useRef, useState } from 'react'; +import CouponForm from './coupon-form'; + +export default function Page() { + const t = useTranslations('coupon'); + const [loading, setLoading] = useState(false); + const { data } = useQuery({ + queryKey: ['getSubscribeList', 'all'], + queryFn: async () => { + const { data } = await getSubscribeList({ + page: 1, + size: 9999, + }); + return data.data?.list as API.SubscribeGroup[]; + }, + }); + const ref = useRef(); + return ( + + action={ref} + header={{ + toolbar: ( + + trigger={t('create')} + title={t('createCoupon')} + loading={loading} + onSubmit={async (values) => { + setLoading(true); + try { + await createCoupon({ + ...values, + enable: false, + }); + toast.success(t('createSuccess')); + ref.current?.refresh(); + setLoading(false); + return true; + } catch (error) { + setLoading(false); + return false; + } + }} + /> + ), + }} + params={[ + { + key: 'search', + }, + { + key: 'subscribe', + placeholder: t('subscribe'), + options: data?.map((item) => ({ + label: item.name, + value: String(item.id), + })), + }, + ]} + request={async (pagination, filters) => { + const { data } = await getCouponList({ + ...pagination, + ...filters, + }); + return { + list: data.data?.list || [], + total: data.data?.total || 0, + }; + }} + columns={[ + { + accessorKey: 'enable', + header: t('enable'), + cell: ({ row }) => { + return ( + { + await updateCoupon({ + ...row, + enable: checked, + } as any); + ref.current?.refresh(); + }} + /> + ); + }, + }, + { + accessorKey: 'name', + header: t('name'), + }, + { + accessorKey: 'code', + header: t('code'), + }, + { + accessorKey: 'type', + header: t('type'), + cell: ({ row }) => ( + + {row.getValue('type') === 1 ? t('percentage') : t('amount')} + + ), + }, + { + accessorKey: 'discount', + header: t('discount'), + cell: ({ row }) => ( + + {row.getValue('type') === 1 ? ( + `${row.original.discount} %` + ) : ( + + )} + + ), + }, + { + accessorKey: 'count', + header: t('count'), + cell: ({ row }) => ( +
+ + {t('count')}: {row.original.count === 0 ? t('unlimited') : row.original.count} + + + {t('remainingTimes')}:{' '} + {row.original.count === 0 + ? t('unlimited') + : row.original.count - row.original.used_count} + + + {t('usedTimes')}: {row.original.used_count} + +
+ ), + }, + { + accessorKey: 'expire', + header: t('validityPeriod'), + cell: ({ row }) => { + const { start_time, expire_time } = row.original; + if (start_time) { + return expire_time ? ( + <> + {formatDate(start_time)} - {formatDate(expire_time)} + + ) : start_time ? ( + formatDate(start_time) + ) : ( + '--' + ); + } + return '--'; + }, + }, + ]} + actions={{ + render: (row) => [ + + key='edit' + trigger={t('edit')} + title={t('editCoupon')} + loading={loading} + initialValues={row} + onSubmit={async (values) => { + setLoading(true); + try { + await updateCoupon({ ...row, ...values }); + toast.success(t('updateSuccess')); + ref.current?.refresh(); + setLoading(false); + return true; + } catch (error) { + setLoading(false); + return false; + } + }} + />, + {t('delete')}} + title={t('confirmDelete')} + description={t('deleteWarning')} + onConfirm={async () => { + await deleteCoupon({ id: row.id }); + toast.success(t('deleteSuccess')); + ref.current?.refresh(); + }} + cancelText={t('cancel')} + confirmText={t('confirm')} + />, + ], + batchRender: (rows) => [ + {t('delete')}} + title={t('confirmDelete')} + description={t('deleteWarning')} + onConfirm={async () => { + await batchDeleteCoupon({ ids: rows.map((item) => item.id) }); + toast.success(t('deleteSuccess')); + ref.current?.reset(); + }} + cancelText={t('cancel')} + confirmText={t('confirm')} + />, + ], + }} + /> + ); +} diff --git a/apps/admin/app/dashboard/document/document-form.tsx b/apps/admin/app/dashboard/document/document-form.tsx new file mode 100644 index 0000000..a57b5f0 --- /dev/null +++ b/apps/admin/app/dashboard/document/document-form.tsx @@ -0,0 +1,154 @@ +import { Icon } from '@iconify/react'; +import { MarkdownEditor } from '@repo/ui/editor'; +import { TagInput } from '@repo/ui/tag-input'; +import { Button } from '@shadcn/ui/button'; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@shadcn/ui/form'; +import { Input } from '@shadcn/ui/input'; +import { useForm } from '@shadcn/ui/lib/react-hook-form'; +import { z, zodResolver } from '@shadcn/ui/lib/zod'; +import { ScrollArea } from '@shadcn/ui/scroll-area'; +import { + Sheet, + SheetContent, + SheetFooter, + SheetHeader, + SheetTitle, + SheetTrigger, +} from '@shadcn/ui/sheet'; +import { useTranslations } from 'next-intl'; +import { useTheme } from 'next-themes'; +import { useEffect, useState } from 'react'; + +const formSchema = z.object({ + title: z.string(), + tags: z.array(z.string()).nullish(), + content: z.string().nullish(), +}); + +interface DocumentFormProps { + onSubmit: (data: T) => Promise | boolean; + initialValues?: T; + loading?: boolean; + trigger: string; + title: string; +} + +export default function DocumentForm>({ + onSubmit, + initialValues, + loading, + trigger, + title, +}: DocumentFormProps) { + const t = useTranslations('document'); + const { resolvedTheme } = useTheme(); + const [open, setOpen] = useState(false); + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + tags: [], + ...initialValues, + } as any, + }); + + useEffect(() => { + form?.reset({ + tags: [], + ...initialValues, + }); + }, [form, initialValues]); + + async function handleSubmit(data: { [x: string]: any }) { + const bool = await onSubmit(data as T); + if (bool) setOpen(false); + } + + return ( + + + + + + + {title} + + +
+ + ( + + {t('form.title')} + + + + + + )} + /> + ( + + {t('form.tags')} + + form.setValue(field.name, value)} + /> + + + + )} + /> + ( + + {t('form.content')} + + { + form.setValue(field.name, value); + }} + /> + + + + )} + /> + + +
+ + + + +
+
+ ); +} diff --git a/apps/admin/app/dashboard/document/page.tsx b/apps/admin/app/dashboard/document/page.tsx new file mode 100644 index 0000000..6c548d8 --- /dev/null +++ b/apps/admin/app/dashboard/document/page.tsx @@ -0,0 +1,170 @@ +'use client'; + +import { ProTable, ProTableActions } from '@/components/pro-table'; +import { + batchDeleteDocument, + createDocument, + deleteDocument, + getDocumentList, + updateDocument, +} from '@/services/admin/document'; +import { ConfirmButton } from '@repo/ui/confirm-button'; +import { formatDate } from '@repo/ui/utils'; +import { Button } from '@shadcn/ui/button'; +import { toast } from '@shadcn/ui/lib/sonner'; +import { Switch } from '@shadcn/ui/switch'; +import { useTranslations } from 'next-intl'; +import { useRef, useState } from 'react'; +import DocumentForm from './document-form'; + +export default function Page() { + const t = useTranslations('document'); + const [loading, setLoading] = useState(false); + + const ref = useRef(); + return ( + + action={ref} + header={{ + title: t('DocumentList'), + toolbar: ( + + key='create' + trigger={t('create')} + title={t('createDocument')} + loading={loading} + onSubmit={async (values) => { + setLoading(true); + try { + await createDocument({ + ...values, + show: false, + }); + toast.success(t('createSuccess')); + ref.current?.refresh(); + setLoading(false); + return true; + } catch (error) { + setLoading(false); + return false; + } + }} + /> + ), + }} + columns={[ + { + accessorKey: 'show', + header: t('show'), + cell: ({ row }) => { + return ( + { + await updateDocument({ + ...row.original, + show: checked, + }); + ref.current?.refresh(); + }} + /> + ); + }, + }, + { + accessorKey: 'title', + header: t('title'), + }, + { + accessorKey: 'tags', + header: t('tags'), + cell: ({ row }) => row.original.tags.join(', '), + }, + { + accessorKey: 'updated_at', + header: t('updatedAt'), + cell: ({ row }) => formatDate(row.getValue('updated_at')), + }, + ]} + params={[ + { + key: 'search', + }, + { + key: 'tag', + placeholder: t('tags'), + }, + ]} + request={async (pagination, filter) => { + const { data } = await getDocumentList({ ...pagination, ...filter }); + return { + list: data.data?.list || [], + total: data.data?.total || 0, + }; + }} + actions={{ + render(row) { + return [ + + key='edit' + trigger={t('edit')} + title={t('editDocument')} + loading={loading} + initialValues={row} + onSubmit={async (values) => { + setLoading(true); + try { + await updateDocument({ + ...row, + ...values, + }); + toast.success(t('updateSuccess')); + ref.current?.refresh(); + setLoading(false); + return true; + } catch (error) { + setLoading(false); + return false; + } + }} + />, + {t('delete')}} + title={t('confirmDelete')} + description={t('deleteDescription')} + onConfirm={async () => { + await deleteDocument({ + id: row.id, + }); + toast.success(t('deleteSuccess')); + ref.current?.refresh(); + }} + cancelText={t('cancel')} + confirmText={t('confirm')} + />, + ]; + }, + batchRender(rows) { + return [ + {t('delete')}} + title={t('confirmDelete')} + description={t('deleteDescription')} + onConfirm={async () => { + await batchDeleteDocument({ + ids: rows.map((item) => item.id), + }); + toast.success(t('deleteSuccess')); + ref.current?.refresh(); + }} + cancelText={t('cancel')} + confirmText={t('confirm')} + />, + ]; + }, + }} + /> + ); +} diff --git a/apps/admin/app/dashboard/layout.tsx b/apps/admin/app/dashboard/layout.tsx new file mode 100644 index 0000000..c92782c --- /dev/null +++ b/apps/admin/app/dashboard/layout.tsx @@ -0,0 +1,21 @@ +import { Header } from '@/components/header'; +import { SidebarLeft } from '@/components/sidebar-left'; +import { ScrollArea } from '@shadcn/ui/scroll-area'; +import { SidebarInset, SidebarProvider } from '@shadcn/ui/sidebar'; +import { cookies } from 'next/headers'; + +export default async function DashboardLayout({ children }: { children: React.ReactNode }) { + const cookieStore = await cookies(); + const defaultOpen = cookieStore.get('sidebar:state')?.value === 'true'; + return ( + + + +
+
+ {children} +
+ + + ); +} diff --git a/apps/admin/app/dashboard/order/page.tsx b/apps/admin/app/dashboard/order/page.tsx new file mode 100644 index 0000000..e984982 --- /dev/null +++ b/apps/admin/app/dashboard/order/page.tsx @@ -0,0 +1,185 @@ +'use client'; + +import { useQuery } from '@tanstack/react-query'; +import { useTranslations } from 'next-intl'; +import { useRef } from 'react'; + +import { Display } from '@/components/display'; +import { ProTable, ProTableActions } from '@/components/pro-table'; +import { getOrderList, updateOrderStatus } from '@/services/admin/order'; +import { getSubscribeList } from '@/services/admin/subscribe'; +import { Combobox } from '@repo/ui/combobox'; +import { formatDate } from '@repo/ui/utils'; +import { Badge } from '@shadcn/ui/badge'; +import { Button } from '@shadcn/ui/button'; +import { HoverCard, HoverCardContent, HoverCardTrigger } from '@shadcn/ui/hover-card'; +import { cn } from '@shadcn/ui/lib/utils'; +import { Separator } from '@shadcn/ui/separator'; +import { UserDetail } from '../user/user-detail'; + +export default function Page() { + const t = useTranslations('order'); + + const statusOptions = [ + { value: 1, label: t('status.1'), className: 'bg-orange-500' }, + { value: 2, label: t('status.2'), className: 'bg-green-500' }, + { value: 3, label: t('status.3'), className: 'bg-gray-500' }, + { value: 4, label: t('status.4'), className: 'bg-red-500' }, + { value: 5, label: t('status.5'), className: 'bg-green-500' }, + ]; + + const ref = useRef(); + + const { data: subscribeList } = useQuery({ + queryKey: ['getSubscribeList', 'all'], + queryFn: async () => { + const { data } = await getSubscribeList({ + page: 1, + size: 9999, + }); + return data.data?.list as API.SubscribeGroup[]; + }, + }); + + return ( + + action={ref} + columns={[ + { + accessorKey: 'order_no', + header: t('orderNumber'), + }, + { + accessorKey: 'type', + header: t('type.0'), + cell: ({ row }) => t(`type.${row.getValue('type')}`), + }, + { + accessorKey: 'subscribe_id', + header: t('subscribe'), + cell: ({ row }) => + subscribeList?.find((item) => item.id === row.getValue('subscribe_id'))?.name, + }, + { + accessorKey: 'amount', + header: t('amount'), + cell: ({ row }) => ( + + + + + +
+ {row.original.trade_no && ( + <> +
{t('tradeNo')}
+ {row.original.trade_no} + + + )} +
    +
  • + {t('subscribePrice')} + + + +
  • +
  • + {t('reductionPrice')} + + + +
  • +
  • + {t('feAmount')} + + + +
  • +
  • + {t('total')} + + + +
  • +
+
+ +
    +
  • + 支付方式 + {t(`methods.${row.original.method}`)} +
  • +
+
+
+ ), + }, + { + accessorKey: 'user_id', + header: t('user'), + cell: ({ row }) => , + }, + { + accessorKey: 'updated_at', + header: t('updateTime'), + cell: ({ row }) => formatDate(row.original.updated_at), + }, + { + accessorKey: 'status', + header: t('status.0'), + cell: ({ row }) => { + const option = statusOptions.find((opt) => opt.value === row.original.status); + if ([1, 3, 4].includes(row.getValue('status'))) { + return ( + + placeholder='状态' + value={row.original.status} + onChange={async (value) => { + await updateOrderStatus({ + id: row.original.id, + status: value, + }); + ref.current?.refresh(); + }} + options={statusOptions} + className={cn(option?.className)} + /> + ); + } + return {t(`status.${row.getValue('status')}`)}; + }, + }, + ]} + params={[ + { key: 'search' }, + { + key: 'status', + placeholder: t('status.0'), + options: statusOptions.map((item) => ({ + label: item.label, + value: String(item.value), + })), + }, + { + key: 'subscribe_id', + placeholder: `${t('subscribe')}`, + options: subscribeList?.map((item) => ({ + label: item.name, + value: String(item.id), + })), + }, + { key: 'user_id', placeholder: `${t('user')}ID` }, + ]} + request={async (pagination, filter) => { + const { data } = await getOrderList({ ...pagination, ...filter }); + return { + list: data.data?.list || [], + total: data.data?.total || 0, + }; + }} + /> + ); +} diff --git a/apps/admin/app/dashboard/page.tsx b/apps/admin/app/dashboard/page.tsx new file mode 100644 index 0000000..83c4f9b --- /dev/null +++ b/apps/admin/app/dashboard/page.tsx @@ -0,0 +1,593 @@ +'use client'; + +import { formatBytes } from '@repo/ui/utils'; +import { Badge } from '@shadcn/ui/badge'; +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@shadcn/ui/card'; +import { + ChartConfig, + ChartContainer, + ChartLegend, + ChartLegendContent, + ChartTooltip, + ChartTooltipContent, +} from '@shadcn/ui/chart'; +import { + Area, + AreaChart, + Bar, + BarChart, + CartesianGrid, + Label, + Pie, + PieChart, + XAxis, +} from '@shadcn/ui/lib/recharts'; +import { Separator } from '@shadcn/ui/separator'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@shadcn/ui/table'; +import { useLocale } from 'next-intl'; + +const UserStatisticsConfig = { + register: { + label: '注册', + color: 'hsl(var(--chart-1))', + }, + new_purchase: { + label: '新购', + color: 'hsl(var(--chart-2))', + }, + repurchase: { + label: '复购', + color: 'hsl(var(--chart-3))', + }, +} satisfies ChartConfig; + +const IncomeStatisticsConfig = { + new_purchase: { + label: '新购', + color: 'hsl(var(--chart-1))', + }, + repurchase: { + label: '复购', + color: 'hsl(var(--chart-2))', + }, +}; + +export default function Dashboard() { + const locale = useLocale(); + return ( +
+ + + 统计 + + +
+
在线IP数
+
666
+
+
+
在线节点数
+
99
+
+
+
离线节点数
+
1
+
+ +
+
待处理工单
+
1
+
+
+
今日上传流量
+
+ {formatBytes(99999999999999)} +
+
+
+
今日下载流量
+
+ {formatBytes(99999999999999)} +
+
+
+
总上传流量
+
+ {formatBytes(99999999999999)} +
+
+
+
总下载流量
+
+ {formatBytes(99999999999999)} +
+
+
+
+
+ + + 今日收入统计 + + + + + } /> + } /> + + + + + + +
+
+
总收入
+
6,666
+
+ +
+
+ {IncomeStatisticsConfig.new_purchase.label} +
+
123
+
+ +
+
+ {IncomeStatisticsConfig.repurchase.label} +
+
456
+
+
+
+
+ + + + 本月收入统计 + + + + + + { + return new Date(value).toLocaleDateString(locale, { + month: 'short', + day: 'numeric', + }); + }} + /> + + + } /> + } /> + + + + +
+
+
总收入
+
654,321
+
+ +
+
+ {IncomeStatisticsConfig.new_purchase.label} +
+
123
+
+ +
+
+ {IncomeStatisticsConfig.repurchase.label} +
+
456
+
+
+
+
+ + + + 收入统计 + + + + + + { + return new Date(value).toLocaleDateString(locale, { + month: 'short', + }); + }} + /> + } /> + + + } /> + + + + +
+
+
总收入
+
987,654,321
+
+
+
+
+ + + + 今日用户统计 + + + + + } /> + } /> + + + + + + +
+
+
+ {UserStatisticsConfig.register.label} +
+
789
+
+ +
+
+ {UserStatisticsConfig.new_purchase.label} +
+
123
+
+ +
+
+ {UserStatisticsConfig.repurchase.label} +
+
456
+
+
+
+
+ + + + 本月用户统计 + + + + + + { + return new Date(value).toLocaleDateString(locale, { + month: 'short', + day: 'numeric', + }); + }} + /> + + + + } /> + } /> + + + + +
+
+
+ {UserStatisticsConfig.register.label} +
+
789
+
+ +
+
+ {UserStatisticsConfig.new_purchase.label} +
+
123
+
+ +
+
+ {UserStatisticsConfig.repurchase.label} +
+
456
+
+
+
+
+ + + + 用户统计 + + + + + + { + return new Date(value).toLocaleDateString(locale, { + month: 'short', + }); + }} + /> + } /> + + + + } /> + + + + +
+
+
+ {UserStatisticsConfig.register.label} +
+
987,654,321
+
+
+
+
+
+
+ + + 今日节点流量排行 + + + + + + 类型 + 节点 + 流量 + + + + {new Array(10) + .toString() + .split(',') + .map((item, index) => ( + + + Trojan + + +
节点名称
+
+ 127.0.0.1:443 +
+
+ 1,000 GB +
+ ))} +
+
+
+
+ + + 今日用户流量排行 + + + {new Array(15) + .toString() + .split(',') + .map((item, index) => ( +
+
olivia.martin@email.com
+
100 GB
+
+ ))} +
+
+
+
+ ); +} diff --git a/apps/admin/app/dashboard/payment/alipayf2f.tsx b/apps/admin/app/dashboard/payment/alipayf2f.tsx new file mode 100644 index 0000000..be1508a --- /dev/null +++ b/apps/admin/app/dashboard/payment/alipayf2f.tsx @@ -0,0 +1,243 @@ +'use client'; + +import { getAlipayF2FPaymentConfig, updateAlipayF2FPaymentConfig } from '@/services/admin/payment'; +import { EnhancedInput } from '@repo/ui/enhanced-input'; +import { unitConversion } from '@repo/ui/utils'; +import { Label } from '@shadcn/ui/label'; +import { toast } from '@shadcn/ui/lib/sonner'; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from '@shadcn/ui/select'; +import { Switch } from '@shadcn/ui/switch'; +import { Table, TableBody, TableCell, TableRow } from '@shadcn/ui/table'; +import { Textarea } from '@shadcn/ui/textarea'; +import { useQuery } from '@tanstack/react-query'; +import { useTranslations } from 'next-intl'; + +export default function AlipayF2F() { + const t = useTranslations('payment'); + + const { data, refetch } = useQuery({ + queryKey: ['getAlipayF2FPaymentConfig'], + queryFn: async () => { + const { data } = await getAlipayF2FPaymentConfig(); + + return data.data; + }, + }); + + async function updateConfig(key: string, value: unknown) { + if (data?.[key] === value) return; + try { + await updateAlipayF2FPaymentConfig({ + ...data, + [key]: value, + } as API.PaymentConfig); + toast.success(t('saveSuccess')); + refetch(); + } catch (error) { + /* empty */ + } + } + + return ( + + + + + +

{t('enableDescription')}

+
+ + { + updateConfig('enable', checked); + }} + /> + +
+ + + +

{t('showNameDescription')}

+
+ + updateConfig('name', value)} + /> + +
+ + + +

{t('iconUrlDescription')}

+
+ + updateConfig('icon', value)} + /> + +
+ + + +

{t('notifyUrlDescription')}

+
+ + updateConfig('domain', value)} + /> + +
+ + + +

{t('feeModeDescription')}

+
+ + + +
+ + + +

{t('feePercentDescription')}

+
+ + updateConfig('fee_percent', value)} + suffix='%' + /> + +
+ + + +

{t('fixedFeeDescription')}

+
+ + unitConversion('centsToDollars', value)} + formatOutput={(value) => unitConversion('dollarsToCents', value)} + onValueBlur={(value) => updateConfig('fee_amount', value)} + /> + +
+ + + +

+ + + + updateConfig('config', { + ...data?.config, + app_id: value, + }) + } + /> + + + + + +

+ + +