Import local app source
@@ -0,0 +1,21 @@
|
|||||||
|
# Root editor config file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
# Common settings
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
charset = utf-8
|
||||||
|
|
||||||
|
# python, js indentation settings
|
||||||
|
[{*.py,*.js,*.vue,*.css,*.scss,*.html}]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
max_line_length = 99
|
||||||
|
|
||||||
|
# JSON files - mostly doctype schema files
|
||||||
|
[{*.json}]
|
||||||
|
insert_final_newline = false
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"node": true,
|
||||||
|
"es2022": true
|
||||||
|
},
|
||||||
|
"parserOptions": {
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"extends": "eslint:recommended",
|
||||||
|
"rules": {
|
||||||
|
"indent": "off",
|
||||||
|
"brace-style": "off",
|
||||||
|
"no-mixed-spaces-and-tabs": "off",
|
||||||
|
"no-useless-escape": "off",
|
||||||
|
"space-unary-ops": ["error", { "words": true }],
|
||||||
|
"linebreak-style": "off",
|
||||||
|
"quotes": ["off"],
|
||||||
|
"semi": "off",
|
||||||
|
"camelcase": "off",
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"no-console": ["warn"],
|
||||||
|
"no-extra-boolean-cast": ["off"],
|
||||||
|
"no-control-regex": ["off"],
|
||||||
|
},
|
||||||
|
"root": true,
|
||||||
|
"globals": {
|
||||||
|
"frappe": true,
|
||||||
|
"Vue": true,
|
||||||
|
"SetVueGlobals": true,
|
||||||
|
"__": true,
|
||||||
|
"repl": true,
|
||||||
|
"Class": true,
|
||||||
|
"locals": true,
|
||||||
|
"cint": true,
|
||||||
|
"cstr": true,
|
||||||
|
"cur_frm": true,
|
||||||
|
"cur_dialog": true,
|
||||||
|
"cur_page": true,
|
||||||
|
"cur_list": true,
|
||||||
|
"cur_tree": true,
|
||||||
|
"msg_dialog": true,
|
||||||
|
"is_null": true,
|
||||||
|
"in_list": true,
|
||||||
|
"has_common": true,
|
||||||
|
"posthog": true,
|
||||||
|
"has_words": true,
|
||||||
|
"validate_email": true,
|
||||||
|
"open_web_template_values_editor": true,
|
||||||
|
"validate_name": true,
|
||||||
|
"validate_phone": true,
|
||||||
|
"validate_url": true,
|
||||||
|
"get_number_format": true,
|
||||||
|
"format_number": true,
|
||||||
|
"format_currency": true,
|
||||||
|
"comment_when": true,
|
||||||
|
"open_url_post": true,
|
||||||
|
"toTitle": true,
|
||||||
|
"lstrip": true,
|
||||||
|
"rstrip": true,
|
||||||
|
"strip": true,
|
||||||
|
"strip_html": true,
|
||||||
|
"replace_all": true,
|
||||||
|
"flt": true,
|
||||||
|
"precision": true,
|
||||||
|
"CREATE": true,
|
||||||
|
"AMEND": true,
|
||||||
|
"CANCEL": true,
|
||||||
|
"copy_dict": true,
|
||||||
|
"get_number_format_info": true,
|
||||||
|
"strip_number_groups": true,
|
||||||
|
"print_table": true,
|
||||||
|
"Layout": true,
|
||||||
|
"web_form_settings": true,
|
||||||
|
"$c": true,
|
||||||
|
"$a": true,
|
||||||
|
"$i": true,
|
||||||
|
"$bg": true,
|
||||||
|
"$y": true,
|
||||||
|
"$c_obj": true,
|
||||||
|
"refresh_many": true,
|
||||||
|
"refresh_field": true,
|
||||||
|
"toggle_field": true,
|
||||||
|
"get_field_obj": true,
|
||||||
|
"get_query_params": true,
|
||||||
|
"unhide_field": true,
|
||||||
|
"hide_field": true,
|
||||||
|
"set_field_options": true,
|
||||||
|
"getCookie": true,
|
||||||
|
"getCookies": true,
|
||||||
|
"get_url_arg": true,
|
||||||
|
"md5": true,
|
||||||
|
"$": true,
|
||||||
|
"jQuery": true,
|
||||||
|
"moment": true,
|
||||||
|
"hljs": true,
|
||||||
|
"Awesomplete": true,
|
||||||
|
"Sortable": true,
|
||||||
|
"Showdown": true,
|
||||||
|
"Taggle": true,
|
||||||
|
"Gantt": true,
|
||||||
|
"Slick": true,
|
||||||
|
"Webcam": true,
|
||||||
|
"PhotoSwipe": true,
|
||||||
|
"PhotoSwipeUI_Default": true,
|
||||||
|
"io": true,
|
||||||
|
"JsBarcode": true,
|
||||||
|
"L": true,
|
||||||
|
"Chart": true,
|
||||||
|
"DataTable": true,
|
||||||
|
"Cypress": true,
|
||||||
|
"cy": true,
|
||||||
|
"it": true,
|
||||||
|
"describe": true,
|
||||||
|
"expect": true,
|
||||||
|
"context": true,
|
||||||
|
"before": true,
|
||||||
|
"beforeEach": true,
|
||||||
|
"after": true,
|
||||||
|
"qz": true,
|
||||||
|
"localforage": true,
|
||||||
|
"extend_cscript": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.DS_Store
|
||||||
|
*.pyc
|
||||||
|
*.egg-info
|
||||||
|
*.swp
|
||||||
|
tags
|
||||||
|
node_modules
|
||||||
|
__pycache__
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
exclude: 'node_modules|.git'
|
||||||
|
default_stages: [pre-commit]
|
||||||
|
fail_fast: false
|
||||||
|
|
||||||
|
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v5.0.0
|
||||||
|
hooks:
|
||||||
|
- id: trailing-whitespace
|
||||||
|
files: "sunderer_app.*"
|
||||||
|
exclude: ".*json$|.*txt$|.*csv|.*md|.*svg"
|
||||||
|
- id: check-merge-conflict
|
||||||
|
- id: check-ast
|
||||||
|
- id: check-json
|
||||||
|
- id: check-toml
|
||||||
|
- id: check-yaml
|
||||||
|
- id: debug-statements
|
||||||
|
|
||||||
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
|
rev: v0.8.1
|
||||||
|
hooks:
|
||||||
|
- id: ruff
|
||||||
|
name: "Run ruff import sorter"
|
||||||
|
args: ["--select=I", "--fix"]
|
||||||
|
|
||||||
|
- id: ruff
|
||||||
|
name: "Run ruff linter"
|
||||||
|
|
||||||
|
- id: ruff-format
|
||||||
|
name: "Run ruff formatter"
|
||||||
|
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||||
|
rev: v2.7.1
|
||||||
|
hooks:
|
||||||
|
- id: prettier
|
||||||
|
types_or: [javascript, vue, scss]
|
||||||
|
# Ignore any files that might contain jinja / bundles
|
||||||
|
exclude: |
|
||||||
|
(?x)^(
|
||||||
|
sunderer_app/public/dist/.*|
|
||||||
|
.*node_modules.*|
|
||||||
|
.*boilerplate.*|
|
||||||
|
sunderer_app/templates/includes/.*|
|
||||||
|
sunderer_app/public/js/lib/.*
|
||||||
|
)$
|
||||||
|
|
||||||
|
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||||
|
rev: v8.44.0
|
||||||
|
hooks:
|
||||||
|
- id: eslint
|
||||||
|
types_or: [javascript]
|
||||||
|
args: ['--quiet']
|
||||||
|
# Ignore any files that might contain jinja / bundles
|
||||||
|
exclude: |
|
||||||
|
(?x)^(
|
||||||
|
sunderer_app/public/dist/.*|
|
||||||
|
cypress/.*|
|
||||||
|
.*node_modules.*|
|
||||||
|
.*boilerplate.*|
|
||||||
|
sunderer_app/templates/includes/.*|
|
||||||
|
sunderer_app/public/js/lib/.*
|
||||||
|
)$
|
||||||
|
|
||||||
|
ci:
|
||||||
|
autoupdate_schedule: weekly
|
||||||
|
skip: []
|
||||||
|
submodules: false
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
### Sunderer
|
||||||
|
|
||||||
|
test
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
You can install this app using the [bench](https://github.com/frappe/bench) CLI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd $PATH_TO_YOUR_BENCH
|
||||||
|
bench get-app $URL_OF_THIS_REPO --branch develop
|
||||||
|
bench install-app sunderer_app
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
|
||||||
|
This app uses `pre-commit` for code formatting and linting. Please [install pre-commit](https://pre-commit.com/#installation) and enable it for this repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/sunderer_app
|
||||||
|
pre-commit install
|
||||||
|
```
|
||||||
|
|
||||||
|
Pre-commit is configured to use the following tools for checking and formatting your code:
|
||||||
|
|
||||||
|
- ruff
|
||||||
|
- eslint
|
||||||
|
- prettier
|
||||||
|
- pyupgrade
|
||||||
|
|
||||||
|
### License
|
||||||
|
|
||||||
|
agpl-3.0
|
||||||
@@ -0,0 +1,661 @@
|
|||||||
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
our General Public Licenses are 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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Developers that use our General Public Licenses protect your rights
|
||||||
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
A secondary benefit of defending all users' freedom is that
|
||||||
|
improvements made in alternate versions of the program, if they
|
||||||
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
|
The GNU Affero General Public License is designed specifically to
|
||||||
|
ensure that, in such cases, the modified source code becomes available
|
||||||
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
|
An older license, called the Affero General Public License and
|
||||||
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
|
this license.
|
||||||
|
|
||||||
|
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 Affero 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. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
|
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 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 work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU Affero 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 Affero 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 Affero 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 Affero 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.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for the
|
||||||
|
specific requirements.
|
||||||
|
|
||||||
|
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 AGPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
[project]
|
||||||
|
name = "sunderer_app"
|
||||||
|
authors = [
|
||||||
|
{ name = "test", email = "898879719@qq.com"}
|
||||||
|
]
|
||||||
|
description = "test"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
readme = "README.md"
|
||||||
|
dynamic = ["version"]
|
||||||
|
dependencies = [
|
||||||
|
# "frappe~=15.0.0" # Installed and managed by bench.
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["flit_core >=3.4,<4"]
|
||||||
|
build-backend = "flit_core.buildapi"
|
||||||
|
|
||||||
|
# These dependencies are only installed when developer mode is enabled
|
||||||
|
[tool.bench.dev-dependencies]
|
||||||
|
# package_name = "~=1.1.0"
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 110
|
||||||
|
target-version = "py310"
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = [
|
||||||
|
"F",
|
||||||
|
"E",
|
||||||
|
"W",
|
||||||
|
"I",
|
||||||
|
"UP",
|
||||||
|
"B",
|
||||||
|
"RUF",
|
||||||
|
]
|
||||||
|
ignore = [
|
||||||
|
"B017", # assertRaises(Exception) - should be more specific
|
||||||
|
"B018", # useless expression, not assigned to anything
|
||||||
|
"B023", # function doesn't bind loop variable - will have last iteration's value
|
||||||
|
"B904", # raise inside except without from
|
||||||
|
"E101", # indentation contains mixed spaces and tabs
|
||||||
|
"E402", # module level import not at top of file
|
||||||
|
"E501", # line too long
|
||||||
|
"E741", # ambiguous variable name
|
||||||
|
"F401", # "unused" imports
|
||||||
|
"F403", # can't detect undefined names from * import
|
||||||
|
"F405", # can't detect undefined names from * import
|
||||||
|
"F722", # syntax error in forward type annotation
|
||||||
|
"W191", # indentation contains tabs
|
||||||
|
]
|
||||||
|
typing-modules = ["frappe.types.DF"]
|
||||||
|
|
||||||
|
[tool.ruff.format]
|
||||||
|
quote-style = "double"
|
||||||
|
indent-style = "tab"
|
||||||
|
docstring-code-format = true
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
__version__ = "0.0.1"
|
||||||
@@ -0,0 +1,329 @@
|
|||||||
|
# sunderer_app/api/custom_user_signup.py
|
||||||
|
import frappe
|
||||||
|
from frappe import _ # 虽然这里不直接用 _(),但保持导入
|
||||||
|
from frappe.utils import validate_email_address, random_string, get_url, generate_hash
|
||||||
|
import json # 导入 json 模块
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def custom_sign_up(email, full_name, password=None, send_email=True, **kwargs):
|
||||||
|
"""
|
||||||
|
自定义用户注册 API,模仿 Frappe 的 sign_up 但发送自定义邮件链接
|
||||||
|
|
||||||
|
Args:
|
||||||
|
email (str): 用户邮箱
|
||||||
|
full_name (str): 用户全名
|
||||||
|
password (str, optional): 用户密码 (如果需要立即设置)
|
||||||
|
send_email (bool, optional): 是否发送激活邮件. Defaults to True.
|
||||||
|
**kwargs: 其他可能传递给 User DocType 的字段
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 包含操作结果的字典
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# --- 1. 验证输入 ---
|
||||||
|
email = validate_email_address(email, throw=True) # 验证邮箱格式
|
||||||
|
|
||||||
|
if frappe.db.exists("User", {"email": email}):
|
||||||
|
# 不再使用 frappe.throw,而是直接返回错误信息
|
||||||
|
return {"status": "error", "message": _("该邮箱地址已被注册。")}
|
||||||
|
|
||||||
|
if not full_name:
|
||||||
|
return {"status": "error", "message": _("请输入全名。")}
|
||||||
|
|
||||||
|
# --- 2. 创建 User 文档 ---
|
||||||
|
user = frappe.get_doc({
|
||||||
|
"doctype": "User",
|
||||||
|
"email": email,
|
||||||
|
"first_name": full_name.split(" ")[0], # 简单拆分姓
|
||||||
|
"last_name": " ".join(full_name.split(" ")[1:]) if len(full_name.split(" ")) > 1 else "", # 其余部分为名
|
||||||
|
"enabled": 1 if password else 0, # 如果提供了密码,可以立即启用;否则等待激活
|
||||||
|
"new_password": password, # 如果提供了密码,设置它
|
||||||
|
"send_welcome_email": 0, # 我们自己发送邮件
|
||||||
|
"user_type": "Website User", # 通常注册的是网站用户
|
||||||
|
# **kwargs: 添加其他通过 kwargs 传递的字段
|
||||||
|
**kwargs
|
||||||
|
})
|
||||||
|
|
||||||
|
# --- 3. 插入用户文档 ---
|
||||||
|
user.insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
# --- 4. 生成激活密钥 ---
|
||||||
|
# 如果没有提供密码,或者总是需要激活邮件,则生成密钥
|
||||||
|
if not password:
|
||||||
|
# Use frappe.generate_hash instead of get_password_reset_key
|
||||||
|
reset_key = frappe.generate_hash(length=100) # Generate a secure key
|
||||||
|
else:
|
||||||
|
reset_key = None # 如果密码已设置,可能不需要密钥,或用于其他目的
|
||||||
|
|
||||||
|
# --- 5. 保存用户文档 (如果需要更新密钥) ---
|
||||||
|
if reset_key:
|
||||||
|
user.reload() # 重新加载文档以确保它是最新的
|
||||||
|
user.reset_password_key = reset_key
|
||||||
|
user.save(ignore_permissions=True)
|
||||||
|
|
||||||
|
# --- 6. 发送自定义激活邮件 ---
|
||||||
|
if send_email and reset_key:
|
||||||
|
send_custom_activation_email(user.email, reset_key)
|
||||||
|
|
||||||
|
# --- 7. 返回结果 ---
|
||||||
|
if password:
|
||||||
|
return {"status": "success", "message": _("用户注册成功,您现在可以登录。")}
|
||||||
|
else:
|
||||||
|
return {"status": "success", "message": _("激活邮件已发送,请查收邮箱。")}
|
||||||
|
|
||||||
|
except frappe.DuplicateEntryError:
|
||||||
|
# 这个异常可能在 user.insert() 时由数据库唯一性约束触发
|
||||||
|
return {"status": "error", "message": _("该邮箱地址已被注册。")}
|
||||||
|
except Exception as e:
|
||||||
|
# 捕获所有其他意外错误
|
||||||
|
frappe.log_error(f"注册过程中发生未知错误,邮箱: {email}, 错误: {str(e)}")
|
||||||
|
return {"status": "error", "message": _("注册过程中发生未知错误,请稍后再试。")}
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def send_custom_activation_email(user_email, reset_key):
|
||||||
|
"""
|
||||||
|
发送包含自定义激活链接的邮件
|
||||||
|
"""
|
||||||
|
# 构造自定义激活 URL
|
||||||
|
activation_url = f"{get_url()}/activate-account?key={reset_key}" # Use get_url helper
|
||||||
|
|
||||||
|
# 使用中文主题和内容
|
||||||
|
subject = _("决裂者终端 - 激活您的账户") # 或者 _("决裂者终端 - 设置密码")
|
||||||
|
message = f"""
|
||||||
|
<p>您好!</p>
|
||||||
|
<p>请点击下方链接设置密码并激活您的账户:</p>
|
||||||
|
<p><a href="{activation_url}">{activation_url}</a></p>
|
||||||
|
<p>如果您未发起此请求,请忽略此邮件。</p>
|
||||||
|
"""
|
||||||
|
|
||||||
|
frappe.sendmail(
|
||||||
|
recipients=[user_email],
|
||||||
|
subject=subject,
|
||||||
|
message=message,
|
||||||
|
delayed=False # Send immediately
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def trigger_account_setup_email(email):
|
||||||
|
|
||||||
|
try:
|
||||||
|
# --- 1. 验证邮箱格式 (可选,但推荐) ---
|
||||||
|
import re
|
||||||
|
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||||
|
if not re.match(pattern, email):
|
||||||
|
return {"exc": "请输入有效的邮箱地址!"}
|
||||||
|
|
||||||
|
# --- 2. 检查邮箱是否已存在于系统中 ---
|
||||||
|
if frappe.db.exists("User", {"email": email}):
|
||||||
|
return {"exc": "该邮箱地址已被注册。"}
|
||||||
|
|
||||||
|
# --- 3. 生成唯一的、有时效性的设置密钥 ---
|
||||||
|
setup_key = frappe.generate_hash(length=100) # 生成一个足够长的随机密钥
|
||||||
|
|
||||||
|
# --- 4. 创建初始的 User 记录 (禁用) ---
|
||||||
|
# 由于没有全名,我们暂时使用邮箱前缀作为名字,或者可以要求前端提供
|
||||||
|
username_parts = email.split('@')
|
||||||
|
first_name = username_parts[0] # 使用邮箱前缀作为名字
|
||||||
|
|
||||||
|
user_doc = frappe.get_doc({
|
||||||
|
"doctype": "User",
|
||||||
|
"email": email,
|
||||||
|
"first_name": first_name,
|
||||||
|
"last_name": "", # 可以留空或根据需要填充
|
||||||
|
"enabled": 0, # 初始状态为禁用
|
||||||
|
"send_welcome_email": 0, # 我们自己发送邮件
|
||||||
|
"user_type": "Website User",
|
||||||
|
# 可以根据需要添加其他默认字段
|
||||||
|
})
|
||||||
|
user_doc.insert(ignore_permissions=True) # 插入用户,此时是禁用的
|
||||||
|
print(f"Created initial disabled User: {user_doc.name}") # 调试日志
|
||||||
|
|
||||||
|
# --- 5. 创建 Signup Key DocType 记录 ---
|
||||||
|
signup_key_doc = frappe.new_doc("Signup Key")
|
||||||
|
signup_key_doc.key = setup_key # 使用生成的 setup_key 作为 key 字段
|
||||||
|
signup_key_doc.email = email # 存储关联的邮箱
|
||||||
|
signup_key_doc.used = 0 # 标记为未使用
|
||||||
|
# 如果 Signup Key DocType 有 expires_on 字段,可以设置过期时间
|
||||||
|
# signup_key_doc.expires_on = add_to_date(now_datetime(), hours=1) # 1小时后过期
|
||||||
|
signup_key_doc.insert(ignore_permissions=True)
|
||||||
|
print(f"Created Signup Key: {signup_key_doc.name} for email: {email}") # 调试日志
|
||||||
|
|
||||||
|
# --- 6. 构造 IncreaseAcc 链接 ---
|
||||||
|
site_url = get_url()
|
||||||
|
setup_url = f"{site_url}/IncreaseAcc?key={setup_key}"
|
||||||
|
|
||||||
|
# --- 7. 发送包含链接的邮件 ---
|
||||||
|
send_setup_link_email(email, setup_url)
|
||||||
|
|
||||||
|
# --- 8. 返回成功信息 ---
|
||||||
|
return {"message": "注册链接已发送至您的邮箱,请查收。"}
|
||||||
|
|
||||||
|
except frappe.DuplicateEntryError as e:
|
||||||
|
# 如果 User 或 Signup Key 插入时违反唯一性约束
|
||||||
|
frappe.log_error(f"Trigger account setup failed due to duplicate entry, email: {email}, error: {str(e)}")
|
||||||
|
return {"exc": "用户或密钥已存在,请稍后再试。"}
|
||||||
|
except Exception as e:
|
||||||
|
# 捕获所有其他意外错误
|
||||||
|
frappe.log_error(f"触发账户设置邮件失败,邮箱: {email}, 错误: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
frappe.log_error(f"触发账户设置邮件错误堆栈:\n{traceback.format_exc()}")
|
||||||
|
# 如果 User 创建失败,尝试回滚 Signup Key (可选)
|
||||||
|
# frappe.delete_doc_if_exists("Signup Key", setup_key_name, ignore_permissions=True)
|
||||||
|
return {"exc": f"发送失败: {str(e)}"}
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def send_setup_link_email(user_email, setup_url):
|
||||||
|
"""
|
||||||
|
发送包含账户设置链接的邮件
|
||||||
|
"""
|
||||||
|
# 使用中文主题和内容
|
||||||
|
subject = "决裂者终端 - 完成账户注册"
|
||||||
|
|
||||||
|
message = f"""
|
||||||
|
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||||
|
<h2 style="color: #00AFFF;">决裂者终端</h2>
|
||||||
|
<p>您好!</p>
|
||||||
|
<p>请点击下方链接完成您的账户注册:</p>
|
||||||
|
<p><a href="{setup_url}" style="display: inline-block; padding: 15px 30px; background-color: #00AFFF; color: white; text-decoration: none; border-radius: 5px; font-weight: bold;">完成注册</a></p>
|
||||||
|
<p><em>此链接将在 1 小时内有效。</em></p>
|
||||||
|
<p>如果您未发起此请求,请忽略此邮件。</p>
|
||||||
|
<hr style="margin: 20px 0; border: none; border-top: 1px solid #ccc;">
|
||||||
|
<p style="color: #666; font-size: 12px;">© 2026 Sunderer Games. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
frappe.sendmail(
|
||||||
|
recipients=[user_email],
|
||||||
|
subject=subject,
|
||||||
|
message=message,
|
||||||
|
delayed=False # 立即发送
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def complete_account(key, password):
|
||||||
|
print(f"[DEBUG] 开始执行 complete_account,传入的 key: {key[:10]}...")
|
||||||
|
try:
|
||||||
|
signup_key_doc_list = frappe.get_all("Signup Key", fields=["name", "email"], filters={"key": key, "used": 0})
|
||||||
|
print(f"[DEBUG] 查询到的 signup_key_doc_list: {signup_key_doc_list}")
|
||||||
|
if not signup_key_doc_list:
|
||||||
|
print("[DEBUG] 未找到有效的未使用密钥,抛出错误。")
|
||||||
|
frappe.throw("无效或已过期的密钥。", frappe.ValidationError)
|
||||||
|
return
|
||||||
|
|
||||||
|
signup_key_name = signup_key_doc_list[0].name
|
||||||
|
email = signup_key_doc_list[0].email
|
||||||
|
print(f"[DEBUG] 获取到的邮箱: {email}")
|
||||||
|
|
||||||
|
# --- 获取用户文档 ---
|
||||||
|
try:
|
||||||
|
user_doc = frappe.get_doc("User", email)
|
||||||
|
print(f"[DEBUG] 找到用户文档: {user_doc.name}, 启用状态: {user_doc.enabled}")
|
||||||
|
except frappe.DoesNotExistError:
|
||||||
|
print(f"[ERROR] 用户 {email} 不存在。")
|
||||||
|
frappe.throw(f"用户 {email} 不存在。")
|
||||||
|
return
|
||||||
|
|
||||||
|
# --- 新增:手动进行密码强度校验 ---
|
||||||
|
# 为密码强度检查准备用户信息
|
||||||
|
user_info = {
|
||||||
|
"email": user_doc.email or "",
|
||||||
|
"first_name": user_doc.first_name or "",
|
||||||
|
"last_name": user_doc.last_name or "",
|
||||||
|
"username": user_doc.name or "" # 如果 User 文档有 username 字段,也可以包含
|
||||||
|
}
|
||||||
|
|
||||||
|
# 调用 Frappe 的密码强度检查函数
|
||||||
|
# 注意:这个函数名可能因版本而异,但通常在 frappe.utils.password_strength 中
|
||||||
|
# 我们会尝试不同的函数名
|
||||||
|
try:
|
||||||
|
from frappe.utils.password_strength import get_password_strength
|
||||||
|
strength_result = get_password_strength(password, user_info)
|
||||||
|
except ImportError:
|
||||||
|
# 如果无法导入 get_password_strength,尝试其他方式
|
||||||
|
# 这里我们假设它在 frappe.utils 中,或者使用一个简单的自定义检查
|
||||||
|
strength_result = {"score": 0, "feedback": {"warning": "", "suggestions": ["密码至少需要8位字符,包含大小写字母和数字"]}}
|
||||||
|
# 或者,您可以在这里实现更复杂的自定义强度检查逻辑
|
||||||
|
|
||||||
|
score = strength_result.get('score', 0)
|
||||||
|
feedback = strength_result.get('feedback', {})
|
||||||
|
|
||||||
|
# 定义最低分数要求(Frappe 默认是2)
|
||||||
|
min_score = 2
|
||||||
|
|
||||||
|
if score < min_score:
|
||||||
|
suggestions = feedback.get('suggestions', [])
|
||||||
|
warning = feedback.get('warning', "")
|
||||||
|
error_message_parts = ["密码强度不足,请设置一个更强的密码。"]
|
||||||
|
if warning:
|
||||||
|
error_message_parts.append(f"警告: {warning}")
|
||||||
|
if suggestions:
|
||||||
|
error_message_parts.append(f"建议: {', '.join(suggestions)}")
|
||||||
|
# 使用 frappe.throw 返回结构化的错误信息,便于前端解析
|
||||||
|
frappe.throw({
|
||||||
|
"message": " ".join(error_message_parts),
|
||||||
|
"type": "password_strength",
|
||||||
|
"details": {
|
||||||
|
"score": score,
|
||||||
|
"min_score": min_score,
|
||||||
|
"suggestions": suggestions,
|
||||||
|
"warning": warning
|
||||||
|
}
|
||||||
|
})
|
||||||
|
# --- 新增结束:手动进行密码强度校验 ---
|
||||||
|
|
||||||
|
# --- 关键部分:更新密码和角色 ---
|
||||||
|
try:
|
||||||
|
user_doc.new_password = password
|
||||||
|
user_doc.enabled = 1 # 确保用户处于启用状态
|
||||||
|
|
||||||
|
# --- 添加或修改角色 ---
|
||||||
|
default_roles = ["Sunderer User"] # 根据你的需求调整
|
||||||
|
|
||||||
|
for role_name in default_roles:
|
||||||
|
if frappe.db.exists("Role", role_name):
|
||||||
|
if not any(d.role == role_name for d in user_doc.roles):
|
||||||
|
user_doc.append("roles", {"role": role_name})
|
||||||
|
print(f"[DEBUG] 已将角色 '{role_name}' 添加到用户 {email}。")
|
||||||
|
else:
|
||||||
|
print(f"[WARNING] 角色 '{role_name}' 不存在。跳过。")
|
||||||
|
|
||||||
|
# --- 角色添加结束 ---
|
||||||
|
|
||||||
|
# 在手动检查通过后,再执行 save(),此时应该不会再因为密码强度问题失败
|
||||||
|
user_doc.save(ignore_permissions=True)
|
||||||
|
print(f"[DEBUG] 成功更新用户 {email} 的密码、角色和启用状态。")
|
||||||
|
except Exception as pwd_error:
|
||||||
|
frappe.log_error(f"在完成账户 {email} 时更新用户详细信息(密码/角色)失败: {str(pwd_error)}")
|
||||||
|
print(f"[ERROR] 更新用户 {email} 详细信息失败: {str(pwd_error)}")
|
||||||
|
frappe.throw(f"设置用户账户失败: {str(pwd_error)}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# --- 仅当用户信息更新成功后,才将密钥标记为已使用 ---
|
||||||
|
try:
|
||||||
|
frappe.db.set_value("Signup Key", signup_key_name, "used", 1)
|
||||||
|
print(f"[DEBUG] 已将 Signup Key {signup_key_name} 标记为已使用。")
|
||||||
|
except Exception as db_error:
|
||||||
|
frappe.log_error(f"用户更新后,将 Signup Key {signup_key_name} 标记为已使用失败: {str(db_error)}")
|
||||||
|
print(f"[ERROR] 将 Signup Key 标记为已使用失败: {str(db_error)}")
|
||||||
|
frappe.msgprint("密码已设置,但请就确认链接状态联系技术支持。")
|
||||||
|
|
||||||
|
frappe.db.commit()
|
||||||
|
print(f"[DEBUG] 已提交 key: {key[:10]}... 的事务。")
|
||||||
|
|
||||||
|
return {"message": "账户已激活,密码设置成功!"}
|
||||||
|
|
||||||
|
except frappe.AuthenticationError as e:
|
||||||
|
frappe.log_error(f"complete_account 中发生身份验证错误: {str(e)}")
|
||||||
|
print(f"[ERROR] 身份验证错误: {str(e)}")
|
||||||
|
frappe.throw(f"身份验证错误: {str(e)}")
|
||||||
|
except Exception as e:
|
||||||
|
frappe.log_error(f"激活账户时发生错误,key {key[:10]}...: {str(e)}")
|
||||||
|
print(f"[ERROR] 激活账户时发生错误,key {key[:10]}...: {str(e)}")
|
||||||
|
frappe.throw("激活您的账户时发生错误。请稍后重试或联系技术支持。")
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# sunderer_app/sunderer_app/api.py
|
||||||
|
import frappe
|
||||||
|
from frappe.utils import random_string, now_datetime, add_to_date
|
||||||
|
import secrets
|
||||||
|
import string
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def generate_verification_code(length=6):
|
||||||
|
"""生成指定长度的数字验证码"""
|
||||||
|
code = ''.join([str(secrets.randbelow(10)) for _ in range(length)])
|
||||||
|
return code
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def send_verification_email(email, verification_code):
|
||||||
|
"""发送验证码邮件"""
|
||||||
|
try:
|
||||||
|
# 邮件标题
|
||||||
|
subject = "决裂者终端 - 验证码"
|
||||||
|
|
||||||
|
# 邮件内容
|
||||||
|
message = f"""
|
||||||
|
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||||
|
<h2 style="color: #00AFFF;">决裂者终端</h2>
|
||||||
|
<p>您好!</p>
|
||||||
|
<p>您的验证码是:</p>
|
||||||
|
<div style="background: linear-gradient(135deg, #00AFFF, #0077CC);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 5px;
|
||||||
|
margin: 20px 0;">
|
||||||
|
{verification_code}
|
||||||
|
</div>
|
||||||
|
<p>此验证码将在 10 分钟内有效。</p>
|
||||||
|
<p>如果您没有请求此验证码,请忽略此邮件。</p>
|
||||||
|
<hr style="margin: 20px 0; border: none; border-top: 1px solid #ccc;">
|
||||||
|
<p style="color: #666; font-size: 12px;">© 2026 Sunderer Games. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 发送邮件
|
||||||
|
frappe.sendmail(
|
||||||
|
recipients=[email],
|
||||||
|
subject=subject,
|
||||||
|
message=message,
|
||||||
|
delayed=False # 立即发送
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"success": True, "message": "验证码已发送到您的邮箱"}
|
||||||
|
except Exception as e:
|
||||||
|
frappe.log_error(f"发送验证码邮件失败: {str(e)}")
|
||||||
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def send_signup_code(email):
|
||||||
|
"""发送注册验证码的主函数"""
|
||||||
|
try:
|
||||||
|
# 验证邮箱格式
|
||||||
|
import re
|
||||||
|
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||||
|
if not re.match(pattern, email):
|
||||||
|
return {"exc": "请输入有效的邮箱地址!"}
|
||||||
|
|
||||||
|
# 生成验证码
|
||||||
|
verification_code = generate_verification_code(6)
|
||||||
|
|
||||||
|
# 将验证码存储到缓存中(有效期10分钟)
|
||||||
|
cache_key = f"signup_code_{email}"
|
||||||
|
frappe.cache().set_value(cache_key, verification_code, expires_in_sec=600) # 10分钟
|
||||||
|
|
||||||
|
# 发送邮件
|
||||||
|
result = send_verification_email(email, verification_code)
|
||||||
|
|
||||||
|
if result["success"]:
|
||||||
|
return {"message": "验证码已发送到您的邮箱"}
|
||||||
|
else:
|
||||||
|
return {"exc": f"发送失败: {result['message']}"}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
frappe.log_error(f"发送注册验证码失败: {str(e)}")
|
||||||
|
return {"exc": f"发送失败: {str(e)}"}
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def verify_signup_code(email, code):
|
||||||
|
"""验证注册验证码"""
|
||||||
|
try:
|
||||||
|
cache_key = f"signup_code_{email}"
|
||||||
|
stored_code = frappe.cache().get_value(cache_key)
|
||||||
|
|
||||||
|
if not stored_code:
|
||||||
|
return {"exc": "验证码已过期,请重新获取"}
|
||||||
|
|
||||||
|
if str(stored_code) != str(code):
|
||||||
|
return {"exc": "验证码错误"}
|
||||||
|
|
||||||
|
# 验证成功,清除缓存
|
||||||
|
frappe.cache().delete_value(cache_key)
|
||||||
|
|
||||||
|
return {"success": True}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
frappe.log_error(f"验证注册验证码失败: {str(e)}")
|
||||||
|
return {"exc": f"验证失败: {str(e)}"}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# sunderer_app/sunderer_app/custom_user.py
|
||||||
|
import frappe
|
||||||
|
from frappe.core.doctype.user.user import User as CoreUser
|
||||||
|
|
||||||
|
class CustomUser(CoreUser):
|
||||||
|
def get_redirect_url(self):
|
||||||
|
"""自定义用户创建后的重定向URL"""
|
||||||
|
# 检查是否是新用户注册
|
||||||
|
if self.flags.in_insert and self.new_password:
|
||||||
|
# 新用户设置密码后,跳转到自定义页面
|
||||||
|
return "/custom-update-password"
|
||||||
|
|
||||||
|
# 默认行为
|
||||||
|
return super().get_redirect_url()
|
||||||
|
|
||||||
|
def after_user_creation(doc, method):
|
||||||
|
"""用户创建后触发的钩子"""
|
||||||
|
# 这里可以添加额外的用户初始化逻辑
|
||||||
|
pass
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# delete_signup_key.py
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
def delete_signup_key_doctype():
|
||||||
|
frappe.delete_doc("DocType", "Signup Key", force=True)
|
||||||
|
print("Signup Key DocType deleted.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
frappe.connect()
|
||||||
|
delete_signup_key_doctype()
|
||||||
|
frappe.db.commit()
|
||||||
|
frappe.destroy()
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Vue 3 + Vite
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||||
|
|
||||||
|
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/icon3.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>决裂者终端用户中心</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "my-vue-app",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --config vite.config.js",
|
||||||
|
"build": "vite build --config vite.config.js",
|
||||||
|
"preview": "vite preview --config vite.config.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@vicons/ionicons5": "^0.13.0",
|
||||||
|
"naive-ui": "^2.43.2",
|
||||||
|
"vue": "^3.4.0",
|
||||||
|
"vue-router": "^4.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^4.5.0",
|
||||||
|
"unplugin-auto-import": "^21.0.0",
|
||||||
|
"unplugin-vue-components": "^31.0.0",
|
||||||
|
"vite": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 60 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,21 @@
|
|||||||
|
<!-- src/App.vue -->
|
||||||
|
<script setup>
|
||||||
|
import { RouterView } from 'vue-router'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<RouterView />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 确保全屏撑开 */
|
||||||
|
html, body, #app {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: #000;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<script setup>
|
||||||
|
import LoginView from './views/LoginView.vue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const email = ref('')
|
||||||
|
const password = ref('')
|
||||||
|
const error = ref('')
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
async function handleLogin() {
|
||||||
|
loading.value = true
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/method/login', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: new URLSearchParams({ usr: email.value, pwd: password.value })
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
throw new Error(data.message || 'Invalid credentials')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录成功!跳转到 Frappe 应用
|
||||||
|
window.location.href = 'http://172.25.162.172:8000/app' // 或你的目标页面
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err.message
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div style="display: flex; justify-content: center; align-items: center; height: 100vh; background: #0f0f1b; color: white;">
|
||||||
|
<div style="background: #1a1a2e; padding: 2rem; border-radius: 10px; width: 300px; box-shadow: 0 4px 20px rgba(0,0,0,0.5);">
|
||||||
|
<h2 style="text-align: center; margin-bottom: 1.5rem;">Sunderer Login</h2>
|
||||||
|
<form @submit.prevent="handleLogin">
|
||||||
|
<input
|
||||||
|
v-model="email"
|
||||||
|
type="text"
|
||||||
|
placeholder="Email or Username"
|
||||||
|
style="width: 100%; padding: 0.5rem; margin: 0.5rem 0; border: 1px solid #444; background: #0f0f1b; color: white; border-radius: 4px;"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
style="width: 100%; padding: 0.5rem; margin: 0.5rem 0; border: 1px solid #444; background: #0f0f1b; color: white; border-radius: 4px;"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div v-if="error" style="color: #ff6b6b; margin-top: 0.5rem; font-size: 0.9rem;">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
:disabled="loading"
|
||||||
|
style="width: 100%; padding: 0.5rem; background: #4e54c8; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem;"
|
||||||
|
>
|
||||||
|
{{ loading ? 'Signing in...' : 'Sign In' }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 496 B |
@@ -0,0 +1,43 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
msg: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
const count = ref(0)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1>{{ msg }}</h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<button type="button" @click="count++">count is {{ count }}</button>
|
||||||
|
<p>
|
||||||
|
Edit
|
||||||
|
<code>components/HelloWorld.vue</code> to test HMR
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Check out
|
||||||
|
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||||
|
>create-vue</a
|
||||||
|
>, the official Vue + Vite starter
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Learn more about IDE Support for Vue in the
|
||||||
|
<a
|
||||||
|
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||||
|
target="_blank"
|
||||||
|
>Vue Docs Scaling up Guide</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.read-the-docs {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// src/increaseacc.js
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import IncreaseAccView from './views/IncreaseAccView.vue'
|
||||||
|
|
||||||
|
const app = createApp(IncreaseAccView)
|
||||||
|
|
||||||
|
// 挂载到 DOM
|
||||||
|
app.mount('#app')
|
||||||
|
|
||||||
|
// 导出 app 实例,以便 UMD 格式可以访问
|
||||||
|
// 如果指定了 name: 'IncreaseAccApp',这个 app 实例将可通过 window.IncreaseAccApp 访问
|
||||||
|
export default app
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
<!-- src/layouts/AppLayout.vue -->
|
||||||
|
<template>
|
||||||
|
<n-layout position="absolute">
|
||||||
|
<!-- 顶部栏 -->
|
||||||
|
<n-layout-header class="header" bordered>
|
||||||
|
<div class="header-content">
|
||||||
|
<h1>My Admin Panel</h1>
|
||||||
|
<div class="header-actions">
|
||||||
|
<n-dropdown trigger="click" :options="profileOptions" @select="handleProfileSelect">
|
||||||
|
<n-button text>
|
||||||
|
<n-avatar round size="small" :src="userAvatar" />
|
||||||
|
{{ userName }}
|
||||||
|
</n-button>
|
||||||
|
</n-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-layout-header>
|
||||||
|
|
||||||
|
<n-layout has-sider position="absolute" style="top: 64px;">
|
||||||
|
<!-- 侧边栏 -->
|
||||||
|
<n-layout-sider
|
||||||
|
bordered
|
||||||
|
collapse-mode="width"
|
||||||
|
:collapsed-width="64"
|
||||||
|
:width="240"
|
||||||
|
:native-scrollbar="false"
|
||||||
|
show-trigger
|
||||||
|
:collapsed="sCollapsed"
|
||||||
|
@collapse="sCollapsed = !sCollapsed"
|
||||||
|
@expand="sCollapsed = !sCollapsed"
|
||||||
|
>
|
||||||
|
<n-menu
|
||||||
|
v-model:value="activeKey"
|
||||||
|
:collapsed="sCollapsed"
|
||||||
|
:collapsed-width="64"
|
||||||
|
:collapsed-icon-size="22"
|
||||||
|
:options="menuOptions"
|
||||||
|
:indent="18"
|
||||||
|
/>
|
||||||
|
</n-layout-sider>
|
||||||
|
|
||||||
|
<!-- 主内容区 -->
|
||||||
|
<n-layout content-style="padding: 24px;">
|
||||||
|
<router-view /> <!-- 这里是后台各页面的渲染位置 -->
|
||||||
|
</n-layout>
|
||||||
|
</n-layout>
|
||||||
|
</n-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, h } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { NIcon, NText } from 'naive-ui'
|
||||||
|
import { BookOutline as BookIcon, PersonOutline as PersonIcon, LogOutOutline as LogoutIcon } from '@vicons/ionicons5'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const sCollapsed = ref(false)
|
||||||
|
const activeKey = ref('dashboard')
|
||||||
|
|
||||||
|
// 示例用户信息
|
||||||
|
const userName = ref('Admin User')
|
||||||
|
const userAvatar = ref('https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg')
|
||||||
|
|
||||||
|
function renderIcon(icon) {
|
||||||
|
return () => h(NIcon, null, { default: () => h(icon) })
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMenuLabel(option) {
|
||||||
|
if (sCollapsed.value) {
|
||||||
|
return h(NIcon, null, { default: () => h(option.icon) });
|
||||||
|
} else {
|
||||||
|
return h('span', null, option.label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 侧边栏菜单
|
||||||
|
const menuOptions = [
|
||||||
|
{
|
||||||
|
label: 'Dashboard',
|
||||||
|
key: 'dashboard',
|
||||||
|
icon: renderIcon(BookIcon),
|
||||||
|
props: { onClick: () => router.push('/app/dashboard') }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Users',
|
||||||
|
key: 'users',
|
||||||
|
icon: renderIcon(PersonIcon),
|
||||||
|
props: { onClick: () => router.push('/app/users') }
|
||||||
|
},
|
||||||
|
// 添加更多菜单项...
|
||||||
|
]
|
||||||
|
|
||||||
|
// 顶栏用户菜单
|
||||||
|
const profileOptions = [
|
||||||
|
{ label: 'Profile', key: 'profile' },
|
||||||
|
{ label: 'Settings', key: 'settings' },
|
||||||
|
{ type: 'divider', key: 'd1' },
|
||||||
|
{ label: 'Logout', key: 'logout', icon: renderIcon(LogoutIcon) }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 顶栏菜单选择处理
|
||||||
|
const handleProfileSelect = (key) => {
|
||||||
|
switch (key) {
|
||||||
|
case 'profile':
|
||||||
|
console.log('Go to Profile');
|
||||||
|
break;
|
||||||
|
case 'settings':
|
||||||
|
console.log('Go to Settings');
|
||||||
|
break;
|
||||||
|
case 'logout':
|
||||||
|
// 执行登出逻辑,例如清除 token
|
||||||
|
localStorage.removeItem('authToken'); // 示例
|
||||||
|
router.push('/login'); // 跳转回登录页
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.header {
|
||||||
|
height: 64px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions .n-button .n-avatar {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// src/main.js
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router' // 确保这个导入是正确的
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(router) // 使用路由器插件
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
// C:\vueprj\my-vue-app\src\router.js
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import LoginView from './views/LoginView.vue'
|
||||||
|
import RegisterView from './views/RegisterView.vue'
|
||||||
|
import CustomPasswordSetup from './views/CustomPasswordSetup.vue'
|
||||||
|
|
||||||
|
// --- 新增:导入后台布局和页面 ---
|
||||||
|
import AppLayout from './layouts/Applayout.vue' // 导入你创建的后台布局
|
||||||
|
const DashboardView = () => import('./views/sundererapp/DashboardView.vue') // 动态导入后台页面
|
||||||
|
const UsersView = () => import('./views/sundererapp/UsersView.vue') // 动态导入后台页面
|
||||||
|
// --- 新增结束 ---
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Home',
|
||||||
|
component: LoginView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'Login',
|
||||||
|
component: LoginView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/register',
|
||||||
|
name: 'Register',
|
||||||
|
component: RegisterView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/update-password',
|
||||||
|
name: 'CustomPasswordSetup',
|
||||||
|
component: CustomPasswordSetup
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/custom-update-password',
|
||||||
|
name: 'CustomPasswordSetupAlt',
|
||||||
|
component: CustomPasswordSetup
|
||||||
|
},
|
||||||
|
// --- 新增:后台路由组 ---
|
||||||
|
{
|
||||||
|
path: '/sundererapp', // 后台页面的基路径
|
||||||
|
component: AppLayout, // 使用 AppLayout 作为父组件
|
||||||
|
meta: { requiresAuth: true }, // 可选:标记此路由组需要认证
|
||||||
|
children: [ // 定义后台页面的子路由
|
||||||
|
{
|
||||||
|
path: '', // /sundererapp 访问时,默认重定向到 dashboard
|
||||||
|
redirect: '/sundererapp/dashboard'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'dashboard', // 最终路径为 /sundererapp/dashboard
|
||||||
|
name: 'AppDashboard',
|
||||||
|
component: DashboardView // 渲染 DashboardView 组件
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'users', // 最终路径为 /sundererapp/users
|
||||||
|
name: 'AppUsers',
|
||||||
|
component: UsersView // 渲染 UsersView 组件
|
||||||
|
},
|
||||||
|
// 你可以在这里添加更多后台页面,例如:
|
||||||
|
// {
|
||||||
|
// path: 'settings',
|
||||||
|
// name: 'AppSettings',
|
||||||
|
// component: () => import('./views/sundererapp/SettingsView.vue')
|
||||||
|
// }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// --- 新增结束 ---
|
||||||
|
{
|
||||||
|
path: '/:pathMatch(.*)*',
|
||||||
|
name: 'NotFound',
|
||||||
|
component: LoginView
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
:root {
|
||||||
|
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #242424;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #646cff;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #535bf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3.2em;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.6em 1.2em;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.25s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
border-color: #646cff;
|
||||||
|
}
|
||||||
|
button:focus,
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
color: #213547;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #747bff;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// src/main.js (或 src/sundererapp.js)
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import App from './App.vue' // 你可能有一个顶层的 App.vue,或者可以直接创建一个简单的根组件
|
||||||
|
import router from './router.js' // 导入你配置好的路由
|
||||||
|
|
||||||
|
// 如果你的 App.vue 是一个空壳,只是为了挂载 router-view
|
||||||
|
// 你也可以不导入 App.vue,直接创建一个根组件
|
||||||
|
// const RootComponent = {
|
||||||
|
// template: '<router-view />'
|
||||||
|
// }
|
||||||
|
// const app = createApp(RootComponent);
|
||||||
|
|
||||||
|
const app = createApp(App); // 创建 Vue 应用实例
|
||||||
|
|
||||||
|
app.use(router) // 安装 Vue Router 插件
|
||||||
|
|
||||||
|
app.mount('#app') // 挂载到 DOM
|
||||||
|
|
||||||
|
// 导出 app 实例,以便 UMD 格式可以访问
|
||||||
|
// 如果 vite.config.js 中指定了 name: 'SundererApp',这个 app 实例将可通过 window.SundererApp 访问
|
||||||
|
export default app
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
<template>
|
||||||
|
<div class="password-setup-container">
|
||||||
|
<div class="setup-card">
|
||||||
|
<h2>设置密码</h2>
|
||||||
|
|
||||||
|
<form @submit.prevent="setupPassword">
|
||||||
|
<div class="input-group">
|
||||||
|
<label>新密码</label>
|
||||||
|
<input
|
||||||
|
v-model="newPassword"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入新密码"
|
||||||
|
class="input-field"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label>确认密码</label>
|
||||||
|
<input
|
||||||
|
v-model="confirmPassword"
|
||||||
|
type="password"
|
||||||
|
placeholder="请再次输入密码"
|
||||||
|
class="input-field"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="error" class="error">{{ error }}</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
:disabled="loading"
|
||||||
|
class="btn-primary"
|
||||||
|
>
|
||||||
|
{{ loading ? '设置中...' : '设置密码' }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const newPassword = ref('')
|
||||||
|
const confirmPassword = ref('')
|
||||||
|
const error = ref('')
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const setupPassword = async () => {
|
||||||
|
if (newPassword.value !== confirmPassword.value) {
|
||||||
|
error.value = '两次输入的密码不一致'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPassword.value.length < 6) {
|
||||||
|
error.value = '密码长度至少为6位'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const actualKey = route.query.key
|
||||||
|
if (!actualKey) {
|
||||||
|
error.value = '无效的密码重置链接'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch('http://172.25.162.172:8000/api/method/frappe.core.doctype.user.user.update_password', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
key: actualKey,
|
||||||
|
new_password: newPassword.value
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
|
||||||
|
if (result.exc) {
|
||||||
|
error.value = result.exc || '设置密码失败'
|
||||||
|
} else {
|
||||||
|
alert('密码设置成功!')
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error.value = '网络错误,请稍后再试'
|
||||||
|
console.error(err)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.password-setup-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #00AFFF, #0077CC);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-card {
|
||||||
|
width: 400px;
|
||||||
|
padding: 30px;
|
||||||
|
background: rgba(0, 0, 0, 0.295);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 0 30px rgba(0, 175, 255, 0.4);
|
||||||
|
border: 1px solid rgba(0, 175, 255, 0.3);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field {
|
||||||
|
width: 94%;
|
||||||
|
padding: 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgb(255, 255, 255);
|
||||||
|
color: rgb(0, 0, 0);
|
||||||
|
font-size: 1rem;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 0 0 1px rgba(0, 175, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background: linear-gradient(90deg, #4CAF50, #45a049);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: #ff6b6b;
|
||||||
|
margin: 10px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<h2>设置您的密码</h2>
|
||||||
|
<form @submit.prevent="setupPassword" class="register-view">
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="password" id="password" v-model="password" required placeholder="请输入新密码" class="input-field"/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="password" id="confirmPassword" v-model="confirmPassword" required placeholder="请确认新密码" class="input-field"/>
|
||||||
|
</div>
|
||||||
|
<button type="submit" :disabled="loading" class="btn-primary">{{ loading ? '正在设置...' : '设置密码' }}</button>
|
||||||
|
<!-- 用于显示普通错误信息 -->
|
||||||
|
<div v-if="message" :class="{ 'success-message': success, 'error-message': !success }">{{ message }}</div>
|
||||||
|
<!-- 专门用于显示密码强度错误信息 -->
|
||||||
|
<div v-if="passwordStrengthMessage" class="password-strength-error">
|
||||||
|
{{ passwordStrengthMessage }}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="back-to-login">
|
||||||
|
<button @click="goBackToLogin" class="btn-secondary">← 返回登录</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'IncreaseAcc',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
message: '', // 用于存储一般性错误或成功信息
|
||||||
|
passwordStrengthMessage: '', // 专门用于存储密码强度错误信息
|
||||||
|
success: false,
|
||||||
|
loading: false,
|
||||||
|
key: null // 存储从 URL 获取的 key
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
this.key = urlParams.get('key');
|
||||||
|
console.log("从 URL 获取的 key:", this.key);
|
||||||
|
|
||||||
|
if (!this.key) {
|
||||||
|
this.message = '未找到密钥。';
|
||||||
|
this.success = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async setupPassword() {
|
||||||
|
// 清空之前的消息
|
||||||
|
this.message = '';
|
||||||
|
this.passwordStrengthMessage = '';
|
||||||
|
this.success = false;
|
||||||
|
|
||||||
|
if (this.password !== this.confirmPassword) {
|
||||||
|
this.message = '两次输入的密码不匹配。';
|
||||||
|
this.success = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.key) {
|
||||||
|
this.message = '缺少密钥参数。';
|
||||||
|
this.success = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/method/sunderer_app.api.custom_user_signup.complete_account', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({key: this.key, password: this.password})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log("API 响应数据:", data);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// 成功情况
|
||||||
|
this.message = "账户已激活,密码设置成功!";
|
||||||
|
this.success = true;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = 'http://172.25.162.172:8000/';
|
||||||
|
}, 5000); // 5秒后跳转
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// --- 修改错误处理逻辑 ---
|
||||||
|
let errorMessage = '操作失败,请稍后再试。';
|
||||||
|
let isPasswordStrengthError = false; // 标记是否是密码强度错误
|
||||||
|
|
||||||
|
if (data && data.exc) {
|
||||||
|
// 尝试解析 frappe.throw 返回的 exc 信息
|
||||||
|
try {
|
||||||
|
// Frappe 可能会将多个错误信息包装在一个字符串中,或者直接返回一个对象
|
||||||
|
let parsedExc = data.exc;
|
||||||
|
if (typeof data.exc === 'string') {
|
||||||
|
// 尝试解析为 JSON 对象(如果后端返回了 JSON 结构的 exc)
|
||||||
|
parsedExc = JSON.parse(data.exc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 parsedExc 是否是我们之前在后端构造的密码强度错误对象
|
||||||
|
if (parsedExc && parsedExc.type === 'password_strength') {
|
||||||
|
// 是密码强度错误
|
||||||
|
isPasswordStrengthError = true;
|
||||||
|
// 构建更友好的提示信息
|
||||||
|
const details = parsedExc.details;
|
||||||
|
const warning = details.warning || "";
|
||||||
|
const suggestions = details.suggestions || [];
|
||||||
|
|
||||||
|
const parts = [];
|
||||||
|
if (warning) parts.push(warning);
|
||||||
|
if (suggestions.length > 0) parts.push(`建议: ${suggestions.join(', ')}`);
|
||||||
|
|
||||||
|
this.passwordStrengthMessage = parts.join(' ');
|
||||||
|
} else {
|
||||||
|
// 不是密码强度错误,处理其他 exc
|
||||||
|
errorMessage = parsedExc._server_messages || parsedExc.message || data._server_messages || '未知错误';
|
||||||
|
this.message = errorMessage;
|
||||||
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
// 如果 exc 不是 JSON 格式,或者解析失败,则直接显示 exc 内容
|
||||||
|
console.error("解析后端错误信息失败:", parseError);
|
||||||
|
// 检查 exc 字符串是否包含密码强度相关的关键词
|
||||||
|
if (typeof data.exc === 'string' &&
|
||||||
|
(data.exc.includes("密码强度") || data.exc.includes("password") || data.exc.includes("strength") || data.exc.includes("重复"))) {
|
||||||
|
isPasswordStrengthError = true;
|
||||||
|
// 尝试从原始字符串中提取一些信息,或者给出通用提示
|
||||||
|
// 这里可以根据实际返回的字符串进行更精细的解析
|
||||||
|
this.passwordStrengthMessage = "密码强度不足,请设置一个更强的密码。";
|
||||||
|
} else {
|
||||||
|
this.message = data.exc || errorMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (data && data.message) {
|
||||||
|
// 检查 data.message 是否是密码强度错误对象
|
||||||
|
if (typeof data.message === 'object' && data.message.type === 'password_strength') {
|
||||||
|
isPasswordStrengthError = true;
|
||||||
|
const details = data.message.details;
|
||||||
|
const warning = details.warning || "";
|
||||||
|
const suggestions = details.suggestions || [];
|
||||||
|
|
||||||
|
const parts = [];
|
||||||
|
if (warning) parts.push(warning);
|
||||||
|
if (suggestions.length > 0) parts.push(`建议: ${suggestions.join(', ')}`);
|
||||||
|
|
||||||
|
this.passwordStrengthMessage = parts.join(' ');
|
||||||
|
} else {
|
||||||
|
// 不是密码强度错误,处理其他 message
|
||||||
|
errorMessage = typeof data.message === 'object' ? JSON.stringify(data.message) : data.message;
|
||||||
|
this.message = errorMessage;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果 data 既没有 exc 也没有 message,可能是网络错误或其他问题
|
||||||
|
this.message = '服务器返回了错误,但没有详细信息。';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果不是密码强度错误,则设置 general message
|
||||||
|
if (!isPasswordStrengthError && !this.passwordStrengthMessage) {
|
||||||
|
this.message = errorMessage; // 这行其实可能已经在上面的条件分支中设置了
|
||||||
|
}
|
||||||
|
// --- 修改结束 ---
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("网络错误或请求失败:", error);
|
||||||
|
this.message = '网络错误或请求失败。';
|
||||||
|
this.success = false;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
goBackToLogin() {
|
||||||
|
window.location.href = '/login'; // 跳转到登录页
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* ... (保持原有的 CSS 不变) ... */
|
||||||
|
|
||||||
|
.register-view {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #00AFFF, #0077CC);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 400px;
|
||||||
|
padding: 30px;
|
||||||
|
background: rgba(0, 0, 0, 0.295);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 0 30px rgba(0, 175, 255, 0.4);
|
||||||
|
border: 1px solid rgba(0, 175, 255, 0.3);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field {
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 10px 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgb(255, 255, 255);
|
||||||
|
color: rgb(0, 0, 0);
|
||||||
|
font-size: 1rem;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 0 0 1px rgba(0, 175, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background: linear-gradient(90deg, #4CAF50, #45a049);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 10px 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:disabled {
|
||||||
|
background: #666;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-message {
|
||||||
|
color: green;
|
||||||
|
margin-top: 15px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: red;
|
||||||
|
margin-top: 15px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 新增:专门用于显示密码强度错误的样式 */
|
||||||
|
.password-strength-error {
|
||||||
|
color: orange; /* 使用橙色区分于红色的一般错误 */
|
||||||
|
margin-top: 15px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.9em; /* 字体稍小一点 */
|
||||||
|
font-style: italic; /* 斜体强调 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-to-login {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
background: linear-gradient(90deg, #6bffce, #c74dff);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-container">
|
||||||
|
<!-- 背景几何图案 -->
|
||||||
|
<div class="bg-pattern"></div>
|
||||||
|
|
||||||
|
<!-- 登录卡 -->
|
||||||
|
<div class="login-card">
|
||||||
|
<div class="logo">
|
||||||
|
<span class="brand">决裂者终端</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form @submit.prevent="handleLogin">
|
||||||
|
<input
|
||||||
|
v-model="email"
|
||||||
|
type="text"
|
||||||
|
placeholder="请输入账号"
|
||||||
|
required
|
||||||
|
class="input-field"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
required
|
||||||
|
class="input-field"
|
||||||
|
/>
|
||||||
|
<div v-if="error" class="error">{{ error }}</div>
|
||||||
|
|
||||||
|
<div class="auth-buttons">
|
||||||
|
<button type="submit" :disabled="loginLoading" class="login-btn">
|
||||||
|
{{ loginLoading ? '正在登录. . .' : '登录' }}
|
||||||
|
</button>
|
||||||
|
<button type="button" @click="goToRegister" :disabled="registerLoading" class="register-btn">
|
||||||
|
{{ registerLoading ? '注册中...' : '注册' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="footer">© 2026 Sunderer games. </p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router' // ✅ 确保导入
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const email = ref('')
|
||||||
|
const password = ref('')
|
||||||
|
const error = ref('')
|
||||||
|
const loginLoading = ref(false)
|
||||||
|
const registerLoading = ref(false)
|
||||||
|
|
||||||
|
|
||||||
|
async function handleLogin() {
|
||||||
|
loginLoading.value = true
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/method/login', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: new URLSearchParams({ usr: email.value, pwd: password.value })
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
throw new Error(data.message || '账号或密码错误')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转到 Frappe 后台
|
||||||
|
window.location.href = 'http://172.25.162.172:8000/app'
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err.message
|
||||||
|
} finally {
|
||||||
|
loginLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRegister() {
|
||||||
|
|
||||||
|
registerLoading.value = true
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/method/frappe.core.doctype.user.user.sign_up', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: email.value,
|
||||||
|
full_name: email.value.split('@')[0], // 简单处理,可用其他字段
|
||||||
|
redirect_to: '/app'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
throw new Error(data.message || '注册失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册成功后,Frappe 会发送验证邮件
|
||||||
|
alert('注册成功!请查收邮箱验证链接。')
|
||||||
|
// 或跳转到提示页
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err.message
|
||||||
|
} finally {
|
||||||
|
registerLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function goToRegister() {
|
||||||
|
router.push('/register')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100vw;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
background: linear-gradient(135deg, #00AFFF, #0077CC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录卡片保持原样 */
|
||||||
|
.login-card {
|
||||||
|
background: rgba(0, 0, 0, 0.295);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 2.5rem;
|
||||||
|
width: 360px;
|
||||||
|
box-shadow: 0 0 30px rgba(0, 175, 255, 0.4);
|
||||||
|
border: 1px solid rgba(0, 175, 255, 0.3);
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center; /* 水平居中内容 */
|
||||||
|
align-items: center; /* 垂直居中内容 */
|
||||||
|
min-height: 300px; /* 防止高度塌陷 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 其他样式(logo、输入框、按钮等)保持不变... */
|
||||||
|
.logo {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
color: white;
|
||||||
|
text-shadow: 0 0 10px rgba(0, 175, 255, 0.8), 0 0 20px rgba(0, 175, 255, 0.6);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
color: white;
|
||||||
|
font-size: 2.2rem;
|
||||||
|
text-shadow: 0 0 10px rgba(0, 175, 255, 0.8), 0 0 20px rgba(0, 175, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
color: #00FFFF;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
margin: 0 0.5rem;
|
||||||
|
text-shadow: 0 0 15px rgba(0, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field {
|
||||||
|
width: 80%;
|
||||||
|
padding: 0.8rem 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgb(255, 255, 255);
|
||||||
|
color: rgb(0, 0, 0);
|
||||||
|
font-size: 1rem;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 0 0 1px rgba(0, 175, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field:focus {
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 175, 255, 0.7), 0 0 10px rgba(0, 175, 255, 0.4);
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: #ff6b6b;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin: -0.5rem 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
width: 70%;
|
||||||
|
padding: 0.8rem;
|
||||||
|
background: linear-gradient(90deg, #00AFFF, #0077CC);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
box-shadow: 0 0 15px rgba(0, 175, 255, 0.5);
|
||||||
|
}
|
||||||
|
.register-btn {
|
||||||
|
width: 70%;
|
||||||
|
padding: 0.8rem;
|
||||||
|
background: linear-gradient(90deg, #6bffce, #c74dff);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
box-shadow: 0 0 15px rgba(255, 107, 107, 0.5);
|
||||||
|
}
|
||||||
|
.login-btn:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 0 20px rgba(0, 175, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.auth-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.register-btn:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 0 20px rgba(255, 107, 107, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-btn:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
<template>
|
||||||
|
<div class="register-view">
|
||||||
|
<div class="container">
|
||||||
|
<h2>用户注册</h2>
|
||||||
|
|
||||||
|
<div v-if="step === 'email'" class="step-email">
|
||||||
|
<input v-model="email" type="email" placeholder="请输入邮箱地址" class="input-field" />
|
||||||
|
<button @click="sendCode" :disabled="sending" class="btn-primary">
|
||||||
|
{{ sending ? '发送中...' : '发送验证码' }}
|
||||||
|
</button>
|
||||||
|
<p v-if="error" class="error-msg">{{ error }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="step === 'verify'" class="step-verify">
|
||||||
|
<input v-model="code" type="text" placeholder="请输入6位验证码" maxlength="6" class="input-field" />
|
||||||
|
<button @click="verifyAndSignup" class="btn-primary">
|
||||||
|
完成注册
|
||||||
|
</button>
|
||||||
|
<p v-if="error" class="error-msg">{{ error }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加返回登录按钮 -->
|
||||||
|
<div class="back-to-login">
|
||||||
|
<button @click="goBackToLogin" class="btn-secondary">
|
||||||
|
← 返回登录
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router' // 引入路由
|
||||||
|
|
||||||
|
const router = useRouter() // 获取路由实例
|
||||||
|
const email = ref('')
|
||||||
|
const code = ref('')
|
||||||
|
const step = ref('email')
|
||||||
|
const error = ref('')
|
||||||
|
const sending = ref(false)
|
||||||
|
|
||||||
|
const validateEmail = (email) => {
|
||||||
|
const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
|
||||||
|
return regex.test(email)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendCode = async () => {
|
||||||
|
if (!validateEmail(email.value)) {
|
||||||
|
error.value = '请输入有效的邮箱地址!'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sending.value = true
|
||||||
|
error.value = ''
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/method/sunderer_app.api.verification.send_signup_code', { // 使用相对路径
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: email.value
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
|
||||||
|
if (result.exc) {
|
||||||
|
error.value = result.exc
|
||||||
|
} else {
|
||||||
|
step.value = 'verify'
|
||||||
|
console.log('验证码发送成功')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error.value = '网络错误,请稍后再试'
|
||||||
|
console.error(err)
|
||||||
|
} finally {
|
||||||
|
sending.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifyAndSignup = async () => {
|
||||||
|
console.log("DEBUG: verifyAndSignup function called");
|
||||||
|
console.log('点击完成注册')
|
||||||
|
console.log('email:', email.value)
|
||||||
|
console.log('code:', code.value)
|
||||||
|
|
||||||
|
if (code.value.length !== 6) {
|
||||||
|
error.value = '请输入6位验证码!'
|
||||||
|
console.log("DEBUG: Code length check failed");
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log("DEBUG: Code length check passed");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 验证验证码
|
||||||
|
const verifyResponse = await fetch('/api/method/sunderer_app.api.verification.verify_signup_code', { // 使用相对路径
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: email.value,
|
||||||
|
code: code.value
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const verifyResult = await verifyResponse.json()
|
||||||
|
console.log("DEBUG: Verify API Response:", verifyResult);
|
||||||
|
|
||||||
|
if (verifyResult.exc) {
|
||||||
|
error.value = verifyResult.exc
|
||||||
|
console.log("DEBUG: Verify API returned error:", verifyResult.exc);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log("DEBUG: Verify API succeeded, proceeding to trigger email...");
|
||||||
|
|
||||||
|
// 2. 验证成功后,调用新API发送包含 IncreaseAcc 链接的邮件
|
||||||
|
// 注意:你需要在后端创建一个新API来处理这个逻辑
|
||||||
|
const triggerEmailResponse = await fetch('/api/method/sunderer_app.api.custom_user_signup.trigger_account_setup_email', { // 新API路径
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: email.value
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const triggerEmailResult = await triggerEmailResponse.json();
|
||||||
|
console.log("DEBUG: Trigger Email API Response:", triggerEmailResult);
|
||||||
|
|
||||||
|
if (triggerEmailResult.exc) {
|
||||||
|
error.value = triggerEmailResult.exc;
|
||||||
|
console.log("DEBUG: Trigger Email API returned error:", triggerEmailResult.exc);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 提示用户并结束当前流程
|
||||||
|
alert('验证成功!请查收邮箱中的注册链接,点击链接完成账户设置。');
|
||||||
|
console.log('Verification successful, email sent with link.');
|
||||||
|
// 可以选择清空输入框或跳转到等待页面,这里简单提示后清空
|
||||||
|
email.value = '';
|
||||||
|
code.value = '';
|
||||||
|
step.value = 'email'; // 回到初始状态,或者可以设计一个等待页面
|
||||||
|
error.value = ''; // 清除可能存在的旧错误
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
error.value = '网络异常,请稍后再试';
|
||||||
|
console.error('注册异常:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const goBackToLogin = () => {
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* ... CSS 样式保持不变 ... */
|
||||||
|
.register-view {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #00AFFF, #0077CC);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 400px;
|
||||||
|
padding: 30px;
|
||||||
|
background: rgba(0, 0, 0, 0.295);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 0 30px rgba(0, 175, 255, 0.4);
|
||||||
|
border: 1px solid rgba(0, 175, 255, 0.3);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field {
|
||||||
|
width: 90%;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 10px 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgb(255, 255, 255);
|
||||||
|
color: rgb(0, 0, 0);
|
||||||
|
font-size: 1rem;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 0 0 1px rgba(0, 175, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background: linear-gradient(90deg, #4CAF50, #45a049);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 10px 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:disabled {
|
||||||
|
background: #666;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
background: linear-gradient(90deg, #6bffce, #c74dff);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-msg {
|
||||||
|
color: #ff6b6b;
|
||||||
|
margin: 10px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-to-login {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<!-- src/views/app/DashboardView.vue -->
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<n-h2>Dashboard</n-h2>
|
||||||
|
<p>Welcome to your admin dashboard!</p>
|
||||||
|
<n-card title="Quick Stats" style="margin-top: 20px;">
|
||||||
|
<n-statistic label="Total Users" value="12345" />
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
// Dashboard 相关逻辑
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<!-- src/views/app/UsersView.vue -->
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<n-h2>Users Management</n-h2>
|
||||||
|
<p>This is the users management page.</p>
|
||||||
|
<n-data-table :columns="columns" :data="data" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { h } from 'vue'
|
||||||
|
import { NTag, NButton } from 'naive-ui'
|
||||||
|
|
||||||
|
// 示例数据
|
||||||
|
const columns = [
|
||||||
|
{ title: 'Name', key: 'name' },
|
||||||
|
{ title: 'Age', key: 'age' },
|
||||||
|
{ title: 'Tags', key: 'tags', render: (row) => row.tags.map(tag => h(NTag, { type: 'info' }, { default: () => tag })) },
|
||||||
|
{ title: 'Actions', key: 'actions', render: () => h(NButton, { size: 'small', type: 'primary' }, { default: () => 'Edit' }) }
|
||||||
|
]
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
{ name: 'John Doe', age: 32, tags: ['admin', 'dev'] },
|
||||||
|
{ name: 'Jane Smith', age: 28, tags: ['user'] }
|
||||||
|
]
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
// vite.dev.config.js
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
base: '/assets/sunderer_app/dist/', // 开发环境使用根路径
|
||||||
|
|
||||||
|
server: {
|
||||||
|
host: '0.0.0.0',
|
||||||
|
port: 5173,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:8000',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: '../public/dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
assetsDir: '',
|
||||||
|
manifest: true,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
entryFileNames: 'js/[name].[hash].js',
|
||||||
|
chunkFileNames: 'js/[name].[hash].js',
|
||||||
|
assetFileNames: (assetInfo) => {
|
||||||
|
if (assetInfo.name.endsWith('.css')) {
|
||||||
|
return 'css/[name].[hash].[ext]';
|
||||||
|
}
|
||||||
|
return 'assets/[name].[hash].[ext]';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
// vite.config.js
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
import AutoImport from 'unplugin-auto-import/vite'
|
||||||
|
import Components from 'unplugin-vue-components/vite'
|
||||||
|
// 导入 Naive UI 的解析器,告诉插件哪些组件属于 Naive UI
|
||||||
|
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
AutoImport({
|
||||||
|
imports: [
|
||||||
|
'vue',
|
||||||
|
'vue-router',
|
||||||
|
{
|
||||||
|
// 配置自动导入 Naive UI 的特定 API
|
||||||
|
'naive-ui': [
|
||||||
|
'useMessage', // 用于显示消息提示
|
||||||
|
'useDialog', // 用于显示对话框
|
||||||
|
'useNotification', // 用于显示通知
|
||||||
|
'useLoadingBar' // 用于显示顶部加载条
|
||||||
|
// 你可以根据需要添加更多,例如 'NButton', 'NCard' 等,
|
||||||
|
// 但通常推荐让 Components 插件处理组件导入,这里只放 API。
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
Components({
|
||||||
|
resolvers: [NaiveUiResolver()]
|
||||||
|
})
|
||||||
|
],
|
||||||
|
build: {
|
||||||
|
outDir: '../public/SundererPage',
|
||||||
|
emptyOutDir: false,
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
sundererapp: 'src/sundererapp.js' // 入口文件
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
format: 'umd', // 设置输出格式为 UMD
|
||||||
|
name: 'SundererApp', // 指定全局变量名,构建后应用会挂载到 window.IncreaseAccApp
|
||||||
|
// 可以保留 hash 以利用缓存,但 UMD 通常用于简单场景,可选
|
||||||
|
entryFileNames: 'js/sundererapp.[hash].js', // 例如: increaseacc.js (或 [name].[hash].js)
|
||||||
|
chunkFileNames: 'js/sundererapp.[hash].js',
|
||||||
|
assetFileNames: 'js/sundererapp.[hash].[ext]', // 例如: increaseacc.css (或 [name].[hash].[ext])
|
||||||
|
// UMD 格式可能需要外部依赖,如果 Vue 是全局引入的,需要声明
|
||||||
|
// globals: {
|
||||||
|
// vue: 'Vue' // 假设 Vue 已通过 script 标签全局引入
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
app_name = "sunderer_app"
|
||||||
|
app_title = "Sunderer"
|
||||||
|
app_publisher = "test"
|
||||||
|
app_description = "test"
|
||||||
|
app_email = "898879719@qq.com"
|
||||||
|
app_license = "agpl-3.0"
|
||||||
|
|
||||||
|
# sunderer_app/hooks.py
|
||||||
|
website_context = {
|
||||||
|
"favicon": "/assets/sunderer_app/icon3.svg",
|
||||||
|
"splash_image": "/assets/sunderer_app/images/icon3.svg"
|
||||||
|
}
|
||||||
|
|
||||||
|
website_route_rules = [
|
||||||
|
{"from_route": "/sundererapp/<path:app_path>", "to_route": "sundererapp/index"},
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
# 禁用 Frappe 默认的登录重定向(非常重要!)
|
||||||
|
disable_website_login_redirect = True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Apps
|
||||||
|
# ------------------
|
||||||
|
|
||||||
|
# required_apps = []
|
||||||
|
|
||||||
|
# Each item in the list will be shown as an app in the apps page
|
||||||
|
# add_to_apps_screen = [
|
||||||
|
# {
|
||||||
|
# "name": "sunderer_app",
|
||||||
|
# "logo": "/assets/sunderer_app/logo.png",
|
||||||
|
# "title": "Sunderer",
|
||||||
|
# "route": "/sunderer_app",
|
||||||
|
# "has_permission": "sunderer_app.api.permission.has_app_permission"
|
||||||
|
# }
|
||||||
|
# ]
|
||||||
|
|
||||||
|
# Includes in <head>
|
||||||
|
# ------------------
|
||||||
|
|
||||||
|
# include js, css files in header of desk.html
|
||||||
|
# app_include_css = "/assets/sunderer_app/css/sunderer_app.css"
|
||||||
|
# app_include_js = "/assets/sunderer_app/js/sunderer_app.js"
|
||||||
|
|
||||||
|
# include js, css files in header of web template
|
||||||
|
# web_include_css = "/assets/sunderer_app/css/sunderer_app.css"
|
||||||
|
# web_include_js = "/assets/sunderer_app/js/sunderer_app.js"
|
||||||
|
|
||||||
|
# include custom scss in every website theme (without file extension ".scss")
|
||||||
|
# website_theme_scss = "sunderer_app/public/scss/website"
|
||||||
|
|
||||||
|
# include js, css files in header of web form
|
||||||
|
# webform_include_js = {"doctype": "public/js/doctype.js"}
|
||||||
|
# webform_include_css = {"doctype": "public/css/doctype.css"}
|
||||||
|
|
||||||
|
# include js in page
|
||||||
|
# page_js = {"page" : "public/js/file.js"}
|
||||||
|
|
||||||
|
# include js in doctype views
|
||||||
|
# doctype_js = {"doctype" : "public/js/doctype.js"}
|
||||||
|
# doctype_list_js = {"doctype" : "public/js/doctype_list.js"}
|
||||||
|
# doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"}
|
||||||
|
# doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"}
|
||||||
|
|
||||||
|
# Svg Icons
|
||||||
|
# ------------------
|
||||||
|
# include app icons in desk
|
||||||
|
# app_include_icons = "sunderer_app/public/icons.svg"
|
||||||
|
|
||||||
|
# Home Pages
|
||||||
|
# ----------
|
||||||
|
|
||||||
|
# application home page (will override Website Settings)
|
||||||
|
# home_page = "login"
|
||||||
|
|
||||||
|
# website user home page (by Role)
|
||||||
|
# role_home_page = {
|
||||||
|
# "Role": "home_page"
|
||||||
|
# }
|
||||||
|
|
||||||
|
# Generators
|
||||||
|
# ----------
|
||||||
|
|
||||||
|
# automatically create page for each record of this doctype
|
||||||
|
# website_generators = ["Web Page"]
|
||||||
|
|
||||||
|
# Jinja
|
||||||
|
# ----------
|
||||||
|
|
||||||
|
# add methods and filters to jinja environment
|
||||||
|
# jinja = {
|
||||||
|
# "methods": "sunderer_app.utils.jinja_methods",
|
||||||
|
# "filters": "sunderer_app.utils.jinja_filters"
|
||||||
|
# }
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
# ------------
|
||||||
|
|
||||||
|
# before_install = "sunderer_app.install.before_install"
|
||||||
|
# after_install = "sunderer_app.install.after_install"
|
||||||
|
|
||||||
|
# Uninstallation
|
||||||
|
# ------------
|
||||||
|
|
||||||
|
# before_uninstall = "sunderer_app.uninstall.before_uninstall"
|
||||||
|
# after_uninstall = "sunderer_app.uninstall.after_uninstall"
|
||||||
|
|
||||||
|
# Integration Setup
|
||||||
|
# ------------------
|
||||||
|
# To set up dependencies/integrations with other apps
|
||||||
|
# Name of the app being installed is passed as an argument
|
||||||
|
|
||||||
|
# before_app_install = "sunderer_app.utils.before_app_install"
|
||||||
|
# after_app_install = "sunderer_app.utils.after_app_install"
|
||||||
|
|
||||||
|
# Integration Cleanup
|
||||||
|
# -------------------
|
||||||
|
# To clean up dependencies/integrations with other apps
|
||||||
|
# Name of the app being uninstalled is passed as an argument
|
||||||
|
|
||||||
|
# before_app_uninstall = "sunderer_app.utils.before_app_uninstall"
|
||||||
|
# after_app_uninstall = "sunderer_app.utils.after_app_uninstall"
|
||||||
|
|
||||||
|
# Desk Notifications
|
||||||
|
# ------------------
|
||||||
|
# See frappe.core.notifications.get_notification_config
|
||||||
|
|
||||||
|
# notification_config = "sunderer_app.notifications.get_notification_config"
|
||||||
|
|
||||||
|
# Permissions
|
||||||
|
# -----------
|
||||||
|
# Permissions evaluated in scripted ways
|
||||||
|
|
||||||
|
# permission_query_conditions = {
|
||||||
|
# "Event": "frappe.desk.doctype.event.event.get_permission_query_conditions",
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# has_permission = {
|
||||||
|
# "Event": "frappe.desk.doctype.event.event.has_permission",
|
||||||
|
# }
|
||||||
|
|
||||||
|
# DocType Class
|
||||||
|
# ---------------
|
||||||
|
# Override standard doctype classes
|
||||||
|
|
||||||
|
# override_doctype_class = {
|
||||||
|
# "ToDo": "custom_app.overrides.CustomToDo"
|
||||||
|
# }
|
||||||
|
|
||||||
|
# Document Events
|
||||||
|
# ---------------
|
||||||
|
# Hook on document methods and events
|
||||||
|
|
||||||
|
# doc_events = {
|
||||||
|
# "*": {
|
||||||
|
# "on_update": "method",
|
||||||
|
# "on_cancel": "method",
|
||||||
|
# "on_trash": "method"
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
|
||||||
|
# Scheduled Tasks
|
||||||
|
# ---------------
|
||||||
|
|
||||||
|
# scheduler_events = {
|
||||||
|
# "all": [
|
||||||
|
# "sunderer_app.tasks.all"
|
||||||
|
# ],
|
||||||
|
# "daily": [
|
||||||
|
# "sunderer_app.tasks.daily"
|
||||||
|
# ],
|
||||||
|
# "hourly": [
|
||||||
|
# "sunderer_app.tasks.hourly"
|
||||||
|
# ],
|
||||||
|
# "weekly": [
|
||||||
|
# "sunderer_app.tasks.weekly"
|
||||||
|
# ],
|
||||||
|
# "monthly": [
|
||||||
|
# "sunderer_app.tasks.monthly"
|
||||||
|
# ],
|
||||||
|
# }
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
# -------
|
||||||
|
|
||||||
|
# before_tests = "sunderer_app.install.before_tests"
|
||||||
|
|
||||||
|
# Overriding Methods
|
||||||
|
# ------------------------------
|
||||||
|
#
|
||||||
|
# override_whitelisted_methods = {
|
||||||
|
# "frappe.desk.doctype.event.event.get_events": "sunderer_app.event.get_events"
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# each overriding function accepts a `data` argument;
|
||||||
|
# generated from the base implementation of the doctype dashboard,
|
||||||
|
# along with any modifications made in other Frappe apps
|
||||||
|
# override_doctype_dashboards = {
|
||||||
|
# "Task": "sunderer_app.task.get_dashboard_data"
|
||||||
|
# }
|
||||||
|
|
||||||
|
# exempt linked doctypes from being automatically cancelled
|
||||||
|
#
|
||||||
|
# auto_cancel_exempted_doctypes = ["Auto Repeat"]
|
||||||
|
|
||||||
|
# Ignore links to specified DocTypes when deleting documents
|
||||||
|
# -----------------------------------------------------------
|
||||||
|
|
||||||
|
# ignore_links_on_delete = ["Communication", "ToDo"]
|
||||||
|
|
||||||
|
# Request Events
|
||||||
|
# ----------------
|
||||||
|
# before_request = ["sunderer_app.utils.before_request"]
|
||||||
|
# after_request = ["sunderer_app.utils.after_request"]
|
||||||
|
|
||||||
|
# Job Events
|
||||||
|
# ----------
|
||||||
|
# before_job = ["sunderer_app.utils.before_job"]
|
||||||
|
# after_job = ["sunderer_app.utils.after_job"]
|
||||||
|
|
||||||
|
# User Data Protection
|
||||||
|
# --------------------
|
||||||
|
|
||||||
|
# user_data_fields = [
|
||||||
|
# {
|
||||||
|
# "doctype": "{doctype_1}",
|
||||||
|
# "filter_by": "{filter_by}",
|
||||||
|
# "redact_fields": ["{field_1}", "{field_2}"],
|
||||||
|
# "partial": 1,
|
||||||
|
# },
|
||||||
|
# {
|
||||||
|
# "doctype": "{doctype_2}",
|
||||||
|
# "filter_by": "{filter_by}",
|
||||||
|
# "partial": 1,
|
||||||
|
# },
|
||||||
|
# {
|
||||||
|
# "doctype": "{doctype_3}",
|
||||||
|
# "strict": False,
|
||||||
|
# },
|
||||||
|
# {
|
||||||
|
# "doctype": "{doctype_4}"
|
||||||
|
# }
|
||||||
|
# ]
|
||||||
|
|
||||||
|
# Authentication and authorization
|
||||||
|
# --------------------------------
|
||||||
|
|
||||||
|
# auth_hooks = [
|
||||||
|
# "sunderer_app.auth.validate"
|
||||||
|
# ]
|
||||||
|
|
||||||
|
# Automatically update python controller files with type annotations for this app.
|
||||||
|
# export_python_type_annotations = True
|
||||||
|
|
||||||
|
# default_log_clearing_doctypes = {
|
||||||
|
# "Logging DocType Name": 30 # days to retain logs
|
||||||
|
# }
|
||||||
|
|
||||||
|
# Translation
|
||||||
|
# ------------
|
||||||
|
# List of apps whose translatable strings should be excluded from this app's translations.
|
||||||
|
# ignore_translatable_strings_from = []
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Sunderer
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[pre_model_sync]
|
||||||
|
# Patches added in this section will be executed before doctypes are migrated
|
||||||
|
# Read docs to understand patches: https://frappeframework.com/docs/v14/user/en/database-migrations
|
||||||
|
|
||||||
|
[post_model_sync]
|
||||||
|
# Patches added in this section will be executed after doctypes are migrated
|
||||||
|
After Width: | Height: | Size: 60 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 60 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"index.html": {
|
||||||
|
"file": "js/index.c1R21Jxr.js",
|
||||||
|
"name": "index",
|
||||||
|
"src": "index.html",
|
||||||
|
"isEntry": true,
|
||||||
|
"css": [
|
||||||
|
"css/index.CJO3dQ9s.css"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
html,body,#app{height:100%;margin:0;padding:0;background:#000;overflow:hidden}.login-container[data-v-6eda41c4]{position:relative;width:100vw;min-height:100vh;display:flex;justify-content:center;align-items:center;overflow:hidden;background:linear-gradient(135deg,#00afff,#07c)}.login-card[data-v-6eda41c4]{background:#0000004b;-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);border-radius:20px;padding:2.5rem;width:360px;box-shadow:0 0 30px #00afff66;border:1px solid rgba(0,175,255,.3);position:relative;z-index:1;text-align:center;display:flex;flex-direction:column;justify-content:center;align-items:center;min-height:300px}.logo[data-v-6eda41c4]{font-family:Arial,sans-serif;font-weight:700;margin-bottom:1.5rem;letter-spacing:1px;color:#fff;text-shadow:0 0 10px rgba(0,175,255,.8),0 0 20px rgba(0,175,255,.6);display:inline-flex;align-items:center;gap:.5rem}.brand[data-v-6eda41c4]{color:#fff;font-size:2.2rem;text-shadow:0 0 10px rgba(0,175,255,.8),0 0 20px rgba(0,175,255,.6)}.icon[data-v-6eda41c4]{color:#0ff;font-size:1.8rem;margin:0 .5rem;text-shadow:0 0 15px rgba(0,255,255,.7)}.input-field[data-v-6eda41c4]{width:80%;padding:.8rem 1rem;margin-bottom:1rem;border:none;border-radius:10px;background:#fff;color:#000;font-size:1rem;outline:none;transition:all .3s ease;box-shadow:0 0 0 1px #00afff4d}.input-field[data-v-6eda41c4]:focus{box-shadow:0 0 0 2px #00afffb3,0 0 10px #00afff66}.error[data-v-6eda41c4]{color:#ff6b6b;font-size:.85rem;margin:-.5rem 0 1rem}.login-btn[data-v-6eda41c4]{width:70%;padding:.8rem;background:linear-gradient(90deg,#00afff,#07c);color:#fff;border:none;border-radius:10px;font-size:1rem;font-weight:600;cursor:pointer;transition:transform .2s,box-shadow .2s;box-shadow:0 0 15px #00afff80}.register-btn[data-v-6eda41c4]{width:70%;padding:.8rem;background:linear-gradient(90deg,#6bffce,#c74dff);color:#fff;border:none;border-radius:10px;font-size:1rem;font-weight:600;cursor:pointer;transition:transform .2s,box-shadow .2s;box-shadow:0 0 15px #ff6b6b80}.login-btn[data-v-6eda41c4]:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 0 20px #00afffb3}.login-btn[data-v-6eda41c4]:disabled{opacity:.7;cursor:not-allowed}.auth-buttons[data-v-6eda41c4]{display:flex;gap:1rem;justify-content:center;margin-top:1.5rem}.footer[data-v-6eda41c4]{margin-top:1.5rem;color:#fff9;font-size:.8rem;text-align:center}.register-btn[data-v-6eda41c4]:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 0 20px #ff6b6bb3}.register-btn[data-v-6eda41c4]:disabled{opacity:.7;cursor:not-allowed}.register-view[data-v-14690773]{display:flex;justify-content:center;align-items:center;min-height:100vh;background:linear-gradient(135deg,#00afff,#07c);color:#fff}.container[data-v-14690773]{width:400px;padding:30px;background:#0000004b;-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);border-radius:20px;box-shadow:0 0 30px #00afff66;border:1px solid rgba(0,175,255,.3);text-align:center}.input-field[data-v-14690773]{width:90%;padding:20px;margin:10px 0;border:none;border-radius:10px;background:#fff;color:#000;font-size:1rem;outline:none;transition:all .3s ease;box-shadow:0 0 0 1px #00afff4d}.btn-primary[data-v-14690773]{width:100%;padding:12px;background:linear-gradient(90deg,#4caf50,#45a049);color:#fff;border:none;border-radius:10px;cursor:pointer;margin:10px 0;font-size:1rem;font-weight:600;transition:transform .2s,box-shadow .2s}.btn-primary[data-v-14690773]:disabled{background:#666;cursor:not-allowed}.btn-secondary[data-v-14690773]{width:100%;padding:10px;background:linear-gradient(90deg,#6bffce,#c74dff);color:#fff;border:none;border-radius:10px;cursor:pointer;margin-top:20px;font-size:1rem;font-weight:600;transition:transform .2s,box-shadow .2s}.error-msg[data-v-14690773]{color:#ff6b6b;margin:10px 0;font-size:14px}.back-to-login[data-v-14690773]{margin-top:20px}.password-setup-container[data-v-378de9a4]{display:flex;justify-content:center;align-items:center;min-height:100vh;background:linear-gradient(135deg,#00afff,#07c);color:#fff}.setup-card[data-v-378de9a4]{width:400px;padding:30px;background:#0000004b;-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);border-radius:20px;box-shadow:0 0 30px #00afff66;border:1px solid rgba(0,175,255,.3);text-align:center}.input-group[data-v-378de9a4]{margin-bottom:15px;text-align:left}.input-group label[data-v-378de9a4]{display:block;margin-bottom:5px;font-weight:700}.input-field[data-v-378de9a4]{width:94%;padding:12px;border:none;border-radius:10px;background:#fff;color:#000;font-size:1rem;outline:none;transition:all .3s ease;box-shadow:0 0 0 1px #00afff4d}.btn-primary[data-v-378de9a4]{width:100%;padding:12px;background:linear-gradient(90deg,#4caf50,#45a049);color:#fff;border:none;border-radius:10px;cursor:pointer;margin-top:20px;font-size:1rem;font-weight:600;transition:transform .2s,box-shadow .2s}.error[data-v-378de9a4]{color:#ff6b6b;margin:10px 0;font-size:14px}
|
||||||
|
After Width: | Height: | Size: 60 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/assets/sunderer_app/dist/icon3.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>决裂者终端用户中心</title>
|
||||||
|
<script type="module" crossorigin src="/assets/sunderer_app/dist/js/index.c1R21Jxr.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/sunderer_app/dist/css/index.CJO3dQ9s.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,90 @@
|
|||||||
|
# sunderer_app/sunderer_app/setup_doctype.py
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||||
|
|
||||||
|
def create_signup_key_doctype():
|
||||||
|
if frappe.db.exists("DocType", "Signup Key"):
|
||||||
|
print("DocType 'Signup Key' already exists.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("Creating DocType 'Signup Key'...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 创建 DocType 文档对象
|
||||||
|
signup_key_doctype = frappe.new_doc("DocType")
|
||||||
|
signup_key_doctype.name = "Signup Key"
|
||||||
|
signup_key_doctype.module = "Sunderer" # 替换为你应用的模块名
|
||||||
|
signup_key_doctype.is_submittable = 0
|
||||||
|
signup_key_doctype.is_tree = 0
|
||||||
|
signup_key_doctype.istable = 0
|
||||||
|
# signup_key_doctype.autoname = "field:key" # <--- 移除这一行!
|
||||||
|
# signup_key_doctype.autoname = "hash" # <--- 或者选择这一行 (如果你想用哈希作为 name)
|
||||||
|
signup_key_doctype.allow_rename = 0
|
||||||
|
|
||||||
|
# 插入 DocType (此时字段还不存在)
|
||||||
|
signup_key_doctype.insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
# 创建字段
|
||||||
|
# 1. Key 字段
|
||||||
|
key_field = frappe.new_doc("DocField")
|
||||||
|
key_field.parent = "Signup Key"
|
||||||
|
key_field.parenttype = "DocType"
|
||||||
|
key_field.parentfield = "fields"
|
||||||
|
key_field.fieldname = "key"
|
||||||
|
key_field.label = "Key"
|
||||||
|
key_field.fieldtype = "Data"
|
||||||
|
key_field.reqd = 1 # 必填
|
||||||
|
key_field.unique = 1 # 唯一索引,很重要!
|
||||||
|
key_field.insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
# 2. Email 字段
|
||||||
|
email_field = frappe.new_doc("DocField")
|
||||||
|
email_field.parent = "Signup Key"
|
||||||
|
email_field.parenttype = "DocType"
|
||||||
|
email_field.parentfield = "fields"
|
||||||
|
email_field.fieldname = "email"
|
||||||
|
email_field.label = "Email"
|
||||||
|
email_field.fieldtype = "Data"
|
||||||
|
email_field.options = "Email"
|
||||||
|
email_field.reqd = 1 # 必填
|
||||||
|
email_field.insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
# 3. Used 字段
|
||||||
|
used_field = frappe.new_doc("DocField")
|
||||||
|
used_field.parent = "Signup Key"
|
||||||
|
used_field.parenttype = "DocType"
|
||||||
|
used_field.parentfield = "fields"
|
||||||
|
used_field.fieldname = "used"
|
||||||
|
used_field.label = "Used"
|
||||||
|
used_field.fieldtype = "Check"
|
||||||
|
used_field.default = "0"
|
||||||
|
used_field.insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
# 4. Expires On 字段 (可选)
|
||||||
|
expires_on_field = frappe.new_doc("DocField")
|
||||||
|
expires_on_field.parent = "Signup Key"
|
||||||
|
expires_on_field.parenttype = "DocType"
|
||||||
|
expires_on_field.parentfield = "fields"
|
||||||
|
expires_on_field.fieldname = "expires_on"
|
||||||
|
expires_on_field.label = "Expires On"
|
||||||
|
expires_on_field.fieldtype = "Datetime"
|
||||||
|
expires_on_field.insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
# 可选:设置列表视图默认排序
|
||||||
|
make_property_setter("Signup Key", "list_view_fields", "value", "['key', 'email', 'used', 'expires_on']", "Text", for_doctype=True)
|
||||||
|
|
||||||
|
# 保存 DocType 以应用所有更改
|
||||||
|
signup_key_doctype.save(ignore_permissions=True)
|
||||||
|
|
||||||
|
print("Successfully created DocType 'Signup Key' with fields: key, email, used, expires_on.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
frappe.log_error(title="Failed to Create Signup Key DocType", message=frappe.get_traceback())
|
||||||
|
print(f"Error creating DocType 'Signup Key': {str(e)}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
frappe.connect()
|
||||||
|
create_signup_key_doctype()
|
||||||
|
frappe.db.commit()
|
||||||
|
frappe.destroy()
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<!-- www/sundererapp.html -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>决裂者终端-用户中心</title>
|
||||||
|
<!-- 如果你的 Vue 应用构建后生成了独立的 CSS 文件,取消下面这行的注释并调整文件名 -->
|
||||||
|
<!-- <link rel="stylesheet" href="/sundererapp.[hash].css"> -->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<!-- 引入你的 Vue 应用构建后的 JS 文件 -->
|
||||||
|
<!-- 路径必须与 vite.config.js 中 outDir 和 rollupOptions.output 的配置结果匹配 -->
|
||||||
|
<!-- 假设 vite.config.js 中 outDir: '../../public/' 且 input key 为 'sundererapp' -->
|
||||||
|
<!-- 构建后文件会出现在 sunderer_app/public/ 目录下,例如: sundererapp.a1b2c3d4.js -->
|
||||||
|
<!-- 所以这里的 src 应该是 /sundererapp.[hash].js -->
|
||||||
|
<!-- 注意:你需要将 [hash] 替换为实际构建后生成的哈希值 -->
|
||||||
|
<script src="/assets/sunderer_app/SundererPage/js/sundererapp.BRnFqPg_.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# templates/pages/sundererapp/sundererapp.py
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
|
||||||
|
def get_context(context):
|
||||||
|
"""
|
||||||
|
SPA 路由处理逻辑。
|
||||||
|
由于 hooks.py 将路由映射到这里,且模板文件名为 sundererapp.html,
|
||||||
|
Frappe 会自动查找 sundererapp/sundererapp.html 作为模板。
|
||||||
|
"""
|
||||||
|
pass # 保持简单,让 Frappe 自动渲染模板
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# sunderer_app/utils.py
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
def update_website_context(context):
|
||||||
|
context["brand_html"] = "Sunderer"
|
||||||
|
# 其他上下文覆盖
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<!-- sunderer_app/sunderer_app/www/IncreaseAcc/index.html -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>决裂者终端 - 完成账户注册</title>
|
||||||
|
<!-- 引用构建后生成的 CSS 文件 (如果存在) -->
|
||||||
|
<!-- <link rel="stylesheet" href="/assets/sunderer_app/css/increaseacc.css"> -->
|
||||||
|
<!-- 如果没有独立 CSS 文件,则保持注释 -->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
|
||||||
|
<!-- 引用构建后生成的 UMD 格式的 JS 文件 -->
|
||||||
|
<!-- 路径指向 public/js 目录 -->
|
||||||
|
<script src="/assets/sunderer_app/IncreaseAcc/increaseacc.js"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/assets/sunderer_app/dist/icon3.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>决裂者终端用户中心</title>
|
||||||
|
<script type="module" crossorigin src="/assets/sunderer_app/dist/js/index.c1R21Jxr.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/sunderer_app/dist/css/index.CJO3dQ9s.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<!-- www/sundererapp.html -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>决裂者终端-用户中心</title>
|
||||||
|
<!-- 如果你的 Vue 应用构建后生成了独立的 CSS 文件,取消下面这行的注释并调整文件名 -->
|
||||||
|
<!-- <link rel="stylesheet" href="/sundererapp.[hash].css"> -->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<!-- 引入你的 Vue 应用构建后的 JS 文件 -->
|
||||||
|
<!-- 路径必须与 vite.config.js 中 outDir 和 rollupOptions.output 的配置结果匹配 -->
|
||||||
|
<!-- 假设 vite.config.js 中 outDir: '../../public/' 且 input key 为 'sundererapp' -->
|
||||||
|
<!-- 构建后文件会出现在 sunderer_app/public/ 目录下,例如: sundererapp.a1b2c3d4.js -->
|
||||||
|
<!-- 所以这里的 src 应该是 /sundererapp.[hash].js -->
|
||||||
|
<!-- 注意:你需要将 [hash] 替换为实际构建后生成的哈希值 -->
|
||||||
|
<script src="/assets/sunderer_app/SundererPage/js/sundererapp.BRnFqPg_.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||