mirror of
https://github.com/XShengTech/MEGREZ.git
synced 2026-01-14 00:57:17 +08:00
[Init] 🎉 MEGREZ Community
This commit is contained in:
commit
f2966d0f5c
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
data
|
||||||
|
|
||||||
|
config.yml
|
||||||
|
docker-compose.yml
|
||||||
|
|
||||||
|
megrez
|
||||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
|
||||||
|
}
|
||||||
23
.vscode/settings.json
vendored
Normal file
23
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit",
|
||||||
|
"source.organizeImports": "explicit"
|
||||||
|
},
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "vscode.typescript-language-features",
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
"[vue]": {
|
||||||
|
"editor.defaultFormatter": "Vue.volar",
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
"[json]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
}
|
||||||
|
}
|
||||||
661
LICENSE
Normal file
661
LICENSE
Normal file
@ -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/>.
|
||||||
12
config.example.yml
Normal file
12
config.example.yml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
http:
|
||||||
|
host: 0.0.0.0
|
||||||
|
port: 34567
|
||||||
|
database:
|
||||||
|
host: gm-postgres
|
||||||
|
port: 5432
|
||||||
|
username: GpuManager
|
||||||
|
password: GpuManager
|
||||||
|
database: GpuManager
|
||||||
|
redis:
|
||||||
|
host: ms-redis
|
||||||
|
port: 6379
|
||||||
41
docker-compose.example.yml
Normal file
41
docker-compose.example.yml
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
version: "3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
megrez:
|
||||||
|
image: zklcdc/ubuntu-with-tzdata:latest
|
||||||
|
container_name: megrez
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
|
volumes:
|
||||||
|
- $PWD/megrez:/home/megrez
|
||||||
|
- $PWD/config.yaml:/home/config.yaml
|
||||||
|
depends_on:
|
||||||
|
- "megrez-redis"
|
||||||
|
- "megrez-postgres"
|
||||||
|
command: sh -c "cd /home/ && /home/megrez -config /home/config.yml"
|
||||||
|
ports:
|
||||||
|
- 34567:34567
|
||||||
|
|
||||||
|
megrez-postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: megrez-postgres
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- $PWD/data/postgres:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
|
- POSTGRES_DB=GpuManager
|
||||||
|
- POSTGRES_USER=GpuManager
|
||||||
|
- POSTGRES_PASSWORD=GpuManager
|
||||||
|
|
||||||
|
megrez-redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: megrez-redis
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- $PWD/data/redis:/data
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
name: megrez-network
|
||||||
13
frontend/.editorconfig
Normal file
13
frontend/.editorconfig
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Editor configuration, see https://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
max_line_length = off
|
||||||
|
trim_trailing_whitespace = false
|
||||||
23
frontend/.eslintrc.cjs
Normal file
23
frontend/.eslintrc.cjs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
require('@rushstack/eslint-patch/modern-module-resolution');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true
|
||||||
|
},
|
||||||
|
extends: ['plugin:vue/vue3-essential', 'eslint:recommended', '@vue/eslint-config-prettier'],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest'
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'vue/multi-word-component-names': 'off',
|
||||||
|
'vue/no-reserved-component-names': 'off',
|
||||||
|
'vue/component-tags-order': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
order: ['script', 'template', 'style']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
14
frontend/.gitignore
vendored
Normal file
14
frontend/.gitignore
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
node_modules
|
||||||
|
coverage
|
||||||
|
*.log*
|
||||||
|
.nuxt
|
||||||
|
.nitro
|
||||||
|
.cache
|
||||||
|
.output
|
||||||
|
.env
|
||||||
|
dist
|
||||||
|
.DS_Store
|
||||||
|
.idea
|
||||||
|
.eslintcache
|
||||||
|
api-generator/typedoc.json
|
||||||
|
**/.DS_Store
|
||||||
10
frontend/.prettierrc.json
Normal file
10
frontend/.prettierrc.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"useTabs": false,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"vueIndentScriptAndStyle": false,
|
||||||
|
"printWidth": 250,
|
||||||
|
"bracketSameLine": false
|
||||||
|
}
|
||||||
16
frontend/index.html
Normal file
16
frontend/index.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="/favicon.ico">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>MEGREZ 天权算能聚联计算平台</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
13
frontend/jsconfig.json
Normal file
13
frontend/jsconfig.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"dist"
|
||||||
|
]
|
||||||
|
}
|
||||||
5113
frontend/package-lock.json
generated
Normal file
5113
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
38
frontend/package.json
Normal file
38
frontend/package.json
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"name": "megrez-ui",
|
||||||
|
"version": "4.1.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"lint": "eslint --fix . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@microsoft/clarity": "^1.0.0",
|
||||||
|
"@primevue/themes": "^4.0.0",
|
||||||
|
"axios": "^1.7.7",
|
||||||
|
"chart.js": "3.3.2",
|
||||||
|
"pinia": "^2.2.4",
|
||||||
|
"pinia-plugin-persistedstate": "^4.1.1",
|
||||||
|
"primeicons": "^6.0.1",
|
||||||
|
"primevue": "^4.0.0",
|
||||||
|
"vue": "^3.4.34",
|
||||||
|
"vue-router": "^4.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@primevue/auto-import-resolver": "^4.0.1",
|
||||||
|
"@rushstack/eslint-patch": "^1.8.0",
|
||||||
|
"@vitejs/plugin-vue": "^5.0.5",
|
||||||
|
"@vue/eslint-config-prettier": "^9.0.0",
|
||||||
|
"autoprefixer": "^10.4.19",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"eslint-plugin-vue": "^9.23.0",
|
||||||
|
"postcss": "^8.4.40",
|
||||||
|
"prettier": "^3.2.5",
|
||||||
|
"sass": "^1.55.0",
|
||||||
|
"tailwindcss": "^3.4.6",
|
||||||
|
"tailwindcss-primeui": "^0.3.2",
|
||||||
|
"unplugin-vue-components": "^0.27.3",
|
||||||
|
"vite": "^5.3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
8
frontend/src/App.vue
Normal file
8
frontend/src/App.vue
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<script setup></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
<Toast />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
126
frontend/src/api.js
Normal file
126
frontend/src/api.js
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const ajax = (url, method, { params = {}, data = {} }) => {
|
||||||
|
axios.defaults.withCredentials = true
|
||||||
|
axios.defaults.crossDomain = true
|
||||||
|
axios.defaults.baseURL = '/api/v1'
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
axios({
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
params,
|
||||||
|
data
|
||||||
|
}).then(res => {
|
||||||
|
if (res.data.code != 200) {
|
||||||
|
reject(res)
|
||||||
|
} else {
|
||||||
|
resolve(res)
|
||||||
|
}
|
||||||
|
}, res => {
|
||||||
|
reject(res)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
GetUserProfile() {
|
||||||
|
return ajax('user/profile', 'get', {})
|
||||||
|
},
|
||||||
|
UserLogin(data) {
|
||||||
|
return ajax('user/login', 'post', { data })
|
||||||
|
},
|
||||||
|
UserLogout() {
|
||||||
|
return ajax('user/logout', 'get', {})
|
||||||
|
},
|
||||||
|
UserRegister(data) {
|
||||||
|
return ajax('user/register', 'post', { data })
|
||||||
|
},
|
||||||
|
|
||||||
|
UserInstancesList(params) {
|
||||||
|
return ajax('user/instances', 'get', { params })
|
||||||
|
},
|
||||||
|
UserInstancesDetail(id) {
|
||||||
|
return ajax(`user/instances/${id}`, 'get', {})
|
||||||
|
},
|
||||||
|
UserInstancesModify(id, data) {
|
||||||
|
return ajax(`user/instances/${id}`, 'post', { data })
|
||||||
|
},
|
||||||
|
UserInstancesModifyLabel(id, data) {
|
||||||
|
return ajax(`user/instances/${id}/label`, 'post', { data })
|
||||||
|
},
|
||||||
|
UserInstancesAction(id, data) {
|
||||||
|
return ajax(`user/instances/${id}/`, 'put', { data })
|
||||||
|
},
|
||||||
|
UserInstancesCreate(data) {
|
||||||
|
return ajax('user/instances', 'post', { data })
|
||||||
|
},
|
||||||
|
UserInstancesDelete(id) {
|
||||||
|
return ajax(`user/instances/${id}`, 'delete', {})
|
||||||
|
},
|
||||||
|
|
||||||
|
UserServerList(params) {
|
||||||
|
return ajax('user/servers', 'get', { params })
|
||||||
|
},
|
||||||
|
UserServerDetail(id) {
|
||||||
|
return ajax(`user/servers/${id}`, 'get', {})
|
||||||
|
},
|
||||||
|
|
||||||
|
UserImages() {
|
||||||
|
return ajax('user/images', 'get', {})
|
||||||
|
},
|
||||||
|
|
||||||
|
AdminInstancesList(params) {
|
||||||
|
return ajax('admin/instances', 'get', { params })
|
||||||
|
},
|
||||||
|
AdminInstancesDetail(id) {
|
||||||
|
return ajax(`admin/instances/${id}`, 'get', {})
|
||||||
|
},
|
||||||
|
AdminInstancesModify(id, data) {
|
||||||
|
return ajax(`admin/instances/${id}`, 'post', { data })
|
||||||
|
},
|
||||||
|
AdminInstancesModifyLabel(id, data) {
|
||||||
|
return ajax(`admin/instances/${id}/label`, 'post', { data })
|
||||||
|
},
|
||||||
|
AdminInstancesAction(id, data) {
|
||||||
|
return ajax(`admin/instances/${id}/`, 'put', { data })
|
||||||
|
},
|
||||||
|
AdminInstancesCreate(data) {
|
||||||
|
return ajax('admin/instances', 'post', { data })
|
||||||
|
},
|
||||||
|
AdminInstancesDelete(id) {
|
||||||
|
return ajax(`admin/instances/${id}`, 'delete', {})
|
||||||
|
},
|
||||||
|
|
||||||
|
AdminUserList(params) {
|
||||||
|
return ajax('admin/users', 'get', { params })
|
||||||
|
},
|
||||||
|
AdminUserModify(id, data) {
|
||||||
|
return ajax(`admin/users/${id}`, 'post', { data })
|
||||||
|
},
|
||||||
|
AdminUserDelete(id) {
|
||||||
|
return ajax(`admin/users/${id}`, 'delete', {})
|
||||||
|
},
|
||||||
|
|
||||||
|
AdminServersList(params) {
|
||||||
|
return ajax('admin/servers', 'get', { params })
|
||||||
|
},
|
||||||
|
AdminServersDetail(id) {
|
||||||
|
return ajax(`admin/servers/${id}`, 'get', {})
|
||||||
|
},
|
||||||
|
AdminServersAdd(data) {
|
||||||
|
return ajax('admin/servers', 'post', { data })
|
||||||
|
},
|
||||||
|
AdminServersModify(id, data) {
|
||||||
|
return ajax(`admin/servers/${id}`, 'post', { data })
|
||||||
|
},
|
||||||
|
AdminServersDelete(id) {
|
||||||
|
return ajax(`admin/servers/${id}`, 'delete', {})
|
||||||
|
},
|
||||||
|
|
||||||
|
AdminImagesList() {
|
||||||
|
return ajax('admin/images', 'get', {})
|
||||||
|
},
|
||||||
|
AdminImagesModify(data) {
|
||||||
|
return ajax('admin/images', 'post', { data })
|
||||||
|
},
|
||||||
|
}
|
||||||
23
frontend/src/assets/layout/_core.scss
Normal file
23
frontend/src/assets/layout/_core.scss
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Lato', sans-serif;
|
||||||
|
color: var(--text-color);
|
||||||
|
// background-color: var(--surface-ground);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
min-height: 100%;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-wrapper {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
8
frontend/src/assets/layout/_footer.scss
Normal file
8
frontend/src/assets/layout/_footer.scss
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.layout-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem 0 1rem 0;
|
||||||
|
gap: 0.5rem;
|
||||||
|
border-top: 1px solid var(--surface-border);
|
||||||
|
}
|
||||||
16
frontend/src/assets/layout/_main.scss
Normal file
16
frontend/src/assets/layout/_main.scss
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
.layout-main-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 2rem 2rem 0 2rem;
|
||||||
|
margin-top: 4rem;
|
||||||
|
transition: margin-left var(--layout-section-transition-duration);
|
||||||
|
border-radius: 1rem;
|
||||||
|
background-color: var(--surface-ground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-main {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
}
|
||||||
160
frontend/src/assets/layout/_menu.scss
Normal file
160
frontend/src/assets/layout/_menu.scss
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
.layout-sidebar {
|
||||||
|
position: fixed;
|
||||||
|
width: 20rem;
|
||||||
|
// height: calc(100vh - 8rem);
|
||||||
|
height: calc(100vh - 4rem);
|
||||||
|
z-index: 999;
|
||||||
|
overflow-y: auto;
|
||||||
|
user-select: none;
|
||||||
|
// top: 6rem;
|
||||||
|
top: 4rem;
|
||||||
|
// left: 2rem;
|
||||||
|
transition:
|
||||||
|
transform var(--layout-section-transition-duration),
|
||||||
|
left var(--layout-section-transition-duration);
|
||||||
|
background-color: var(--surface-overlay);
|
||||||
|
// border-radius: var(--content-border-radius);
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-menu {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style-type: none;
|
||||||
|
|
||||||
|
.layout-root-menuitem {
|
||||||
|
> .layout-menuitem-root-text {
|
||||||
|
font-size: 0.857rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-color);
|
||||||
|
margin: 0.75rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> a {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&.active-menuitem {
|
||||||
|
> .layout-submenu-toggler {
|
||||||
|
transform: rotate(-180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
li.active-menuitem {
|
||||||
|
> a {
|
||||||
|
.layout-submenu-toggler {
|
||||||
|
transform: rotate(-180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style-type: none;
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
outline: 0 none;
|
||||||
|
color: var(--text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: var(--content-border-radius);
|
||||||
|
transition:
|
||||||
|
background-color var(--element-transition-duration),
|
||||||
|
box-shadow var(--element-transition-duration);
|
||||||
|
|
||||||
|
.layout-menuitem-icon {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-submenu-toggler {
|
||||||
|
font-size: 75%;
|
||||||
|
margin-left: auto;
|
||||||
|
transition: transform var(--element-transition-duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active-route {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
@include focused-inset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--content-border-radius);
|
||||||
|
|
||||||
|
li {
|
||||||
|
a {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
a {
|
||||||
|
margin-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
a {
|
||||||
|
margin-left: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
a {
|
||||||
|
margin-left: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
a {
|
||||||
|
margin-left: 3.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
a {
|
||||||
|
margin-left: 4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-submenu-enter-from,
|
||||||
|
.layout-submenu-leave-to {
|
||||||
|
max-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-submenu-enter-to,
|
||||||
|
.layout-submenu-leave-from {
|
||||||
|
max-height: 1000px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-submenu-leave-active {
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.45s cubic-bezier(0, 1, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-submenu-enter-active {
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 1s ease-in-out;
|
||||||
|
}
|
||||||
15
frontend/src/assets/layout/_mixins.scss
Normal file
15
frontend/src/assets/layout/_mixins.scss
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
@mixin focused() {
|
||||||
|
outline-width: var(--focus-ring-width);
|
||||||
|
outline-style: var(--focus-ring-style);
|
||||||
|
outline-color: var(--focus-ring-color);
|
||||||
|
outline-offset: var(--focus-ring-offset);
|
||||||
|
box-shadow: var(--focus-ring-shadow);
|
||||||
|
transition:
|
||||||
|
box-shadow var(--transition-duration),
|
||||||
|
outline-color var(--transition-duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin focused-inset() {
|
||||||
|
outline-offset: -1px;
|
||||||
|
box-shadow: inset var(--focus-ring-shadow);
|
||||||
|
}
|
||||||
47
frontend/src/assets/layout/_preloading.scss
Normal file
47
frontend/src/assets/layout/_preloading.scss
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
.preloader {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 999999;
|
||||||
|
background: #edf1f5;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.preloader-content {
|
||||||
|
border: 0 solid transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
position: absolute;
|
||||||
|
top: calc(50vh - 75px);
|
||||||
|
left: calc(50vw - 75px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preloader-content:before, .preloader-content:after{
|
||||||
|
content: '';
|
||||||
|
border: 1em solid var(--primary-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
width: inherit;
|
||||||
|
height: inherit;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
animation: loader 2s linear infinite;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preloader-content:before{
|
||||||
|
animation-delay: 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loader{
|
||||||
|
0%{
|
||||||
|
transform: scale(0);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
50%{
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100%{
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
110
frontend/src/assets/layout/_responsive.scss
Normal file
110
frontend/src/assets/layout/_responsive.scss
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
@media screen and (min-width: 1960px) {
|
||||||
|
.layout-main,
|
||||||
|
.landing-wrapper {
|
||||||
|
width: 1504px;
|
||||||
|
margin-left: auto !important;
|
||||||
|
margin-right: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.layout-wrapper {
|
||||||
|
&.layout-overlay {
|
||||||
|
.layout-main-container {
|
||||||
|
margin-left: 0;
|
||||||
|
padding-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-sidebar {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100vh;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-right: 1px solid var(--surface-border);
|
||||||
|
transition:
|
||||||
|
transform 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99),
|
||||||
|
left 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99);
|
||||||
|
box-shadow:
|
||||||
|
0px 3px 5px rgba(0, 0, 0, 0.02),
|
||||||
|
0px 0px 2px rgba(0, 0, 0, 0.05),
|
||||||
|
0px 1px 4px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.layout-overlay-active {
|
||||||
|
.layout-sidebar {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.layout-static {
|
||||||
|
.layout-main-container {
|
||||||
|
margin-left: 20rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.layout-static-inactive {
|
||||||
|
.layout-sidebar {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-main-container {
|
||||||
|
margin-left: 0;
|
||||||
|
padding-left: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-mask {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 991px) {
|
||||||
|
.blocked-scroll {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-wrapper {
|
||||||
|
.layout-main-container {
|
||||||
|
margin-left: 0;
|
||||||
|
padding-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-sidebar {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100vh;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
transition:
|
||||||
|
transform 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99),
|
||||||
|
left 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-mask {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 998;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--maskbg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.layout-mobile-active {
|
||||||
|
.layout-sidebar {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-mask {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
204
frontend/src/assets/layout/_topbar.scss
Normal file
204
frontend/src/assets/layout/_topbar.scss
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
.layout-topbar {
|
||||||
|
position: fixed;
|
||||||
|
height: 4rem;
|
||||||
|
z-index: 997;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 2rem;
|
||||||
|
background-color: var(--surface-card);
|
||||||
|
transition: left var(--layout-section-transition-duration);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.layout-topbar-logo-container {
|
||||||
|
width: 28rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-topbar-logo {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
border-radius: var(--content-border-radius);
|
||||||
|
color: var(--text-color);
|
||||||
|
font-weight: 500;
|
||||||
|
gap: 0.8rem;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
@include focused();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-topbar-action {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
transition: background-color var(--element-transition-duration);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
@include focused();
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 1rem;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.layout-topbar-action-highlight {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: var(--primary-contrast-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-menu-button {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-topbar-menu-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-topbar-actions {
|
||||||
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-topbar-menu-content {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-config-menu {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 991px) {
|
||||||
|
.layout-topbar {
|
||||||
|
padding: 0 2rem;
|
||||||
|
|
||||||
|
.layout-topbar-logo-container {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-menu-button {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-topbar-menu-button {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-topbar-menu {
|
||||||
|
position: absolute;
|
||||||
|
background-color: var(--surface-overlay);
|
||||||
|
transform-origin: top;
|
||||||
|
box-shadow:
|
||||||
|
0px 3px 5px rgba(0, 0, 0, 0.02),
|
||||||
|
0px 0px 2px rgba(0, 0, 0, 0.05),
|
||||||
|
0px 1px 4px rgba(0, 0, 0, 0.08);
|
||||||
|
border-radius: var(--content-border-radius);
|
||||||
|
padding: 1rem;
|
||||||
|
right: 2rem;
|
||||||
|
top: 4rem;
|
||||||
|
min-width: 15rem;
|
||||||
|
border: 1px solid var(--surface-border);
|
||||||
|
|
||||||
|
.layout-topbar-menu-content {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-topbar-action {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
justify-content: flex-start;
|
||||||
|
border-radius: var(--content-border-radius);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-weight: medium;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-topbar-menu-content {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-panel {
|
||||||
|
.config-panel-label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-secondary-color);
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-panel-colors {
|
||||||
|
> div {
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
button {
|
||||||
|
border: none;
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
outline-color: transparent;
|
||||||
|
outline-width: 2px;
|
||||||
|
outline-style: solid;
|
||||||
|
outline-offset: 1px;
|
||||||
|
|
||||||
|
&.active-color {
|
||||||
|
outline-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-panel-settings {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
68
frontend/src/assets/layout/_typography.scss
Normal file
68
frontend/src/assets/layout/_typography.scss
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
margin: 1.5rem 0 1rem 0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--text-color);
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
mark {
|
||||||
|
background: #fff8e1;
|
||||||
|
padding: 0.25rem 0.4rem;
|
||||||
|
border-radius: var(--content-border-radius);
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
margin: 1rem 0;
|
||||||
|
padding: 0 2rem;
|
||||||
|
border-left: 4px solid #90a4ae;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border-top: solid var(--surface-border);
|
||||||
|
border-width: 1px 0 0 0;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
frontend/src/assets/layout/_utils.scss
Normal file
25
frontend/src/assets/layout/_utils.scss
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/* Utils */
|
||||||
|
.clearfix:after {
|
||||||
|
content: ' ';
|
||||||
|
display: block;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--surface-card);
|
||||||
|
padding: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
border-radius: var(--content-border-radius);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-toast {
|
||||||
|
&.p-toast-top-right,
|
||||||
|
&.p-toast-top-left,
|
||||||
|
&.p-toast-top-center {
|
||||||
|
top: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
frontend/src/assets/layout/layout.scss
Normal file
13
frontend/src/assets/layout/layout.scss
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
@import './variables/_common';
|
||||||
|
@import './variables/_light';
|
||||||
|
@import './variables/_dark';
|
||||||
|
@import './_mixins';
|
||||||
|
@import './_preloading';
|
||||||
|
@import './_core';
|
||||||
|
@import './_main';
|
||||||
|
@import './_topbar';
|
||||||
|
@import './_menu';
|
||||||
|
@import './_footer';
|
||||||
|
@import './_responsive';
|
||||||
|
@import './_utils';
|
||||||
|
@import './_typography';
|
||||||
20
frontend/src/assets/layout/variables/_common.scss
Normal file
20
frontend/src/assets/layout/variables/_common.scss
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
:root {
|
||||||
|
--primary-color: var(--p-primary-color);
|
||||||
|
--primary-contrast-color: var(--p-primary-contrast-color);
|
||||||
|
--text-color: var(--p-text-color);
|
||||||
|
--text-color-secondary: var(--p-text-muted-color);
|
||||||
|
--surface-border: var(--p-content-border-color);
|
||||||
|
--surface-card: var(--p-content-background);
|
||||||
|
--surface-hover: var(--p-content-hover-background);
|
||||||
|
--surface-overlay: var(--p-overlay-popover-background);
|
||||||
|
--transition-duration: var(--p-transition-duration);
|
||||||
|
--maskbg: var(--p-mask-background);
|
||||||
|
--content-border-radius: var(--p-content-border-radius);
|
||||||
|
--layout-section-transition-duration: 0.2s;
|
||||||
|
--element-transition-duration: var(--p-transition-duration);
|
||||||
|
--focus-ring-width: var(--p-focus-ring-width);
|
||||||
|
--focus-ring-style: var(--p-focus-ring-style);
|
||||||
|
--focus-ring-color: var(--p-focus-ring-color);
|
||||||
|
--focus-ring-offset: var(--p-focus-ring-offset);
|
||||||
|
--focus-ring-shadow: var(--p-focus-ring-shadow);
|
||||||
|
}
|
||||||
5
frontend/src/assets/layout/variables/_dark.scss
Normal file
5
frontend/src/assets/layout/variables/_dark.scss
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
:root[class*='app-dark'] {
|
||||||
|
--surface-ground: var(--p-surface-950);
|
||||||
|
--code-background: var(--p-surface-800);
|
||||||
|
--code-color: var(--p-surface-100);
|
||||||
|
}
|
||||||
5
frontend/src/assets/layout/variables/_light.scss
Normal file
5
frontend/src/assets/layout/variables/_light.scss
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
:root {
|
||||||
|
--surface-ground: var(--p-surface-100);
|
||||||
|
--code-background: var(--p-surface-900);
|
||||||
|
--code-color: var(--p-surface-200);
|
||||||
|
}
|
||||||
BIN
frontend/src/assets/logo-text.webp
Normal file
BIN
frontend/src/assets/logo-text.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
18
frontend/src/assets/logo.svg
Executable file
18
frontend/src/assets/logo.svg
Executable file
@ -0,0 +1,18 @@
|
|||||||
|
<svg width="1994" height="1994" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g>
|
||||||
|
<title>Layer 1</title>
|
||||||
|
<g>
|
||||||
|
|
||||||
|
<g data-name="图层 20" id="svg_51">
|
||||||
|
<circle fill="#fdba74" r="161.38" cy="161.38001" cx="161.38" class="cls-2"/>
|
||||||
|
<circle fill="#fdba74" r="67.24" cy="1831.64001" cx="161.38" class="cls-2"/>
|
||||||
|
<path fill="#2563eb" d="m275.69,614.58001c-26,0 -47.07,21.07 -47.07,47.07l0,946.75c0,37.14 -30.1,67.24 -67.24,67.24s-67.24,-30.1 -67.24,-67.24l0,-1178.06l-0.07,0c-0.72,-25.37 -21.46,-45.72 -47,-45.72s-46.28,20.36 -47,45.72l-0.02,0c0,0.19 0,0.38 -0.01,0.56c0,0.26 -0.04,0.52 -0.04,0.78c0,0.12 0.02,0.24 0.02,0.36c0,0.33 -0.02,0.65 -0.02,0.98l0,1398.61c0,89.13 72.25,161.38 161.38,161.38s161.38,-72.25 161.38,-161.38l0,-1169.98c0,-26 -21.07,-47.07 -47.07,-47.07zm-114.31,1284.3c-37.14,0 -67.24,-30.1 -67.24,-67.24s30.1,-67.24 67.24,-67.24s67.24,30.1 67.24,67.24s-30.1,67.24 -67.24,67.24z" class="cls-1"/>
|
||||||
|
<path fill="#fdba74" d="m1082.31,1286.21001c-29.26,22.86 -71.52,17.67 -94.38,-11.59c-22.86,-29.26 -17.67,-71.52 11.59,-94.38s71.52,-17.67 94.38,11.59s17.67,71.52 -11.59,94.38z" class="cls-2"/>
|
||||||
|
<path d="m1879.19,1504.37001c12.16,12.16 19.69,28.99 19.69,47.55l0,61.86c0,37.14 -30.1,67.24 -67.24,67.24s-67.24,-30.1 -67.24,-67.24l0,-61.86c0,-37.14 30.1,-67.24 67.24,-67.24c18.56,0 35.4,7.53 47.55,19.69z"/>
|
||||||
|
<path fill="#2563eb" d="m1945.76,95.67001c-11.86,-11.86 -25.58,-21.87 -40.64,-29.59l-3.52,-1.75c-1.16,-0.56 -2.34,-1.13 -3.55,-1.67c-66.38,-30.39 -147.31,-12.59 -194.22,47.45l-667.81,854.74l-580.39,-742.87l-0.24,0.19c-8.63,-10.81 -21.89,-17.75 -36.82,-17.75c-25.98,0 -47.07,21.09 -47.07,47.07c0,10.79 3.68,20.66 9.76,28.59l-0.16,0.13l582.9,746.08c4.36,5.57 7.69,11.62 10.03,17.91c9.98,26.73 2.07,57.96 -21.62,76.47c-12.29,9.6 -26.87,14.26 -41.34,14.26c-2.45,0 -4.87,-0.13 -7.29,-0.38c-17.43,-1.91 -34.1,-10.57 -45.75,-25.47l-542.66,-694.57c-8.36,-13.1 -23,-21.81 -39.67,-21.81c-26.01,0 -47.07,21.09 -47.07,47.07c0,12.18 4.68,23.27 12.29,31.63l667.86,854.85c41.42,53.04 109.41,73.08 170.47,56.11c0.3,-0.11 0.62,-0.19 0.94,-0.27c1.29,-0.38 2.56,-0.75 3.85,-1.13c1.67,-0.54 3.34,-1.08 4.98,-1.64c1.43,-0.51 2.82,-1.02 4.25,-1.53c1.51,-0.56 3.01,-1.16 4.49,-1.8c1.1,-0.43 2.18,-0.89 3.25,-1.37c0.78,-0.32 1.56,-0.67 2.34,-1.05c0.97,-0.43 1.91,-0.89 2.85,-1.34c1.02,-0.48 2.04,-0.97 3.04,-1.51c20.47,-10.36 39.03,-25.28 54.09,-44.54l506.94,-648.85l105.54,-135.07c0.03,-0.05 0.05,-0.08 0.08,-0.13c1.8,-2.66 3.79,-5.19 5.94,-7.56c0,-0.03 0.03,-0.03 0.03,-0.05c12.32,-13.53 30.04,-22.03 49.79,-22.03c18.56,0 35.4,7.53 47.55,19.69c12.16,12.18 19.69,28.97 19.69,47.55l0,754.36c0,37.14 -30.1,67.24 -67.24,67.24s-67.24,-30.1 -67.24,-67.24l0,-560.79c0,-25.98 -21.09,-47.07 -47.07,-47.07s-47.07,21.09 -47.07,47.07l0,835.13c0,89.13 72.24,161.38 161.38,161.38s161.38,-72.24 161.38,-161.38l0,-1398.64c0,-44.57 -18.07,-84.91 -47.26,-114.12l-0.01,0zm-863.45,1190.54c-29.26,22.86 -71.52,17.67 -94.38,-11.59c-22.86,-29.26 -17.67,-71.52 11.59,-94.38s71.52,-17.67 94.38,11.59s17.67,71.52 -11.59,94.38zm816.57,327.57c0,37.14 -30.1,67.24 -67.24,67.24s-67.24,-30.1 -67.24,-67.24l0,-61.86c0,-37.14 30.1,-67.24 67.24,-67.24c18.56,0 35.4,7.53 47.55,19.69c12.16,12.16 19.69,28.99 19.69,47.55l0,61.86zm-315.49,-974.53l-351.32,449.65c-22.86,29.26 -65.12,34.45 -94.38,11.59c-29.26,-22.86 -34.45,-65.12 -11.59,-94.38l351.29,-449.65c22.89,-29.26 65.14,-34.45 94.41,-11.59c16.97,13.26 25.85,33.06 25.85,53.04c0,14.47 -4.65,29.05 -14.26,41.34zm304.31,-390.7l-179.56,229.8c-9.87,12.64 -23.37,20.79 -37.87,24.13c-19.07,4.38 -39.89,0.46 -56.51,-12.53c-29.26,-22.86 -34.45,-65.12 -11.59,-94.38l68.1,-87.14l111.46,-142.66c22.86,-29.26 65.12,-34.45 94.38,-11.59c16.97,13.26 25.85,33.06 25.85,53.04c0,14.47 -4.65,29.05 -14.26,41.34l0,-0.01z" class="cls-1"/>
|
||||||
|
<path fill="#fdba74" d="m1898.88,1551.92001l0,61.86c0,37.14 -30.1,67.24 -67.24,67.24s-67.24,-30.1 -67.24,-67.24l0,-61.86c0,-37.14 30.1,-67.24 67.24,-67.24c18.56,0 35.4,7.53 47.55,19.69c12.16,12.16 19.69,28.99 19.69,47.55z" class="cls-2"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.9 KiB |
4
frontend/src/assets/styles.scss
Normal file
4
frontend/src/assets/styles.scss
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/* You can add global styles to this file, and also import other style files */
|
||||||
|
@import '@/assets/layout/layout.scss';
|
||||||
|
@import 'primeicons/primeicons.css';
|
||||||
|
|
||||||
3
frontend/src/assets/tailwind.css
Normal file
3
frontend/src/assets/tailwind.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
50
frontend/src/components/CopyIcon.vue
Normal file
50
frontend/src/components/CopyIcon.vue
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<i v-if="!clickStatus" class="pi pi-copy" @click="copyToClipboard(text)" />
|
||||||
|
<i v-else class="pi pi-check text-green-500" @click="copyToClipboard(text)" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const clickStatus = ref(false)
|
||||||
|
const timeoutId = ref(null)
|
||||||
|
|
||||||
|
const spanClass = computed(() => `inline-flex justify-center items-center ${props.w} ${props.h}`)
|
||||||
|
const spanClassCheck = computed(() => `inline-flex justify-center items-center ${props.w} ${props.h} text-green-500`)
|
||||||
|
|
||||||
|
const iconSize = computed(() => props.size ?? 16)
|
||||||
|
|
||||||
|
const copyToClipboard = (text) => {
|
||||||
|
clearTimeout(timeoutId.value)
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
console.log('Text copied to clipboard')
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Failed to copy text: ', err)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const textarea = document.createElement('textarea')
|
||||||
|
textarea.value = text
|
||||||
|
document.body.appendChild(textarea)
|
||||||
|
textarea.select();
|
||||||
|
try {
|
||||||
|
document.execCommand('copy')
|
||||||
|
console.log('Text copied to clipboard')
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy text: ', err)
|
||||||
|
}
|
||||||
|
document.body.removeChild(textarea)
|
||||||
|
}
|
||||||
|
clickStatus.value = true
|
||||||
|
timeoutId.vaue = setTimeout(() => {
|
||||||
|
clickStatus.value = false
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
33
frontend/src/components/EditLabel.vue
Normal file
33
frontend/src/components/EditLabel.vue
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="!editStatus" class="text-sm text-muted-color">
|
||||||
|
<span>{{ text || props.emptyText }}</span>
|
||||||
|
<i class="ml-1 pi pi-pencil" style="font-size: 0.75rem" @click="editStatus = true"
|
||||||
|
v-tooltip.top="props.emptyText"></i>
|
||||||
|
</div>
|
||||||
|
<InputText v-else v-model="text" type="text" size="small" @blur="saveLabel" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const editStatus = ref(false)
|
||||||
|
|
||||||
|
const text = defineModel()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
emptyText: {
|
||||||
|
type: String,
|
||||||
|
default: 'No label'
|
||||||
|
},
|
||||||
|
|
||||||
|
save: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const saveLabel = () => {
|
||||||
|
props.save(props.modelValue)
|
||||||
|
editStatus.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
12
frontend/src/components/FloatingConfigurator.vue
Normal file
12
frontend/src/components/FloatingConfigurator.vue
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useLayout } from '@/layout/composables/layout';
|
||||||
|
|
||||||
|
const { toggleDarkMode, isDarkTheme } = useLayout();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="fixed flex gap-4 top-8 right-8">
|
||||||
|
<Button type="button" @click="toggleDarkMode" rounded :icon="isDarkTheme ? 'pi pi-moon' : 'pi pi-sun'"
|
||||||
|
severity="secondary" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
28
frontend/src/components/SectionBanner.vue
Normal file
28
frontend/src/components/SectionBanner.vue
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<template>
|
||||||
|
<section class="mb-6 flex items-center justify-between">
|
||||||
|
<div class="flex items-center justify-start">
|
||||||
|
<Avatar :icon="icon" class="mr-2" size="large" shape="circle" style="background-color: #fff;" />
|
||||||
|
<h1 class="text-3xl leading-snug font-bold -mt-[0.25rem] ml-1">
|
||||||
|
{{ label }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div class="space-x-4">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
10
frontend/src/layout/AppFooter.vue
Normal file
10
frontend/src/layout/AppFooter.vue
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<script setup></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="layout-footer">
|
||||||
|
© 2024-Precent 晓声智能科技 |
|
||||||
|
<a href="https://www.xsheng-ai.com" target="_blank" rel="noopener noreferrer"
|
||||||
|
class="text-primary font-bold hover:underline">MEGREZ 天权算能聚联计算平台</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
87
frontend/src/layout/AppLayout.vue
Normal file
87
frontend/src/layout/AppLayout.vue
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useLayout } from '@/layout/composables/layout';
|
||||||
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
|
import AppFooter from './AppFooter.vue';
|
||||||
|
import AppSidebar from './AppSidebar.vue';
|
||||||
|
import AppTopbar from './AppTopbar.vue';
|
||||||
|
|
||||||
|
import api from '@/api';
|
||||||
|
import { useProfileStore } from '@/stores/profile';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const profileStore = useProfileStore()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const { layoutConfig, layoutState, isSidebarActive, resetMenu } = useLayout();
|
||||||
|
|
||||||
|
const outsideClickListener = ref(null);
|
||||||
|
|
||||||
|
watch(isSidebarActive, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
bindOutsideClickListener();
|
||||||
|
} else {
|
||||||
|
unbindOutsideClickListener();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const containerClass = computed(() => {
|
||||||
|
return {
|
||||||
|
'layout-overlay': layoutConfig.menuMode === 'overlay',
|
||||||
|
'layout-static': layoutConfig.menuMode === 'static',
|
||||||
|
'layout-static-inactive': layoutState.staticMenuDesktopInactive && layoutConfig.menuMode === 'static',
|
||||||
|
'layout-overlay-active': layoutState.overlayMenuActive,
|
||||||
|
'layout-mobile-active': layoutState.staticMenuMobileActive
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function bindOutsideClickListener() {
|
||||||
|
if (!outsideClickListener.value) {
|
||||||
|
outsideClickListener.value = (event) => {
|
||||||
|
if (isOutsideClicked(event)) {
|
||||||
|
resetMenu();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('click', outsideClickListener.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unbindOutsideClickListener() {
|
||||||
|
if (outsideClickListener.value) {
|
||||||
|
document.removeEventListener('click', outsideClickListener);
|
||||||
|
outsideClickListener.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOutsideClicked(event) {
|
||||||
|
const sidebarEl = document.querySelector('.layout-sidebar');
|
||||||
|
const topbarEl = document.querySelector('.layout-menu-button');
|
||||||
|
|
||||||
|
return !(sidebarEl.isSameNode(event.target) || sidebarEl.contains(event.target) || topbarEl.isSameNode(event.target) || topbarEl.contains(event.target));
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
api.GetUserProfile().then(res => {
|
||||||
|
profileStore.setUserProfile(res.data.data.result)
|
||||||
|
}).catch(_ => {
|
||||||
|
toast.add({ severity: 'error', summary: '登录过期,请重新登录', life: 3000 })
|
||||||
|
profileStore.clearUserProfile()
|
||||||
|
router.push('/login')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="layout-wrapper" :class="containerClass">
|
||||||
|
<app-topbar></app-topbar>
|
||||||
|
<app-sidebar></app-sidebar>
|
||||||
|
<div class="layout-main-container">
|
||||||
|
<div class="layout-main">
|
||||||
|
<router-view></router-view>
|
||||||
|
</div>
|
||||||
|
<app-footer></app-footer>
|
||||||
|
</div>
|
||||||
|
<div class="layout-mask animate-fadein"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
134
frontend/src/layout/AppMenu.vue
Normal file
134
frontend/src/layout/AppMenu.vue
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useProfileStore } from '@/stores/profile';
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import AppMenuItem from './AppMenuItem.vue';
|
||||||
|
|
||||||
|
const profileStore = useProfileStore();
|
||||||
|
|
||||||
|
const model = ref([
|
||||||
|
{
|
||||||
|
label: '实例管理',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: '实例列表',
|
||||||
|
icon: 'pi pi-fw pi-desktop text-blue-600',
|
||||||
|
to: '/instances'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '创建实例',
|
||||||
|
icon: 'pi pi-fw pi-plus-circle text-purple-600',
|
||||||
|
to: '/instances/create'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '费用管理',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: '历史订单',
|
||||||
|
icon: 'pi pi-fw pi-ticket text-emerald-500',
|
||||||
|
// disabled: true,
|
||||||
|
// to: '/landing'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '设置',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: '个人信息',
|
||||||
|
icon: 'pi pi-fw pi-user text-cyan-500',
|
||||||
|
// to: '/settings'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '安全设置',
|
||||||
|
icon: 'pi pi-fw pi-cog text-emerald-500',
|
||||||
|
to: '/settings',
|
||||||
|
// disabled: true,
|
||||||
|
// to: '/landing'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '相关信息',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: '使用文档',
|
||||||
|
icon: 'pi pi-fw pi-book text-amber-500',
|
||||||
|
url: '#',
|
||||||
|
target: '_blank'
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// label: '开源信息',
|
||||||
|
// icon: 'pi pi-fw pi-cog',
|
||||||
|
// url: 'https://github.com/primefaces/sakai-vue',
|
||||||
|
// target: '_blank'
|
||||||
|
// },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const adminModel = ref({
|
||||||
|
label: '系统设置',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: '实例管理',
|
||||||
|
icon: 'pi pi-fw pi-desktop text-lime-500',
|
||||||
|
to: '/admin/instances'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '用户管理',
|
||||||
|
icon: 'pi pi-fw pi-users text-indigo-500',
|
||||||
|
to: '/admin/users'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const superAdminModel = ref({
|
||||||
|
label: '系统设置',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: '节点管理',
|
||||||
|
icon: 'pi pi-fw pi-server text-yellow-400',
|
||||||
|
to: '/admin/servers'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '实例管理',
|
||||||
|
icon: 'pi pi-fw pi-desktop text-lime-500',
|
||||||
|
to: '/admin/instances'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '镜像管理',
|
||||||
|
icon: 'pi pi-fw pi-images text-teal-500',
|
||||||
|
to: '/admin/images'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '用户管理',
|
||||||
|
icon: 'pi pi-fw pi-users text-indigo-500',
|
||||||
|
to: '/admin/users'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const superAdmin = profileStore.isSuperAdmin
|
||||||
|
const admin = profileStore.isAdmin
|
||||||
|
if (superAdmin) {
|
||||||
|
model.value.push(superAdminModel.value)
|
||||||
|
} else if (admin) {
|
||||||
|
model.value.push(adminModel.value)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ul class="layout-menu">
|
||||||
|
<template v-for="(item, i) in model" :key="item">
|
||||||
|
<app-menu-item v-if="!item.separator" :item="item" :index="i"></app-menu-item>
|
||||||
|
<li v-if="item.separator" class="menu-separator"></li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
95
frontend/src/layout/AppMenuItem.vue
Normal file
95
frontend/src/layout/AppMenuItem.vue
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useLayout } from '@/layout/composables/layout';
|
||||||
|
import { onBeforeMount, ref, watch } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const { layoutState, setActiveMenuItem, onMenuToggle } = useLayout();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
index: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
root: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
parentItemKey: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const isActiveMenu = ref(false);
|
||||||
|
const itemKey = ref(null);
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
itemKey.value = props.parentItemKey ? props.parentItemKey + '-' + props.index : String(props.index);
|
||||||
|
|
||||||
|
const activeItem = layoutState.activeMenuItem;
|
||||||
|
|
||||||
|
isActiveMenu.value = activeItem === itemKey.value || activeItem ? activeItem.startsWith(itemKey.value + '-') : false;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => layoutState.activeMenuItem,
|
||||||
|
(newVal) => {
|
||||||
|
isActiveMenu.value = newVal === itemKey.value || newVal.startsWith(itemKey.value + '-');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function itemClick(event, item) {
|
||||||
|
if (item.disabled) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((item.to || item.url) && (layoutState.staticMenuMobileActive || layoutState.overlayMenuActive)) {
|
||||||
|
onMenuToggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.command) {
|
||||||
|
item.command({ originalEvent: event, item: item });
|
||||||
|
}
|
||||||
|
|
||||||
|
const foundItemKey = item.items ? (isActiveMenu.value ? props.parentItemKey : itemKey) : itemKey.value;
|
||||||
|
|
||||||
|
setActiveMenuItem(foundItemKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkActiveRoute(item) {
|
||||||
|
return route.path === item.to;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<li :class="{ 'layout-root-menuitem': root, 'active-menuitem': isActiveMenu, 'p-disabled': item.disabled }">
|
||||||
|
<div v-if="root && item.visible !== false" class="layout-menuitem-root-text">{{ item.label }}</div>
|
||||||
|
<a v-if="(!item.to || item.items) && item.visible !== false" :href="item.url"
|
||||||
|
@click="itemClick($event, item, index)" :class="item.class" :target="item.target" tabindex="0">
|
||||||
|
<i :class="item.icon" class="layout-menuitem-icon"></i>
|
||||||
|
<span class="layout-menuitem-text">{{ item.label }}</span>
|
||||||
|
<i class="pi pi-fw pi-angle-down layout-submenu-toggler" v-if="item.items"></i>
|
||||||
|
</a>
|
||||||
|
<router-link v-if="item.to && !item.items && item.visible !== false" @click="itemClick($event, item, index)"
|
||||||
|
:class="[item.class, { 'active-route': checkActiveRoute(item) }]" tabindex="0" :to="item.to">
|
||||||
|
<i :class="item.icon" class="layout-menuitem-icon"></i>
|
||||||
|
<span class="layout-menuitem-text">{{ item.label }}</span>
|
||||||
|
<i class="pi pi-fw pi-angle-down layout-submenu-toggler" v-if="item.items"></i>
|
||||||
|
</router-link>
|
||||||
|
<Transition v-if="item.items && item.visible !== false" name="layout-submenu">
|
||||||
|
<ul v-show="root ? true : isActiveMenu" class="layout-submenu">
|
||||||
|
<app-menu-item v-for="(child, i) in item.items" :key="child" :index="i" :item="child" :parentItemKey="itemKey"
|
||||||
|
:root="false"></app-menu-item>
|
||||||
|
</ul>
|
||||||
|
</Transition>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
11
frontend/src/layout/AppSidebar.vue
Normal file
11
frontend/src/layout/AppSidebar.vue
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script setup>
|
||||||
|
import AppMenu from './AppMenu.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="layout-sidebar">
|
||||||
|
<app-menu></app-menu>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
110
frontend/src/layout/AppTopbar.vue
Normal file
110
frontend/src/layout/AppTopbar.vue
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<template>
|
||||||
|
<div class="layout-topbar">
|
||||||
|
<div class="layout-topbar-logo-container">
|
||||||
|
<button class="layout-menu-button layout-topbar-action" @click="onMenuToggle">
|
||||||
|
<i class="pi pi-bars"></i>
|
||||||
|
</button>
|
||||||
|
<router-link to="/" class="layout-topbar-logo">
|
||||||
|
<img :src="logo" />
|
||||||
|
|
||||||
|
<span>MEGREZ</span>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="layout-topbar-actions">
|
||||||
|
<div class="layout-config-menu">
|
||||||
|
<button type="button" class="layout-topbar-action" @click="toggleDarkMode">
|
||||||
|
<i :class="['pi', { 'pi-moon': isDarkTheme, 'pi-sun': !isDarkTheme }]"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="layout-topbar-menu-button layout-topbar-action"
|
||||||
|
v-styleclass="{ selector: '@next', enterFromClass: 'hidden', enterActiveClass: 'animate-scalein', leaveToClass: 'hidden', leaveActiveClass: 'animate-fadeout', hideOnOutsideClick: true }">
|
||||||
|
<i class="pi pi-ellipsis-v"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="layout-topbar-menu hidden lg:block">
|
||||||
|
<div class="layout-topbar-menu-content">
|
||||||
|
<!-- <button type="button" class="layout-topbar-action layout-topbar-action-highlight">
|
||||||
|
<i class="pi pi-user"></i>
|
||||||
|
<span>Profile</span>
|
||||||
|
</button> -->
|
||||||
|
<Button severity="secondary" rounded class="h-10" @click="showMenu($event)">
|
||||||
|
<Button icon="pi pi-user" rounded class="!h-[2.3rem] !w-[2.3rem] -ml-3" />
|
||||||
|
<span class="font-bold">{{ username }}</span>
|
||||||
|
<i class="pi" :class="profileMenuActive ? 'pi-angle-up' : 'pi-angle-down'"></i>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Menu ref="profileMenu" :model="instanceMenuItems" :popup="true" @blur="profileMenuActive = false" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import api from '@/api';
|
||||||
|
import logo from '@/assets/logo.svg';
|
||||||
|
import { useLayout } from '@/layout/composables/layout';
|
||||||
|
import { useProfileStore } from '@/stores/profile';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const { onMenuToggle, toggleDarkMode, isDarkTheme } = useLayout();
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const toast = useToast()
|
||||||
|
const profileStore = useProfileStore();
|
||||||
|
const username = ref('');
|
||||||
|
|
||||||
|
const profileMenu = ref(null);
|
||||||
|
const profileMenuActive = ref(false);
|
||||||
|
const instanceMenuItems = [
|
||||||
|
{
|
||||||
|
label: '个人信息',
|
||||||
|
icon: 'pi pi-user !text-cyan-500',
|
||||||
|
command: () => {
|
||||||
|
profileMenu.value.hide();
|
||||||
|
// router.push('/profile');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '安全设置',
|
||||||
|
icon: 'pi pi-cog !text-emerald-500',
|
||||||
|
command: () => {
|
||||||
|
profileMenu.value.hide();
|
||||||
|
router.push('/settings');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '退出登录',
|
||||||
|
icon: 'pi pi-sign-out',
|
||||||
|
command: () => {
|
||||||
|
profileMenu.value.hide();
|
||||||
|
logout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const showMenu = (event) => {
|
||||||
|
profileMenu.value.show(event);
|
||||||
|
profileMenuActive.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
api.UserLogout().then(() => {
|
||||||
|
profileStore.clearUserProfile()
|
||||||
|
toast.add({ severity: 'success', summary: '退出登录成功', life: 3000 })
|
||||||
|
router.push('/login')
|
||||||
|
}).catch(_ => {
|
||||||
|
toast.add({ severity: 'error', summary: '退出登录失败', life: 3000 })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
username.value = profileStore.username;
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
84
frontend/src/layout/composables/layout.js
Normal file
84
frontend/src/layout/composables/layout.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { computed, reactive, readonly } from 'vue';
|
||||||
|
|
||||||
|
const layoutConfig = reactive({
|
||||||
|
preset: 'Aura',
|
||||||
|
primary: 'blue',
|
||||||
|
surface: null,
|
||||||
|
darkTheme: false,
|
||||||
|
menuMode: 'static'
|
||||||
|
});
|
||||||
|
|
||||||
|
const layoutState = reactive({
|
||||||
|
staticMenuDesktopInactive: false,
|
||||||
|
overlayMenuActive: false,
|
||||||
|
profileSidebarVisible: false,
|
||||||
|
configSidebarVisible: false,
|
||||||
|
staticMenuMobileActive: false,
|
||||||
|
menuHoverActive: false,
|
||||||
|
activeMenuItem: null
|
||||||
|
});
|
||||||
|
|
||||||
|
export function useLayout() {
|
||||||
|
const setPrimary = (value) => {
|
||||||
|
layoutConfig.primary = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setSurface = (value) => {
|
||||||
|
layoutConfig.surface = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setPreset = (value) => {
|
||||||
|
layoutConfig.preset = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setActiveMenuItem = (item) => {
|
||||||
|
layoutState.activeMenuItem = item.value || item;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setMenuMode = (mode) => {
|
||||||
|
layoutConfig.menuMode = mode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleDarkMode = () => {
|
||||||
|
if (!document.startViewTransition) {
|
||||||
|
executeDarkModeToggle();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.startViewTransition(() => executeDarkModeToggle(event));
|
||||||
|
};
|
||||||
|
|
||||||
|
const executeDarkModeToggle = () => {
|
||||||
|
layoutConfig.darkTheme = !layoutConfig.darkTheme;
|
||||||
|
document.documentElement.classList.toggle('app-dark');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMenuToggle = () => {
|
||||||
|
if (layoutConfig.menuMode === 'overlay') {
|
||||||
|
layoutState.overlayMenuActive = !layoutState.overlayMenuActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.innerWidth > 991) {
|
||||||
|
layoutState.staticMenuDesktopInactive = !layoutState.staticMenuDesktopInactive;
|
||||||
|
} else {
|
||||||
|
layoutState.staticMenuMobileActive = !layoutState.staticMenuMobileActive;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetMenu = () => {
|
||||||
|
layoutState.overlayMenuActive = false;
|
||||||
|
layoutState.staticMenuMobileActive = false;
|
||||||
|
layoutState.menuHoverActive = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSidebarActive = computed(() => layoutState.overlayMenuActive || layoutState.staticMenuMobileActive);
|
||||||
|
|
||||||
|
const isDarkTheme = computed(() => layoutConfig.darkTheme);
|
||||||
|
|
||||||
|
const getPrimary = computed(() => layoutConfig.primary);
|
||||||
|
|
||||||
|
const getSurface = computed(() => layoutConfig.surface);
|
||||||
|
|
||||||
|
return { layoutConfig: readonly(layoutConfig), layoutState: readonly(layoutState), onMenuToggle, isSidebarActive, isDarkTheme, getPrimary, getSurface, setActiveMenuItem, toggleDarkMode, setPrimary, setSurface, setPreset, resetMenu, setMenuMode };
|
||||||
|
}
|
||||||
83
frontend/src/main.js
Normal file
83
frontend/src/main.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { createApp } from 'vue';
|
||||||
|
import App from './App.vue';
|
||||||
|
import router from './router';
|
||||||
|
|
||||||
|
import { createPinia } from 'pinia';
|
||||||
|
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
|
||||||
|
|
||||||
|
import { updatePreset } from '@primevue/themes';
|
||||||
|
import Aura from '@primevue/themes/aura';
|
||||||
|
import PrimeVue from 'primevue/config';
|
||||||
|
import ConfirmationService from 'primevue/confirmationservice';
|
||||||
|
import ToastService from 'primevue/toastservice';
|
||||||
|
|
||||||
|
import '@/assets/styles.scss';
|
||||||
|
import '@/assets/tailwind.css';
|
||||||
|
|
||||||
|
const app = createApp(App);
|
||||||
|
|
||||||
|
const pinia = createPinia()
|
||||||
|
pinia.use(piniaPluginPersistedstate)
|
||||||
|
|
||||||
|
app.use(pinia);
|
||||||
|
app.use(router);
|
||||||
|
app.use(PrimeVue, {
|
||||||
|
theme: {
|
||||||
|
preset: Aura,
|
||||||
|
options: {
|
||||||
|
darkModeSelector: '.app-dark'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pt: {
|
||||||
|
card: {
|
||||||
|
root: {
|
||||||
|
class: 'rounded-2xl'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
root: {
|
||||||
|
class: 'rounded-2xl'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
app.use(ToastService);
|
||||||
|
app.use(ConfirmationService);
|
||||||
|
|
||||||
|
updatePreset({
|
||||||
|
semantic: {
|
||||||
|
primary: { 50: '#eff6ff', 100: '#dbeafe', 200: '#bfdbfe', 300: '#93c5fd', 400: '#60a5fa', 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', 800: '#1e40af', 900: '#1e3a8a', 950: '#172554' },
|
||||||
|
colorScheme: {
|
||||||
|
light: {
|
||||||
|
primary: {
|
||||||
|
color: '{primary.500}',
|
||||||
|
contrastColor: '#ffffff',
|
||||||
|
hoverColor: '{primary.600}',
|
||||||
|
activeColor: '{primary.700}'
|
||||||
|
},
|
||||||
|
highlight: {
|
||||||
|
background: '{primary.50}',
|
||||||
|
focusBackground: '{primary.100}',
|
||||||
|
color: '{primary.700}',
|
||||||
|
focusColor: '{primary.800}'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
primary: {
|
||||||
|
color: '{primary.400}',
|
||||||
|
contrastColor: '{surface.900}',
|
||||||
|
hoverColor: '{primary.300}',
|
||||||
|
activeColor: '{primary.200}'
|
||||||
|
},
|
||||||
|
highlight: {
|
||||||
|
background: 'color-mix(in srgb, {primary.400}, transparent 84%)',
|
||||||
|
focusBackground: 'color-mix(in srgb, {primary.400}, transparent 76%)',
|
||||||
|
color: 'rgba(255,255,255,.87)',
|
||||||
|
focusColor: 'rgba(255,255,255,.87)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.mount('#app');
|
||||||
85
frontend/src/router/index.js
Normal file
85
frontend/src/router/index.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import AppLayout from '@/layout/AppLayout.vue';
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router';
|
||||||
|
|
||||||
|
import Login from '@/views/Login.vue';
|
||||||
|
import Register from '@/views/Register.vue';
|
||||||
|
|
||||||
|
import InstanceCreate from '@/views/users/InstanceCreate.vue';
|
||||||
|
import InstanceList from '@/views/users/InstanceList.vue';
|
||||||
|
import Settings from '@/views/users/Settings.vue';
|
||||||
|
|
||||||
|
import Images from '@/views/admin/Images.vue';
|
||||||
|
import Instances from '@/views/admin/Instances.vue';
|
||||||
|
import Servers from '@/views/admin/Servers.vue';
|
||||||
|
import Users from '@/views/admin/Users.vue';
|
||||||
|
|
||||||
|
import NotFound from '@/views/NotFound.vue';
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
redirect: '/instances'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'login',
|
||||||
|
component: Login
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/register',
|
||||||
|
name: 'register',
|
||||||
|
component: Register
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'dashboard',
|
||||||
|
component: AppLayout,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'instances',
|
||||||
|
name: 'instances',
|
||||||
|
component: InstanceList
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'instances/create',
|
||||||
|
name: 'instances-create',
|
||||||
|
component: InstanceCreate
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'settings',
|
||||||
|
name: 'settings',
|
||||||
|
component: Settings
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'admin/images',
|
||||||
|
name: 'admin-images',
|
||||||
|
component: Images
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'admin/instances',
|
||||||
|
name: 'admin-instances',
|
||||||
|
component: Instances
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'admin/servers',
|
||||||
|
name: 'admin-servers',
|
||||||
|
component: Servers
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'admin/users',
|
||||||
|
name: 'admin-users',
|
||||||
|
component: Users
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/:pathMatch(.*)*',
|
||||||
|
name: 'not-found',
|
||||||
|
component: NotFound
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
30
frontend/src/stores/profile.js
Normal file
30
frontend/src/stores/profile.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
export const useProfileStore = defineStore('profile', {
|
||||||
|
state: () => ({
|
||||||
|
userProfile: {
|
||||||
|
username: '',
|
||||||
|
email: '',
|
||||||
|
role: 0
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
setUserProfile(profile) {
|
||||||
|
this.userProfile = profile;
|
||||||
|
},
|
||||||
|
clearUserProfile() {
|
||||||
|
this.userProfile = {
|
||||||
|
username: '',
|
||||||
|
email: '',
|
||||||
|
role: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
isAuthenticated: (state) => !!state.userProfile.email,
|
||||||
|
isAdmin: (state) => state.userProfile.role == 2,
|
||||||
|
isSuperAdmin: (state) => state.userProfile.role === 3,
|
||||||
|
username: (state) => state.userProfile.username,
|
||||||
|
},
|
||||||
|
persist: true // 开启持久化
|
||||||
|
});
|
||||||
17
frontend/src/utils/time.js
Normal file
17
frontend/src/utils/time.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
function formatDate(dateString) {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
|
||||||
|
// 自定义格式化输出 (YYYY-MM-DD HH:mm:ss)
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0') // 月份从 0 开始
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||||
|
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
formatDate
|
||||||
|
};
|
||||||
84
frontend/src/views/Login.vue
Normal file
84
frontend/src/views/Login.vue
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<template>
|
||||||
|
<FloatingConfigurator />
|
||||||
|
<div
|
||||||
|
class="bg-surface-50 dark:bg-surface-950 flex items-center justify-center min-h-screen min-w-[100vw] overflow-hidden">
|
||||||
|
<div class="flex flex-col items-center justify-center">
|
||||||
|
<div
|
||||||
|
style="border-radius: 56px; padding: 0.3rem; background: linear-gradient(180deg, var(--primary-color) 10%, rgba(33, 150, 243, 0) 30%)">
|
||||||
|
<div class="w-full bg-surface-0 dark:bg-surface-900 py-20 px-8 sm:px-20" style="border-radius: 53px">
|
||||||
|
<div class="text-center items-center flex flex-col mb-8">
|
||||||
|
<img :src="logo" style="width: 16rem;" />
|
||||||
|
<div class="text-surface-900 dark:text-surface-0 text-3xl font-medium mt-6 mb-4">天权 算能聚联计算平台</div>
|
||||||
|
<span class="text-muted-color font-medium">登入以继续</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="email1" class="block text-surface-900 dark:text-surface-0 text-xl font-medium mb-2">账号</label>
|
||||||
|
<InputText id="email1" type="text" placeholder="用户名或邮箱" class="w-full md:w-[30rem] mb-8"
|
||||||
|
v-model="form.account" />
|
||||||
|
|
||||||
|
<label for="password1"
|
||||||
|
class="block text-surface-900 dark:text-surface-0 font-medium text-xl mb-2">密码</label>
|
||||||
|
<Password id="password1" v-model="form.password" placeholder="密码" :toggleMask="true" class="mb-4" fluid
|
||||||
|
:feedback="false" @keydown.enter="handleSubmit" />
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mt-2 mb-8 gap-8">
|
||||||
|
<span class="font-medium no-underline ml-2 text-right cursor-pointer text-slate-600">没有账号?<span
|
||||||
|
class="text-primary" @click="handleRegister">立即注册</span></span>
|
||||||
|
<span class="font-medium no-underline ml-2 text-right cursor-pointer text-primary">忘记密码</span>
|
||||||
|
</div>
|
||||||
|
<Button label="登入" class="w-full" @click="handleSubmit"></Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import logo from '@/assets/logo-text.webp';
|
||||||
|
import FloatingConfigurator from '@/components/FloatingConfigurator.vue';
|
||||||
|
|
||||||
|
import api from '@/api';
|
||||||
|
import { useProfileStore } from '@/stores/profile';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
const profileStore = useProfileStore()
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
account: '',
|
||||||
|
password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
api.UserLogin(form.value).then(res => {
|
||||||
|
// console.log(res)
|
||||||
|
toast.add({ severity: 'success', summary: '登入成功', detail: '欢迎回来!', life: 3000 })
|
||||||
|
profileStore.setUserProfile(res.data.data.result)
|
||||||
|
router.push('/')
|
||||||
|
}).catch(err => {
|
||||||
|
toast.add({ severity: 'error', summary: '登录失败', detail: err.response.data.msg, life: 3000 })
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRegister = () => {
|
||||||
|
router.push('/register')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pi-eye {
|
||||||
|
transform: scale(1.6);
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-eye-slash {
|
||||||
|
transform: scale(1.6);
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
52
frontend/src/views/NotFound.vue
Normal file
52
frontend/src/views/NotFound.vue
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<script setup>
|
||||||
|
import logo from '@/assets/logo.svg';
|
||||||
|
import FloatingConfigurator from '@/components/FloatingConfigurator.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FloatingConfigurator />
|
||||||
|
<div class="flex items-center justify-center min-h-screen overflow-hidden">
|
||||||
|
<div class="flex flex-col items-center justify-center">
|
||||||
|
<img :src="logo" width="64" height="32" class="-mt-6 mb-4" />
|
||||||
|
<div
|
||||||
|
style="border-radius: 56px; padding: 0.3rem; background: linear-gradient(180deg, color-mix(in srgb, var(--primary-color), transparent 60%) 10%, var(--surface-ground) 30%)">
|
||||||
|
<div class="w-full bg-surface-0 dark:bg-surface-900 py-20 px-8 sm:px-20 flex flex-col items-center"
|
||||||
|
style="border-radius: 53px">
|
||||||
|
<span class="text-primary font-bold text-3xl">404</span>
|
||||||
|
<h1 class="text-surface-900 dark:text-surface-0 font-bold text-3xl lg:text-5xl mb-2">Not Found</h1>
|
||||||
|
<div class="text-surface-600 dark:text-surface-200 mb-8">页面不存在</div>
|
||||||
|
<router-link to="/" class="w-full flex items-center py-8 border-surface-300 dark:border-surface-500 border-b">
|
||||||
|
<span class="flex justify-center items-center border-2 border-primary text-primary rounded-border"
|
||||||
|
style="height: 3.5rem; width: 3.5rem">
|
||||||
|
<i class="pi pi-fw pi-question-circle !text-2xl"></i>
|
||||||
|
</span>
|
||||||
|
<span class="ml-6 flex flex-col">
|
||||||
|
<span class="text-surface-900 dark:text-surface-0 lg:text-xl font-medium mb-0">
|
||||||
|
帮助文档
|
||||||
|
</span>
|
||||||
|
<span class="text-surface-600 dark:text-surface-200 lg:text-xl">
|
||||||
|
可能可以找到解决办法?
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</router-link>
|
||||||
|
<router-link to="/"
|
||||||
|
class="w-full flex items-center mb-8 py-8 border-surface-300 dark:border-surface-500 border-b">
|
||||||
|
<span class="flex justify-center items-center border-2 border-primary text-primary rounded-border"
|
||||||
|
style="height: 3.5rem; width: 3.5rem">
|
||||||
|
<i class="pi pi-fw pi-unlock !text-2xl"></i>
|
||||||
|
</span>
|
||||||
|
<span class="ml-6 flex flex-col">
|
||||||
|
<span class="text-surface-900 dark:text-surface-0 lg:text-xl font-medium mb-0">
|
||||||
|
权限限制
|
||||||
|
</span>
|
||||||
|
<span class="text-surface-600 dark:text-surface-200 lg:text-xl">
|
||||||
|
没有权限访问此页面
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</router-link>
|
||||||
|
<Button as="router-link" label="回到仪表盘" to="/" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
83
frontend/src/views/Register.vue
Normal file
83
frontend/src/views/Register.vue
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<template>
|
||||||
|
<FloatingConfigurator />
|
||||||
|
<div
|
||||||
|
class="bg-surface-50 dark:bg-surface-950 flex items-center justify-center min-h-screen min-w-[100vw] overflow-hidden">
|
||||||
|
<div class="flex flex-col items-center justify-center">
|
||||||
|
<div
|
||||||
|
style="border-radius: 56px; padding: 0.3rem; background: linear-gradient(180deg, var(--primary-color) 10%, rgba(33, 150, 243, 0) 30%)">
|
||||||
|
<div class="w-full bg-surface-0 dark:bg-surface-900 py-20 px-8 sm:px-20" style="border-radius: 53px">
|
||||||
|
<div class="text-center items-center flex flex-col mb-8">
|
||||||
|
<img :src="logo" style="width: 16rem;" />
|
||||||
|
<div class="text-surface-900 dark:text-surface-0 text-3xl font-medium mt-6 mb-4">天权 算能聚联计算平台</div>
|
||||||
|
<span class="text-muted-color font-medium">注册新账号</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="username"
|
||||||
|
class="block text-surface-900 dark:text-surface-0 text-xl font-medium mb-2">用户名</label>
|
||||||
|
<InputText id="用户名" type="text" placeholder="用户名" class="w-full md:w-[30rem] mb-8"
|
||||||
|
v-model="form.username" />
|
||||||
|
|
||||||
|
<label for="email" class="block text-surface-900 dark:text-surface-0 text-xl font-medium mb-2">邮箱</label>
|
||||||
|
<InputText id="email" type="text" placeholder="邮箱" class="w-full md:w-[30rem] mb-8" v-model="form.email" />
|
||||||
|
|
||||||
|
<label for="password" class="block text-surface-900 dark:text-surface-0 font-medium text-xl mb-2">密码</label>
|
||||||
|
<Password id="password" v-model="form.password" placeholder="密码" :toggleMask="true" class="mb-4" fluid
|
||||||
|
:feedback="false" @keydown.enter="handleSubmit" />
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mt-2 mb-8 gap-8">
|
||||||
|
<span class="font-medium no-underline ml-2 text-right cursor-pointer text-slate-600">已有账号?<span
|
||||||
|
class="text-primary" @click="handleLogin">立即登入</span></span>
|
||||||
|
</div>
|
||||||
|
<Button label="注册" class="w-full" @click="handleSubmit"></Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import logo from '@/assets/logo-text.webp';
|
||||||
|
import FloatingConfigurator from '@/components/FloatingConfigurator.vue';
|
||||||
|
|
||||||
|
import api from '@/api';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
username: '',
|
||||||
|
email: '',
|
||||||
|
password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
api.UserRegister(form.value).then(res => {
|
||||||
|
toast.add({ severity: 'success', summary: '注册成功', life: 3000 })
|
||||||
|
router.push('/login')
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
toast.add({ severity: 'error', summary: '注册失败', detail: err.response.data.msg, life: 3000 })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogin = () => {
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pi-eye {
|
||||||
|
transform: scale(1.6);
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-eye-slash {
|
||||||
|
transform: scale(1.6);
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
118
frontend/src/views/admin/Images.vue
Normal file
118
frontend/src/views/admin/Images.vue
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<template>
|
||||||
|
<SectionBanner label="镜像管理" icon="pi pi-images text-teal-500">
|
||||||
|
<Button icon="pi pi-plus" label="添加镜像" rounded @click="addImage" />
|
||||||
|
<Button severity="success" icon="pi pi-save" label="保存" rounded @click="saveImages" />
|
||||||
|
</SectionBanner>
|
||||||
|
|
||||||
|
<div class="card mt-7 rounded-2xl">
|
||||||
|
<!-- <Fluid v-for="(image, index) in data" :key="image.name">
|
||||||
|
<Fieldset :legend="image.name" :toggleable="true">
|
||||||
|
<InputText v-model="data[index].tag" type="text" size="large" placeholder="Large" />
|
||||||
|
</Fieldset>
|
||||||
|
</Fluid> -->
|
||||||
|
<Accordion value="0" expandIcon="pi pi-plus" collapseIcon="pi pi-minus">
|
||||||
|
<AccordionPanel v-for="(image, index) in data" :key="image.name" :value="image.name">
|
||||||
|
<AccordionHeader>
|
||||||
|
<span v-if="!image.edit">{{ image.name }}</span>
|
||||||
|
<InputText v-else v-model="tmp.name" class="flex-auto mr-6" type="text" placeholder="备注名" />
|
||||||
|
</AccordionHeader>
|
||||||
|
<AccordionContent>
|
||||||
|
<span class="flex items-center gap-2 w-full">
|
||||||
|
<span class="whitespace-nowrap">镜像名:
|
||||||
|
<Chip v-if="!image.edit" :label="image.tag" />
|
||||||
|
<InputText v-else v-model="tmp.tag" class="flex-auto w-full" type="text" placeholder="镜像名" />
|
||||||
|
</span>
|
||||||
|
<!-- <Button class="ml-auto mr-2" icon="pi pi-check" size="small" aria-label="Filter" /> -->
|
||||||
|
<div class="ml-auto mr-0 flex gap-2">
|
||||||
|
<Tag v-if="!image.edit" class="ml-auto" icon="pi pi-pencil" value="编辑" @click="editImage(index)" />
|
||||||
|
<Tag v-else class="ml-auto" severity="success" icon="pi pi-save" value="保存"
|
||||||
|
@click="saveEditImage(index)" />
|
||||||
|
<Tag class="ml-auto" severity="danger" icon="pi pi-trash" value="删除" @click="deleteImage(index)"></Tag>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionPanel>
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
import api from '@/api';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const data = ref([])
|
||||||
|
const tmp = ref(null)
|
||||||
|
|
||||||
|
const refreshIcon = ref('pi pi-refresh')
|
||||||
|
|
||||||
|
const getImages = async () => {
|
||||||
|
refreshIcon.value = 'pi pi-spin pi-refresh'
|
||||||
|
data.value = []
|
||||||
|
await api.AdminImagesList().then(res => {
|
||||||
|
Object.keys(res.data.data.result).forEach(key => {
|
||||||
|
data.value.push({
|
||||||
|
name: key,
|
||||||
|
tag: res.data.data.result[key],
|
||||||
|
edit: false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
data.value.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
}).catch(err => {
|
||||||
|
toast.add({ severity: 'error', summary: '获取镜像列表错误', detail: err.response.data.msg, life: 3000 })
|
||||||
|
}).finally(() => {
|
||||||
|
refreshIcon.value = 'pi pi-refresh'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveImages = async () => {
|
||||||
|
let images = {}
|
||||||
|
data.value.forEach(item => {
|
||||||
|
images[item.name] = item.tag
|
||||||
|
})
|
||||||
|
await api.AdminImagesModify(images).then(res => {
|
||||||
|
toast.add({ severity: 'success', summary: '保存成功', detail: res.data.msg, life: 3000 })
|
||||||
|
getImages()
|
||||||
|
}).catch(err => {
|
||||||
|
toast.add({ severity: 'error', summary: '保存失败', detail: err.response.data.msg, life: 3000 })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const addImage = () => {
|
||||||
|
data.value.unshift({
|
||||||
|
name: '',
|
||||||
|
tag: '',
|
||||||
|
edit: true
|
||||||
|
})
|
||||||
|
tmp.value = {
|
||||||
|
name: '',
|
||||||
|
tag: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const editImage = (index) => {
|
||||||
|
tmp.value = {
|
||||||
|
name: data.value[index].name,
|
||||||
|
tag: data.value[index].tag
|
||||||
|
}
|
||||||
|
data.value[index].edit = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveEditImage = (index) => {
|
||||||
|
data.value[index].name = tmp.value.name
|
||||||
|
data.value[index].tag = tmp.value.tag
|
||||||
|
data.value[index].edit = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteImage = (index) => {
|
||||||
|
data.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getImages()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
609
frontend/src/views/admin/Instances.vue
Normal file
609
frontend/src/views/admin/Instances.vue
Normal file
@ -0,0 +1,609 @@
|
|||||||
|
<template>
|
||||||
|
<SectionBanner label="实例管理" icon="pi pi-desktop text-lime-500">
|
||||||
|
<Button severity="contrast" :icon="refreshIcon" label="刷新" rounded @click="getInstances" />
|
||||||
|
</SectionBanner>
|
||||||
|
|
||||||
|
<div class="card mt-7 rounded-2xl">
|
||||||
|
<DataTable :value="data" pt:header:class="font-black">
|
||||||
|
<template #empty>
|
||||||
|
<div class="text-center mt-2 mb-2 text-secondary">
|
||||||
|
未创建实例
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<Column field="id" frozen class="font-bold"></Column>
|
||||||
|
<Column header="主机名称 / 用户" class="min-w-40">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<span class="text-primary font-bold" @click="getServerDetailEvent($event, data.server_id)"
|
||||||
|
@mouseenter="getServerDetailEvent($event, data.server_id)">{{ data.server_name }}</span>
|
||||||
|
<br />
|
||||||
|
<span class="font-bold">{{ data.username }}</span>
|
||||||
|
<br />
|
||||||
|
<EditLabel v-model="data.label" class="mt-1" emptyText="设置备注"
|
||||||
|
:save="(label) => instanceModifyLabel(data.id, label)" />
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column header="状态" class="min-w-28 font-bold">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<Tag v-if="data.status == statusRunning" icon="pi pi-play" value="运行中" />
|
||||||
|
<Tag v-else-if="data.status == statusPaused" severity="warn" icon="pi pi-pause" value="暂停" />
|
||||||
|
<Tag v-else-if="data.status == statusStoped" severity="secondary" icon="pi pi-stop-circle" value="已停止" />
|
||||||
|
|
||||||
|
<Tag v-else-if="data.status == statusReady" severity="success" icon="pi pi-spin pi-spinner" value="准备中" />
|
||||||
|
<Tag v-else-if="data.status == statusStarting" severity="info" icon="pi pi-spin pi-spinner" value="启动中" />
|
||||||
|
<Tag v-else-if="data.status == statusStopping" severity="info" icon="pi pi-spin pi-spinner" value="停止中" />
|
||||||
|
<Tag v-else-if="data.status == statusPausing" severity="info" icon="pi pi-spin pi-spinner" value="暂停中" />
|
||||||
|
<Tag v-else-if="data.status == statusRestarting" severity="info" icon="pi pi-spin pi-spinner" value="重启中" />
|
||||||
|
<Tag v-else-if="data.status == statusModifying" severity="success" icon="pi pi-spin pi-spinner" value="调整中" />
|
||||||
|
<Tag v-else-if="data.status == statusDeleting" severity="danger" icon="pi pi-spin pi-spinner" value="删除中" />
|
||||||
|
|
||||||
|
<Tag v-else-if="data.status == statusFail" severity="danger" icon="pi pi-exclamation-triangle" value="错误" />
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column header="规格详情" class="min-w-36 font-bold">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<span v-if="data.gpu_count !== 0">{{ data.gpu_type }} * {{ data.gpu_count }}</span>
|
||||||
|
<span v-else>无卡模式</span>
|
||||||
|
<br />
|
||||||
|
<span class="text-primary" @click="getInstanceDetail($event, data)"
|
||||||
|
@mouseenter="getInstanceDetail($event, data)">查看详情</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column header="SSH 信息">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<span class="text-surface-900 dark:text-surface-0 font-medium mr-2 mb-1 md:mb-0">地址:</span>
|
||||||
|
<div v-if="data.status === statusRunning" class="mt-1 text-muted-color">{{ data.ssh_address }}
|
||||||
|
<CopyIcon class="ml-1" :text="data.ssh_address" />
|
||||||
|
</div>
|
||||||
|
<div v-else class="mt-1 text-muted-color">暂无</div>
|
||||||
|
<span class="text-surface-900 dark:text-surface-0 font-medium mr-2 mb-1 md:mb-0">密码:</span>
|
||||||
|
<div v-if="data.status === statusRunning" class="mt-1 text-muted-color">********
|
||||||
|
<CopyIcon class="ml-1" :text="data.ssh_passwd" />
|
||||||
|
</div>
|
||||||
|
<div v-else class="mt-1 text-muted-color">暂无</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="create_at" header="创建时间"></Column>
|
||||||
|
<Column>
|
||||||
|
<template #body="{ data }">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button v-if="data.status == statusRunning" icon="pi pi-inbox" aria-label="Filter" as="a"
|
||||||
|
:href="'http://' + data.jupyter_address + '/lab'" target="_blank" v-tooltip.top="'Jupter Lab'" />
|
||||||
|
<Button v-else icon="pi pi-inbox" aria-label="Filter" v-tooltip.top="'Jupter Lab'" disabled />
|
||||||
|
<Button v-if="data.status == statusRunning" severity="contrast" icon="pi pi-chart-bar" as="a"
|
||||||
|
:href="'http://' + data.grafana_address + '/public-dashboards/2c510f203876465ba76617510ce3e219'"
|
||||||
|
target="_blank" v-tooltip.top="'监控'" />
|
||||||
|
<Button v-else severity="contrast" icon="pi pi-chart-bar" v-tooltip.top="'监控'" disabled />
|
||||||
|
<Button v-if="!isAdmin" icon="pi pi-ellipsis-h" severity="secondary" aria-label="Bookmark"
|
||||||
|
@click="showMenu($event, data)" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
|
||||||
|
<Paginator class="ml-auto mt-4" :rows="10" :totalRecords="total" :rowsPerPageOptions="[10, 20, 30]"
|
||||||
|
@page="changePage">
|
||||||
|
<template #start>
|
||||||
|
共 {{ total }} 个
|
||||||
|
</template>
|
||||||
|
</Paginator>
|
||||||
|
|
||||||
|
<Menu ref="instanceMenu" :model="instanceMenuItems" :popup="true" />
|
||||||
|
|
||||||
|
<Popover ref="serverDetailRef">
|
||||||
|
<Fluid v-if="serverDetail" class="rounded flex flex-col gap-4 w-96">
|
||||||
|
<Fieldset legend="主机名称">
|
||||||
|
<span>{{ serverDetail.name }}</span>
|
||||||
|
</Fieldset>
|
||||||
|
<Fieldset legend="GPU空闲/总量">
|
||||||
|
<span>{{ serverDetail.gpu_num - serverDetail.gpu_used }} / {{ serverDetail.gpu_num }}</span>
|
||||||
|
</Fieldset>
|
||||||
|
<Fieldset legend="数据盘可扩容">
|
||||||
|
<span>{{ serverDetail.volume_total - serverDetail.volume_used }} GB</span>
|
||||||
|
</Fieldset>
|
||||||
|
<div class="flex flex-col md:flex-row gap-4">
|
||||||
|
<Fieldset class="flex flex-wrap gap-2 w-full" legend="GPU驱动版本">
|
||||||
|
<span>{{ serverDetail.gpu_driver_version }}</span>
|
||||||
|
</Fieldset>
|
||||||
|
<Fieldset class="flex flex-wrap gap-2 w-full" legend="CUDA版本">
|
||||||
|
<span>≤ {{ serverDetail.gpu_cuda_version }}</span>
|
||||||
|
</Fieldset>
|
||||||
|
</div>
|
||||||
|
</Fluid>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<Popover ref="instanceDetailRef">
|
||||||
|
<Fluid v-if="instanceDetail" class="rounded flex flex-col gap-4 w-full">
|
||||||
|
<Fieldset legend="镜像">
|
||||||
|
<span>{{ imagesValueMap[instanceDetail.image_name] || instanceDetail.image_name }}</span>
|
||||||
|
</Fieldset>
|
||||||
|
<Fieldset legend="GPU">
|
||||||
|
<span v-if="instanceDetail.gpu_count !== 0">{{ instanceDetail.gpu_type }} * {{ instanceDetail.gpu_count
|
||||||
|
}}</span>
|
||||||
|
<span v-else>无卡模式</span>
|
||||||
|
</Fieldset>
|
||||||
|
<div class="flex flex-col md:flex-row gap-4">
|
||||||
|
<Fieldset class="flex flex-wrap gap-2 w-full" legend="CPU">
|
||||||
|
<span v-if="instanceDetail.gpu_count !== 0">{{ instanceDetail.cpu_count_per_gpu * instanceDetail.gpu_count
|
||||||
|
}}
|
||||||
|
核</span>
|
||||||
|
<span v-else>1 核</span>
|
||||||
|
</Fieldset>
|
||||||
|
<Fieldset class="flex flex-wrap gap-2 w-full" legend="内存">
|
||||||
|
<span v-if="instanceDetail.gpu_count !== 0">{{ instanceDetail.memory_per_gpu * instanceDetail.gpu_count }}
|
||||||
|
GB</span>
|
||||||
|
<span v-else>2 GB</span>
|
||||||
|
</Fieldset>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col md:flex-row gap-4">
|
||||||
|
<Fieldset class="flex flex-wrap gap-2 w-full" legend="系统盘">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<span>30G</span>
|
||||||
|
<i class="pi pi-info-circle mt-1" v-tooltip.top="'本地盘,快速'"></i>
|
||||||
|
</div>
|
||||||
|
</Fieldset>
|
||||||
|
<Fieldset class="flex flex-wrap gap-2 w-full" legend="数据盘">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<span> 50 + {{ instanceDetail.volume_size - 50 }} GB</span>
|
||||||
|
<i class="pi pi-info-circle mt-1" v-tooltip.top="'本地盘,快速,免费 50G,可扩容/缩容'"></i>
|
||||||
|
</div>
|
||||||
|
</Fieldset>
|
||||||
|
</div>
|
||||||
|
</Fluid>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Drawer v-model:visible="instanceModifyVisible" header="修改实例配置" position="right" :dismissable="false"
|
||||||
|
:showCloseIcon="false" class="!w-96">
|
||||||
|
<Fluid class="flex flex-col gap-4 w-full">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label>实例 ID</label>
|
||||||
|
<InputText v-model="instanceDetail.id" type="text" disabled />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label>主机名称</label>
|
||||||
|
<InputText v-model="instanceDetail.server_name" type="text" disabled />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label>GPU类型</label>
|
||||||
|
<InputText v-model="serverDetail.gpu_type" type="text" disabled />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label>CPU 核数</label>
|
||||||
|
<InputGroup>
|
||||||
|
<InputNumber v-model="instanceConfiguration.cpu_count" disabled />
|
||||||
|
<InputGroupAddon>核</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label>内存</label>
|
||||||
|
<InputGroup>
|
||||||
|
<InputNumber v-model="instanceConfiguration.memory" disabled />
|
||||||
|
<InputGroupAddon>GB</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label>无卡模式</label>
|
||||||
|
<ToggleSwitch v-model="cpu_only_mode" @change="instanceModifyCpuonlyChange" />
|
||||||
|
</div>
|
||||||
|
<div v-show="!instanceConfiguration.cpu_only" class="flex flex-col gap-2">
|
||||||
|
<label>GPU 数量</label>
|
||||||
|
<SelectButton v-model="instanceConfiguration.gpu_count" :options="instanceConfiguration.options"
|
||||||
|
optionDisabled="disabled" optionLabel="label" optionValue="value" aria-labelledby="basic" :allowEmpty="false"
|
||||||
|
@change="instanceModifyGpuChange" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label>数据盘</label>
|
||||||
|
<InputNumber v-model="instanceConfiguration.volume_size" showButtons buttonLayout="horizontal" :step="1"
|
||||||
|
suffix=" GB" :min="50"
|
||||||
|
:max="serverDetail.volume_total - serverDetail.volume_used + instanceDetail.volume_size">
|
||||||
|
<template #incrementbuttonicon>
|
||||||
|
<span class="pi pi-plus" />
|
||||||
|
</template>
|
||||||
|
<template #decrementbuttonicon>
|
||||||
|
<span class="pi pi-minus" />
|
||||||
|
</template>
|
||||||
|
</InputNumber>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label>GPU 驱动版本</label>
|
||||||
|
<InputText v-model="serverDetail.gpu_driver_version" type="text" disabled />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label>CUDA 版本</label>
|
||||||
|
<InputText v-model="serverDetail.gpu_cuda_version" type="text" disabled />
|
||||||
|
</div>
|
||||||
|
</Fluid>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex items-center gap-4 w-52 ml-auto">
|
||||||
|
<Button label="取消" class="flex-auto" severity="secondary" @click="instanceModifyVisible = false"></Button>
|
||||||
|
<Button label="确定" class="flex-auto" @click="instanceModify"></Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Drawer>
|
||||||
|
|
||||||
|
<ConfirmDialog></ConfirmDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import CopyIcon from '@/components/CopyIcon.vue';
|
||||||
|
import EditLabel from '@/components/EditLabel.vue';
|
||||||
|
import SectionBanner from '@/components/SectionBanner.vue';
|
||||||
|
|
||||||
|
import api from '@/api';
|
||||||
|
import { useProfileStore } from '@/stores/profile';
|
||||||
|
import { formatDate } from '@/utils/time.js';
|
||||||
|
import { useConfirm } from "primevue/useconfirm";
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import { nextTick, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
const statusFail = ref(-1)
|
||||||
|
const statusRunning = ref(0)
|
||||||
|
const statusPaused = ref(1)
|
||||||
|
const statusStoped = ref(2)
|
||||||
|
const statusReady = ref(3)
|
||||||
|
const statusStarting = ref(4)
|
||||||
|
const statusStopping = ref(5)
|
||||||
|
const statusPausing = ref(6)
|
||||||
|
const statusRestarting = ref(7)
|
||||||
|
const statusModifying = ref(8)
|
||||||
|
const statusDeleting = ref(9)
|
||||||
|
|
||||||
|
const statusIng = [statusReady.value, statusStarting.value, statusStopping.value, statusPausing.value, statusRestarting.value, statusModifying.value, statusDeleting.value]
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
const confirm = useConfirm()
|
||||||
|
const profileStore = useProfileStore();
|
||||||
|
|
||||||
|
const isAdmin = ref(profileStore.isAdmin)
|
||||||
|
|
||||||
|
const instanceMenu = ref(null)
|
||||||
|
const serverDetailRef = ref(null)
|
||||||
|
const serverDetail = ref(null)
|
||||||
|
const instanceDetailRef = ref(null)
|
||||||
|
const instanceDetail = ref(null)
|
||||||
|
|
||||||
|
const offset = ref(0)
|
||||||
|
const limit = ref(10)
|
||||||
|
|
||||||
|
const data = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const imagesValueMap = ref({})
|
||||||
|
|
||||||
|
const instanceModifyVisible = ref(false)
|
||||||
|
const instanceConfiguration = ref({})
|
||||||
|
const cpu_only_mode = ref(false)
|
||||||
|
|
||||||
|
const refreshIcon = ref('pi pi-refresh')
|
||||||
|
|
||||||
|
const instanceMenuItemsTemplate = ref([
|
||||||
|
{ label: '无卡模式开机', icon: 'pi pi-power-off !text-emerald-600', command: () => { cpuOnlyMode(instanceDetail.value.id) } },
|
||||||
|
{ label: '开机', icon: 'pi pi-play !text-blue-600', command: () => { instanceStart(instanceDetail.value.id) } },
|
||||||
|
{ label: '关机', icon: 'pi pi-stop', command: () => { instanceStop(instanceDetail.value.id) } },
|
||||||
|
{ label: '暂停', icon: 'pi pi-pause !text-amber-600', command: () => { instancePause(instanceDetail.value.id) } },
|
||||||
|
{ label: '重启实例', icon: 'pi pi-refresh !text-sky-500', command: () => { instanceRestart(instanceDetail.value.id) } },
|
||||||
|
{ label: '调整配置', icon: 'pi pi-sliders-h !text-indigo-500', command: () => { openInstanceModify() } },
|
||||||
|
{ label: '删除实例', icon: 'pi pi-trash !text-red-500', command: () => { openInstanceDelete() } },
|
||||||
|
])
|
||||||
|
const instanceMenuItems = ref([])
|
||||||
|
|
||||||
|
const changePage = async (event) => {
|
||||||
|
limit.value = event.rows
|
||||||
|
offset.value = event.first
|
||||||
|
await getInstances()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getInstances = async () => {
|
||||||
|
// data.value = []
|
||||||
|
refreshIcon.value = 'pi pi-spin pi-refresh'
|
||||||
|
await api.AdminInstancesList({ offset: offset.value, limit: limit.value }).then(res => {
|
||||||
|
data.value = res.data.data.result
|
||||||
|
let refresh = false
|
||||||
|
for (let i = 0; i < data.value.length; i++) {
|
||||||
|
if (statusIng.indexOf(data.value[i].status) !== -1) {
|
||||||
|
refresh = true
|
||||||
|
}
|
||||||
|
data.value[i].create_at = formatDate(data.value[i].create_at)
|
||||||
|
}
|
||||||
|
total.value = res.data.data.total || 0
|
||||||
|
refreshIcon.value = 'pi pi-refresh'
|
||||||
|
if (refresh) {
|
||||||
|
setTimeout(() => {
|
||||||
|
getInstances()
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
toast.add({ severity: 'error', summary: '获取实例列表失败', detail: err.response.data.msg, life: 3000 });
|
||||||
|
refreshIcon.value = 'pi pi-refresh'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getImages = async () => {
|
||||||
|
await api.UserImages().then((res) => {
|
||||||
|
imagesValueMap.value = Object.entries(res.data.data.result).reduce((acc, [key, value]) => {
|
||||||
|
acc[value] = key
|
||||||
|
return acc
|
||||||
|
}, {});
|
||||||
|
}).catch((error) => {
|
||||||
|
Message.error('获取镜像列表失败')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getInstanceDetail = (event, instance) => {
|
||||||
|
instanceDetailRef.value.hide()
|
||||||
|
|
||||||
|
if (instanceDetail.value?.id === instance.id) {
|
||||||
|
instanceDetail.value = null
|
||||||
|
} else {
|
||||||
|
instanceDetail.value = instance
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
instanceDetailRef.value.show(event)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getServerDetail = async (id) => {
|
||||||
|
await api.UserServerDetail(id).then(res => {
|
||||||
|
serverDetail.value = res.data.data.result
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getServerDetailEvent = (event, serverId) => {
|
||||||
|
serverDetailRef.value.hide()
|
||||||
|
|
||||||
|
if (serverDetail.value?.id === serverId) {
|
||||||
|
serverDetail.value = null
|
||||||
|
} else {
|
||||||
|
nextTick(() => {
|
||||||
|
serverDetailRef.value.show(event)
|
||||||
|
});
|
||||||
|
getServerDetail(serverId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const showMenu = (event, instance) => {
|
||||||
|
instanceDetail.value = instance
|
||||||
|
instanceMenuItems.value = []
|
||||||
|
instanceMenuItemsTemplate.value.forEach(item => {
|
||||||
|
let newItem = { ...item }
|
||||||
|
switch (item.label) {
|
||||||
|
case '无卡模式开机':
|
||||||
|
if (instanceDetail.value.cpu_only === true || instanceDetail.value.status !== statusStoped.value) {
|
||||||
|
newItem.disabled = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case '开机':
|
||||||
|
if (instanceDetail.value.status === statusRunning.value || statusIng.indexOf(instanceDetail.value.status) !== -1) {
|
||||||
|
newItem.disabled = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case '关机':
|
||||||
|
if (instanceDetail.value.status === statusStoped.value || statusIng.indexOf(instanceDetail.value.status) !== -1) {
|
||||||
|
newItem.disabled = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case '暂停':
|
||||||
|
if (instanceDetail.value.status === statusPaused.value || instanceDetail.value.status === statusStoped.value || statusIng.indexOf(instanceDetail.value.status) !== -1) {
|
||||||
|
newItem.disabled = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case '重启实例':
|
||||||
|
if (statusIng.indexOf(instanceDetail.value.status) !== -1) {
|
||||||
|
newItem.disabled = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case '调整配置':
|
||||||
|
if (instanceDetail.value.status !== statusStoped.value) {
|
||||||
|
newItem.disabled = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case '删除实例':
|
||||||
|
if (instanceDetail.value.status === statusDeleting.value) {
|
||||||
|
newItem.disabled = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
instanceMenuItems.value.push(newItem)
|
||||||
|
})
|
||||||
|
|
||||||
|
instanceMenu.value.show(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cpuOnlyMode = async (id) => {
|
||||||
|
toast.add({ severity: 'info', summary: '切换为无卡模式', detail: '正在切换为无卡模式', life: 3000 });
|
||||||
|
setTimeout(() => {
|
||||||
|
getInstances()
|
||||||
|
}, 100);
|
||||||
|
await api.AdminInstancesModify(id, { cpu_only: true }).then(async (res) => {
|
||||||
|
toast.add({ severity: 'success', summary: '切换为无卡模式', detail: '已切换为无卡模式', life: 3000 });
|
||||||
|
await getInstances()
|
||||||
|
// await instanceStart(id)
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
toast.add({ severity: 'error', summary: '切换为无卡模式失败', detail: err.response.data.msg, life: 3000 });
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const instanceStart = async (id) => {
|
||||||
|
toast.add({ severity: 'info', summary: '开机', detail: '正在开机', life: 3000 });
|
||||||
|
setTimeout(() => {
|
||||||
|
getInstances()
|
||||||
|
}, 100);
|
||||||
|
await api.AdminInstancesAction(id, { action: 1 }).then(async (res) => {
|
||||||
|
toast.add({ severity: 'success', summary: '开机', detail: '实例已开机', life: 3000 });
|
||||||
|
await getInstances()
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
toast.add({ severity: 'error', summary: '开机失败', detail: err.response.data.msg, life: 3000 });
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const instancePause = async (id) => {
|
||||||
|
toast.add({ severity: 'info', summary: '暂停实例', detail: '正在暂停实例', life: 3000 });
|
||||||
|
setTimeout(() => {
|
||||||
|
getInstances()
|
||||||
|
}, 100);
|
||||||
|
await api.AdminInstancesAction(id, { action: 2 }).then(async (res) => {
|
||||||
|
toast.add({ severity: 'success', summary: '暂停实例', detail: '实例已暂停', life: 3000 });
|
||||||
|
await getInstances()
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
toast.add({ severity: 'error', summary: '暂停实例失败', detail: err.response.data.msg, life: 3000 });
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const instanceStop = async (id) => {
|
||||||
|
toast.add({ severity: 'info', summary: '停止实例', detail: '正在停止实例', life: 3000 });
|
||||||
|
setTimeout(() => {
|
||||||
|
getInstances()
|
||||||
|
}, 100);
|
||||||
|
await api.AdminInstancesAction(id, { action: 3 }).then(async (res) => {
|
||||||
|
toast.add({ severity: 'success', summary: '停止实例', detail: '实例已停止', life: 3000 });
|
||||||
|
await getInstances()
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
toast.add({ severity: 'error', summary: '停止实例失败', detail: err.response.data.msg, life: 3000 });
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const instanceRestart = async (id) => {
|
||||||
|
toast.add({ severity: 'info', summary: '重启实例', detail: '正在重启实例', life: 3000 });
|
||||||
|
setTimeout(() => {
|
||||||
|
getInstances()
|
||||||
|
}, 100);
|
||||||
|
await api.AdminInstancesAction(id, { action: 4 }).then(async (res) => {
|
||||||
|
toast.add({ severity: 'success', summary: '重启实例', detail: '实例已重启', life: 3000 });
|
||||||
|
await getInstances()
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
toast.add({ severity: 'error', summary: '重启实例失败', detail: err.response.data.msg, life: 3000 });
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const instanceModify = async () => {
|
||||||
|
toast.add({ severity: 'info', summary: '调整配置', detail: '正在调整配置', life: 3000 });
|
||||||
|
setTimeout(() => {
|
||||||
|
getInstances()
|
||||||
|
}, 100);
|
||||||
|
await api.AdminInstancesModify(instanceDetail.value.id, instanceConfiguration.value).then(async (res) => {
|
||||||
|
toast.add({ severity: 'success', summary: '调整配置', detail: '已调整配置', life: 3000 });
|
||||||
|
instanceModifyVisible.value = false
|
||||||
|
await getInstances()
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
toast.add({ severity: 'error', summary: '调整配置失败', detail: err.response.data.msg, life: 3000 });
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const instanceDelete = async (id) => {
|
||||||
|
toast.add({ severity: 'info', summary: '释放实例', detail: '正在释放实例', life: 3000 });
|
||||||
|
setTimeout(() => {
|
||||||
|
getInstances()
|
||||||
|
}, 100);
|
||||||
|
await api.AdminInstancesDelete(id).then(async (res) => {
|
||||||
|
toast.add({ severity: 'success', summary: '释放实例', detail: '实例已释放', life: 3000 });
|
||||||
|
await getInstances()
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
toast.add({ severity: 'error', summary: '释放实例失败', detail: err.response.data.msg, life: 3000 });
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const openInstanceModify = async () => {
|
||||||
|
instanceConfiguration.value.gpu_count = instanceDetail.value.gpu_count
|
||||||
|
instanceConfiguration.value.volume_size = instanceDetail.value.volume_size
|
||||||
|
instanceConfiguration.value.cpu_only = instanceDetail.value.cpu_only
|
||||||
|
await getServerDetail(instanceDetail.value.server_id)
|
||||||
|
instanceConfiguration.value.options = []
|
||||||
|
for (let i = 0; i < serverDetail.value.gpu_num; i++) {
|
||||||
|
const tmp = {
|
||||||
|
label: i + 1,
|
||||||
|
value: i + 1,
|
||||||
|
disabled: i + 1 > serverDetail.value.gpu_num - serverDetail.value.gpu_used
|
||||||
|
}
|
||||||
|
instanceConfiguration.value.options.push(tmp)
|
||||||
|
}
|
||||||
|
if (serverDetail.value.gpu_num - serverDetail.value.gpu_used < instanceConfiguration.value.gpu_count) {
|
||||||
|
instanceConfiguration.value.gpu_count = serverDetail.value.gpu_num - serverDetail.value.gpu_used
|
||||||
|
}
|
||||||
|
instanceConfiguration.value.cpu_count = serverDetail.value.cpu_count_per_gpu * instanceConfiguration.value.gpu_count
|
||||||
|
instanceConfiguration.value.memory = serverDetail.value.memory_per_gpu * instanceConfiguration.value.gpu_count
|
||||||
|
if (serverDetail.value.gpu_num === serverDetail.value.gpu_used || instanceDetail.value.cpu_only) {
|
||||||
|
instanceConfiguration.value.gpu_count = 0
|
||||||
|
instanceConfiguration.value.cpu_only = true
|
||||||
|
cpu_only_mode.value = true
|
||||||
|
instanceConfiguration.value.cpu_count = 1
|
||||||
|
instanceConfiguration.value.memory = 2
|
||||||
|
}
|
||||||
|
instanceModifyVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const instanceModifyGpuChange = (event) => {
|
||||||
|
instanceConfiguration.value.cpu_count = serverDetail.value.cpu_count_per_gpu * event.value
|
||||||
|
instanceConfiguration.value.memory = serverDetail.value.memory_per_gpu * event.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const instanceModifyCpuonlyChange = () => {
|
||||||
|
if (!cpu_only_mode.value && serverDetail.value.gpu_num === serverDetail.value.gpu_used) {
|
||||||
|
toast.add({ severity: 'error', summary: '无法调整显卡资源', detail: '宿主机显卡资源不足', life: 3000 });
|
||||||
|
cpu_only_mode.value = true
|
||||||
|
}
|
||||||
|
if (cpu_only_mode.value) {
|
||||||
|
instanceConfiguration.value.cpu_only = true
|
||||||
|
instanceConfiguration.value.cpu_count = 1
|
||||||
|
instanceConfiguration.value.memory = 2
|
||||||
|
} else {
|
||||||
|
instanceConfiguration.value.cpu_only = false
|
||||||
|
instanceConfiguration.value.gpu_count = instanceDetail.value.gpu_count
|
||||||
|
if (instanceConfiguration.value.gpu_count === 0) {
|
||||||
|
instanceConfiguration.value.gpu_count = 1
|
||||||
|
}
|
||||||
|
if (serverDetail.value.gpu_num - serverDetail.value.gpu_used < instanceConfiguration.value.gpu_count) {
|
||||||
|
instanceConfiguration.value.gpu_count = serverDetail.value.gpu_num - serverDetail.value.gpu_used
|
||||||
|
}
|
||||||
|
instanceConfiguration.value.cpu_count = serverDetail.value.cpu_count_per_gpu * instanceConfiguration.value.gpu_count
|
||||||
|
instanceConfiguration.value.memory = serverDetail.value.memory_per_gpu * instanceConfiguration.value.gpu_count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openInstanceDelete = () => {
|
||||||
|
confirm.require({
|
||||||
|
header: '确认删除 实例ID: ' + instanceDetail.value.id,
|
||||||
|
message: '实例删除后,数据将无法恢复,请确认删除',
|
||||||
|
icon: 'pi pi-info-circle',
|
||||||
|
rejectProps: {
|
||||||
|
label: '取消',
|
||||||
|
severity: 'secondary',
|
||||||
|
outlined: true
|
||||||
|
},
|
||||||
|
acceptProps: {
|
||||||
|
label: '删除',
|
||||||
|
severity: 'danger'
|
||||||
|
},
|
||||||
|
accept: async () => {
|
||||||
|
await instanceDelete(instanceDetail.value.id)
|
||||||
|
},
|
||||||
|
reject: () => { }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const instanceModifyLabel = (id, label) => {
|
||||||
|
api.AdminInstancesModifyLabel(id, { label: label }).then(async (res) => {
|
||||||
|
toast.add({ severity: 'success', summary: '修改备注成功', detail: '已保存备注', life: 3000 });
|
||||||
|
await getInstances()
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
toast.add({ severity: 'error', summary: '修改备注失败', detail: err.response.data.msg, life: 3000 });
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getInstances()
|
||||||
|
getImages()
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
404
frontend/src/views/admin/Servers.vue
Normal file
404
frontend/src/views/admin/Servers.vue
Normal file
@ -0,0 +1,404 @@
|
|||||||
|
<template>
|
||||||
|
<SectionBanner label="节点管理" icon="pi pi-server text-yellow-400">
|
||||||
|
<Button icon="pi pi-plus" label="添加节点" rounded @click="openServerAdd" />
|
||||||
|
<Button severity="contrast" :icon="refreshIcon" label="刷新" rounded @click="getServers" />
|
||||||
|
</SectionBanner>
|
||||||
|
|
||||||
|
<div class="card mt-7 rounded-2xl">
|
||||||
|
<DataTable :value="data" pt:header:class="font-black">
|
||||||
|
<template #empty>
|
||||||
|
<div class="text-center mt-2 mb-2 text-secondary">
|
||||||
|
未添加节点
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<Column field="id" frozen class="font-bold"></Column>
|
||||||
|
<Column header="主机 / IP" class="min-w-32">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<span class="text-primary font-bold">{{ data.name }}</span>
|
||||||
|
<br />
|
||||||
|
<span class="text-muted-color">{{ data.ip }}</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column header="规格" class="min-w-64 font-bold">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<span class="mt-2 mb-2 block">显卡:
|
||||||
|
<Tag :value="data.gpu_type" class="mr-1" rounded></Tag>
|
||||||
|
<Tag v-if="data.gpu_num > data.gpu_used" severity="success"
|
||||||
|
:value="'剩余 ' + Number(data.gpu_num - data.gpu_used) + '/' + data.gpu_num + ' 张'" rounded></Tag>
|
||||||
|
<Tag v-else severity="danger"
|
||||||
|
:value="'剩余 ' + Number(data.gpu_num - data.gpu_used) + '/' + data.gpu_num + ' 张'" rounded></Tag>
|
||||||
|
</span>
|
||||||
|
<span class="mt-2 mb-2 block">数据盘:
|
||||||
|
<Tag v-if="data.volume_total > data.volume_used"
|
||||||
|
:value="Number(data.volume_total - data.volume_used) + '/' + data.volume_total + ' GB'" rounded></Tag>
|
||||||
|
<Tag v-else severity="danger"
|
||||||
|
:value="Number(data.volume_total - data.volume_used) + '/' + data.volume_total + ' GB'" rounded></Tag>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column header="配置详情" class="min-w-32">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<div class="flex items-center gap-2 px-1">
|
||||||
|
<div class="text-surface-900 dark:text-surface-0 font-medium -ml-1 mb-1 md:mb-0">CPU:</div>
|
||||||
|
<span class="text-muted-color">{{ data.cpu_count_per_gpu }} 核/卡</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 px-1">
|
||||||
|
<div class="text-surface-900 dark:text-surface-0 font-medium -ml-1 mb-1 md:mb-0">内存:</div>
|
||||||
|
<span class="text-muted-color">{{ data.memory_per_gpu }} GB/卡</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-primary font-bold" @click="getServerDetailEvent($event, data.id)"
|
||||||
|
@mouseenter="getServerDetailEvent($event, data.id)">查看详情</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="create_at" header="创建时间"></Column>
|
||||||
|
<Column>
|
||||||
|
<template #body="{ data }">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button icon="pi pi-pencil" v-tooltip.top="'编辑'" @click="openServerModify(data)" />
|
||||||
|
<Button severity="danger" icon="pi pi-trash" v-tooltip.top="'删除'" @click="openServerDelete(data)" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
|
||||||
|
<Paginator class="ml-auto mt-4" :rows="10" :totalRecords="total" :rowsPerPageOptions="[10, 20, 30]"
|
||||||
|
@page="changePage">
|
||||||
|
<template #start>
|
||||||
|
共 {{ total }} 个
|
||||||
|
</template>
|
||||||
|
</Paginator>
|
||||||
|
|
||||||
|
<Popover ref="serverDetailRef">
|
||||||
|
<Fluid v-if="serverDetail" class="rounded flex flex-col gap-4 w-96">
|
||||||
|
<Fieldset legend="GPU 空闲/总量">
|
||||||
|
<span>{{ serverDetail.gpu_num - serverDetail.gpu_used }} / {{ serverDetail.gpu_num }} 张</span>
|
||||||
|
</Fieldset>
|
||||||
|
<Fieldset legend="数据盘 空闲/总量">
|
||||||
|
<span>{{ serverDetail.volume_total - serverDetail.volume_used }} / {{ serverDetail.volume_total }} GB</span>
|
||||||
|
</Fieldset>
|
||||||
|
<div class="flex flex-col md:flex-row gap-4">
|
||||||
|
<Fieldset class="flex flex-wrap gap-2 w-full" legend="CPU">
|
||||||
|
<span>{{ serverDetail.cpu_count_per_gpu }} 核/卡</span>
|
||||||
|
</Fieldset>
|
||||||
|
<Fieldset class="flex flex-wrap gap-2 w-full" legend="内存">
|
||||||
|
<span>{{ serverDetail.memory_per_gpu }} GB/卡</span>
|
||||||
|
</Fieldset>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col md:flex-row gap-4">
|
||||||
|
<Fieldset class="flex flex-wrap gap-2 w-full" legend="GPU驱动版本">
|
||||||
|
<span>{{ serverDetail.gpu_driver_version }}</span>
|
||||||
|
</Fieldset>
|
||||||
|
<Fieldset class="flex flex-wrap gap-2 w-full" legend="CUDA版本">
|
||||||
|
<span>≤ {{ serverDetail.gpu_cuda_version }}</span>
|
||||||
|
</Fieldset>
|
||||||
|
</div>
|
||||||
|
</Fluid>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog v-model:visible="serverModifyVisible" modal :header="'编辑节点 - ' + serverDetail.name"
|
||||||
|
:style="{ width: '42rem' }">
|
||||||
|
<span class="text-surface-500 dark:text-surface-400 block mb-6">编辑节点配置</span>
|
||||||
|
<div class="flex items-center gap-0 mb-4">
|
||||||
|
<label class="font-semibold w-20">名称:</label>
|
||||||
|
<InputText v-model="serverDetail.name" class="flex-auto" type="text" />
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 md:flex-row gap-8 mb-4">
|
||||||
|
<div class="flex gap-0">
|
||||||
|
<label class="font-semibold w-28 mt-2">IP:</label>
|
||||||
|
<InputGroup class="flex-auto">
|
||||||
|
<InputText v-model="serverDetail.ip" type="text" />
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-0">
|
||||||
|
<label class="font-semibold w-28 mt-2">端口:</label>
|
||||||
|
<InputGroup class="flex-auto">
|
||||||
|
<InputNumber v-model="serverDetail.port" :useGrouping="false" :min="1" :max="65535" />
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 md:flex-row gap-8 mb-4">
|
||||||
|
<div class="flex gap-0">
|
||||||
|
<label class="font-semibold w-28 mt-2">APIKEY:</label>
|
||||||
|
<InputGroup class="flex-auto">
|
||||||
|
<InputText v-model="serverDetail.apikey" />
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-0">
|
||||||
|
<label class="font-semibold w-28 mt-2">CPU:</label>
|
||||||
|
<InputGroup class="flex-auto">
|
||||||
|
<InputNumber v-model="serverDetail.cpu_count_per_gpu" :useGrouping="false" :min="1" />
|
||||||
|
<InputGroupAddon>核 / 卡</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 md:flex-row gap-8 mb-4">
|
||||||
|
<div class="flex gap-0">
|
||||||
|
<label class="font-semibold w-28 mt-2">内存:</label>
|
||||||
|
<InputGroup class="flex-auto">
|
||||||
|
<InputNumber v-model="serverDetail.memory_per_gpu" :useGrouping="false" :min="1" />
|
||||||
|
<InputGroupAddon>GB / 卡</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-0">
|
||||||
|
<label class="font-semibold w-28 mt-2">数据盘:</label>
|
||||||
|
<InputGroup class="flex-auto">
|
||||||
|
<InputNumber v-model="serverDetail.volume_total" :useGrouping="false" :min="1" />
|
||||||
|
<InputGroupAddon>GB</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 md:flex-row gap-8 mb-4">
|
||||||
|
<div class="flex gap-0">
|
||||||
|
<label class="font-semibold w-28 mt-2">GPU 类型:</label>
|
||||||
|
<InputGroup class="flex-auto">
|
||||||
|
<InputText v-model="serverDetail.gpu_type" />
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-0">
|
||||||
|
<label class="font-semibold w-28 mt-2">GPU 数量:</label>
|
||||||
|
<InputGroup class="flex-auto">
|
||||||
|
<InputNumber v-model="serverDetail.gpu_num" :useGrouping="false" :min="0" />
|
||||||
|
<InputGroupAddon>张</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 md:flex-row gap-8 mb-4">
|
||||||
|
<div class="flex gap-0">
|
||||||
|
<label class="font-semibold w-28 mt-2">驱动版本:</label>
|
||||||
|
<InputGroup>
|
||||||
|
<InputText v-model="serverDetail.gpu_driver_version" class="flex-auto" type="text" />
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-0">
|
||||||
|
<label class="font-semibold w-28 mt-2">CUDA:</label>
|
||||||
|
<InputGroup>
|
||||||
|
<InputText v-model="serverDetail.gpu_cuda_version" class="flex-auto" />
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<Button type="button" label="取消" severity="secondary" @click="serverModifyVisible = false"></Button>
|
||||||
|
<Button type="button" label="保存" @click="serverModify"></Button>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog v-model:visible="serverAddVisible" modal :header="'添加节点'" :style="{ width: '42rem' }">
|
||||||
|
<span class="text-surface-500 dark:text-surface-400 block mb-6">添加节点配置</span>
|
||||||
|
<div class="flex items-center gap-0 mb-4">
|
||||||
|
<label class="font-semibold w-20">名称:</label>
|
||||||
|
<InputText v-model="serverDetail.name" class="flex-auto" type="text" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col md:flex-row gap-8 mb-4">
|
||||||
|
<div class="flex gap-0">
|
||||||
|
<label class="font-semibold w-28 mt-2">IP:</label>
|
||||||
|
<InputGroup class="flex-auto">
|
||||||
|
<InputText v-model="serverDetail.ip" type="text" />
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-0">
|
||||||
|
<label class="font-semibold w-28 mt-2">端口:</label>
|
||||||
|
<InputGroup class="flex-auto">
|
||||||
|
<InputNumber v-model="serverDetail.port" :useGrouping="false" :min="1" :max="65535" />
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 md:flex-row gap-8 mb-4">
|
||||||
|
<div class="flex gap-0">
|
||||||
|
<label class="font-semibold w-28 mt-2">APIKEY:</label>
|
||||||
|
<InputGroup class="flex-auto">
|
||||||
|
<InputText v-model="serverDetail.apikey" />
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-0">
|
||||||
|
<label class="font-semibold w-28 mt-2">CPU:</label>
|
||||||
|
<InputGroup class="flex-auto">
|
||||||
|
<InputNumber v-model="serverDetail.cpu_count_per_gpu" :useGrouping="false" :min="1" />
|
||||||
|
<InputGroupAddon>核 / 卡</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 md:flex-row gap-8 mb-4">
|
||||||
|
<div class="flex gap-0">
|
||||||
|
<label class="font-semibold w-28 mt-2">内存:</label>
|
||||||
|
<InputGroup class="flex-auto">
|
||||||
|
<InputNumber v-model="serverDetail.memory_per_gpu" :useGrouping="false" :min="1" />
|
||||||
|
<InputGroupAddon>GB / 卡</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-0">
|
||||||
|
<label class="font-semibold w-28 mt-2">数据盘:</label>
|
||||||
|
<InputGroup class="flex-auto">
|
||||||
|
<InputNumber v-model="serverDetail.volume_total" :useGrouping="false" :min="1" />
|
||||||
|
<InputGroupAddon>GB</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 md:flex-row gap-8 mb-4">
|
||||||
|
<div class="flex gap-0">
|
||||||
|
<label class="font-semibold w-28 mt-2">GPU 类型:</label>
|
||||||
|
<InputGroup class="flex-auto">
|
||||||
|
<InputText v-model="serverDetail.gpu_type" />
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-0">
|
||||||
|
<label class="font-semibold w-28 mt-2">GPU 数量:</label>
|
||||||
|
<InputGroup class="flex-auto">
|
||||||
|
<InputNumber v-model="serverDetail.gpu_num" :useGrouping="false" :min="0" />
|
||||||
|
<InputGroupAddon>张</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 md:flex-row gap-8 mb-4">
|
||||||
|
<div class="flex gap-0">
|
||||||
|
<label class="font-semibold w-28 mt-2">驱动版本:</label>
|
||||||
|
<InputGroup>
|
||||||
|
<InputText v-model="serverDetail.gpu_driver_version" class="flex-auto" type="text" />
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-0">
|
||||||
|
<label class="font-semibold w-28 mt-2">CUDA:</label>
|
||||||
|
<InputGroup>
|
||||||
|
<InputText v-model="serverDetail.gpu_cuda_version" class="flex-auto" />
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<Button type="button" label="取消" severity="secondary" @click="serverAddVisible = false"></Button>
|
||||||
|
<Button type="button" label="添加" @click="serverAdd"></Button>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<ConfirmDialog></ConfirmDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { nextTick, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import api from '@/api';
|
||||||
|
import { formatDate } from '@/utils/time.js';
|
||||||
|
import { useConfirm } from "primevue/useconfirm";
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
const confirm = useConfirm()
|
||||||
|
|
||||||
|
const offset = ref(0)
|
||||||
|
const limit = ref(10)
|
||||||
|
|
||||||
|
const data = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
|
||||||
|
const serverDetailRef = ref(null)
|
||||||
|
const serverDetail = ref({})
|
||||||
|
const serverAddVisible = ref(false)
|
||||||
|
const serverModifyVisible = ref(false)
|
||||||
|
|
||||||
|
const refreshIcon = ref('pi pi-refresh')
|
||||||
|
|
||||||
|
const changePage = async (event) => {
|
||||||
|
limit.value = event.rows
|
||||||
|
offset.value = event.first
|
||||||
|
await getServers()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getServers = async () => {
|
||||||
|
refreshIcon.value = 'pi pi-spin pi-refresh'
|
||||||
|
await api.AdminServersList({ offset: offset.value, limit: limit.value }).then(res => {
|
||||||
|
data.value = res.data.data.result.map(item => {
|
||||||
|
item.create_at = formatDate(item.create_at)
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
total.value = res.data.data.total || 0
|
||||||
|
}).catch(err => {
|
||||||
|
toast.add({ severity: 'error', summary: '获取节点列表失败', detail: err.response.data.msg, life: 3000 })
|
||||||
|
console.error(err)
|
||||||
|
}).finally(() => {
|
||||||
|
refreshIcon.value = 'pi pi-refresh'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getServerDetail = async (serverId) => {
|
||||||
|
await api.AdminServersDetail(serverId).then(res => {
|
||||||
|
serverDetail.value = res.data.data.result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getServerDetailEvent = (event, serverId) => {
|
||||||
|
serverDetailRef.value.hide()
|
||||||
|
|
||||||
|
if (serverDetail.value?.id === serverId) {
|
||||||
|
serverDetail.value = null
|
||||||
|
} else {
|
||||||
|
nextTick(() => {
|
||||||
|
serverDetailRef.value.show(event)
|
||||||
|
});
|
||||||
|
getServerDetail(serverId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openServerAdd = () => {
|
||||||
|
serverDetail.value = {}
|
||||||
|
serverAddVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverAdd = async () => {
|
||||||
|
await api.AdminServersAdd(serverDetail.value).then(res => {
|
||||||
|
toast.add({ severity: 'success', summary: '节点添加成功', life: 3000 })
|
||||||
|
serverAddVisible.value = false
|
||||||
|
getServers()
|
||||||
|
}).catch(err => {
|
||||||
|
toast.add({ severity: 'error', summary: '节点添加失败', detail: err.response.data.msg, life: 3000 })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const openServerModify = (data) => {
|
||||||
|
serverDetail.value = data
|
||||||
|
serverModifyVisible.value = true
|
||||||
|
getServerDetail(data.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverModify = async () => {
|
||||||
|
await api.AdminServersModify(serverDetail.value.id, serverDetail.value).then(res => {
|
||||||
|
toast.add({ severity: 'success', summary: '节点编辑成功', life: 3000 })
|
||||||
|
serverModifyVisible.value = false
|
||||||
|
getServers()
|
||||||
|
}).catch(err => {
|
||||||
|
toast.add({ severity: 'error', summary: '节点编辑失败', detail: err.response.data.msg, life: 3000 })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const openServerDelete = (data) => {
|
||||||
|
confirm.require({
|
||||||
|
header: '删除节点 - ' + data.name,
|
||||||
|
message: '确定删除节点 ' + data.name + '(ID: ' + data.id + ') 吗?',
|
||||||
|
icon: 'pi pi-exclamation-triangle',
|
||||||
|
rejectProps: {
|
||||||
|
label: '取消',
|
||||||
|
severity: 'secondary',
|
||||||
|
outlined: true
|
||||||
|
},
|
||||||
|
acceptProps: {
|
||||||
|
label: '删除',
|
||||||
|
severity: 'danger'
|
||||||
|
},
|
||||||
|
accept: () => {
|
||||||
|
serverDelete(data.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverDelete = async (id) => {
|
||||||
|
await api.AdminServersDelete(id).then(res => {
|
||||||
|
toast.add({ severity: 'success', summary: '节点删除成功', life: 3000 })
|
||||||
|
getServers()
|
||||||
|
}).catch(err => {
|
||||||
|
toast.add({ severity: 'error', summary: '节点删除失败', detail: err.response.data.msg, life: 3000 })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getServers()
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
186
frontend/src/views/admin/Users.vue
Normal file
186
frontend/src/views/admin/Users.vue
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
<template>
|
||||||
|
<SectionBanner label="用户管理" icon="pi pi-users text-indigo-500"></SectionBanner>
|
||||||
|
|
||||||
|
<div class="card mt-7 rounded-2xl">
|
||||||
|
<DataTable :value="data" pt:header:class="font-black">
|
||||||
|
<template #empty>
|
||||||
|
<div class="text-center mt-2 mb-2 text-secondary">
|
||||||
|
暂无用户
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<Column field="id" frozen class="font-bold"></Column>
|
||||||
|
<Column header="用户名" field="username" class="min-w-28 text-primary font-bold"></Column>
|
||||||
|
<Column header="邮箱" field="email" class="min-w-40 font-bold"></Column>
|
||||||
|
<Column v-if="!isAdmin" header="权限" class="min-w-28 font-bold">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<Tag v-if="data.role == 0" severity="secondary" value="受限用户" @click="userVerify(data.id)"
|
||||||
|
v-tooltip.top="'点击授权'" />
|
||||||
|
<Tag v-else-if="data.role == 1" value="普通用户" />
|
||||||
|
<Tag v-else-if="data.role == 2" severity="warn" value="管理员" />
|
||||||
|
<Tag v-else-if="data.role == 3" severity="danger" value="超级管理员" />
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="create_at" header="创建时间"></Column>
|
||||||
|
<Column v-if="!isAdmin">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button icon="pi pi-pencil" v-tooltip.top="'编辑'" @click="openUserModify(data)" />
|
||||||
|
<Button severity="danger" icon="pi pi-trash" v-tooltip.top="'删除'" @click="openUserDelete(data)" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
|
||||||
|
<Paginator class="ml-auto mt-4" :rows="10" :totalRecords="total" :rowsPerPageOptions="[10, 20, 30]"
|
||||||
|
@page="changePage">
|
||||||
|
<template #start>
|
||||||
|
共 {{ total }} 个
|
||||||
|
</template>
|
||||||
|
</Paginator>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog v-model:visible="userModifyVisible" modal :header="'编辑用户 - ' + userData.username" :style="{ width: '25rem' }">
|
||||||
|
<span class="text-surface-500 dark:text-surface-400 block mb-6">编辑用户信息</span>
|
||||||
|
<div class="flex items-center gap-0 mb-4">
|
||||||
|
<label class="font-semibold w-20">邮箱:</label>
|
||||||
|
<InputText v-model="userData.email" class="flex-auto" type="text" disabled />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-0 mb-4">
|
||||||
|
<label class="font-semibold w-20">密码:</label>
|
||||||
|
<InputText v-model="userData.password" class="flex-auto" type="text" placeholder="留空不修改密码" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-0 mb-4">
|
||||||
|
<label class="font-semibold w-20">权限:</label>
|
||||||
|
<!-- <InputText v-model="userData.role" class="flex-auto" type="text" /> -->
|
||||||
|
<Select v-model="userData.role" class="flex-auto" :options="permissionOptions" optionLabel="label"
|
||||||
|
optionValue="value" />
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<Button type="button" label="取消" severity="secondary" @click="userModifyVisible = false"></Button>
|
||||||
|
<Button type="button" label="保存" @click="userModify"></Button>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<ConfirmDialog></ConfirmDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import api from '@/api';
|
||||||
|
import { useProfileStore } from '@/stores/profile';
|
||||||
|
import { useConfirm } from "primevue/useconfirm";
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
const confirm = useConfirm()
|
||||||
|
const profileStore = useProfileStore();
|
||||||
|
|
||||||
|
const isAdmin = ref(profileStore.isAdmin)
|
||||||
|
|
||||||
|
const offset = ref(0)
|
||||||
|
const limit = ref(10)
|
||||||
|
|
||||||
|
const data = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
|
||||||
|
const userModifyVisible = ref(false)
|
||||||
|
const userData = ref({})
|
||||||
|
|
||||||
|
const permissionOptions = ref([
|
||||||
|
{ label: '受限用户', value: 0 },
|
||||||
|
{ label: '普通用户', value: 1 },
|
||||||
|
{ label: '管理员', value: 2 },
|
||||||
|
{ label: '超级管理员', value: 3 }
|
||||||
|
])
|
||||||
|
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
|
||||||
|
// 自定义格式化输出 (YYYY-MM-DD HH:mm:ss)
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0') // 月份从 0 开始
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||||
|
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const changePage = (event) => {
|
||||||
|
offset.value = event.first
|
||||||
|
limit.value = event.rows
|
||||||
|
getUsers()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUsers = async () => {
|
||||||
|
await api.AdminUserList({ offset: offset.value, limit: limit.value }).then(res => {
|
||||||
|
data.value = res.data.data.result
|
||||||
|
total.value = res.data.data.total
|
||||||
|
for (let i = 0; i < data.value.length; i++) {
|
||||||
|
data.value[i].create_at = formatDate(data.value[i].create_at)
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
toast.add({ severity: 'error', summary: '获取用户列表失败', detail: err.response.data.msg, life: 3000 })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const userVerify = async (id) => {
|
||||||
|
await api.AdminUserModify(id, { 'role': 1 }).then(res => {
|
||||||
|
toast.add({ severity: 'success', summary: '用户授权成功', life: 3000 })
|
||||||
|
getUsers()
|
||||||
|
}).catch(err => {
|
||||||
|
toast.add({ severity: 'error', summary: '用户授权失败', detail: err.response.data.msg, life: 3000 })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const openUserModify = (data) => {
|
||||||
|
userData.value = data
|
||||||
|
userModifyVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const userModify = async () => {
|
||||||
|
await api.AdminUserModify(userData.value.id, userData.value).then(res => {
|
||||||
|
toast.add({ severity: 'success', summary: '用户编辑成功', life: 3000 })
|
||||||
|
userModifyVisible.value = false
|
||||||
|
getUsers()
|
||||||
|
}).catch(err => {
|
||||||
|
toast.add({ severity: 'error', summary: '用户编辑失败', detail: err.response.data.msg, life: 3000 })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const openUserDelete = (data) => {
|
||||||
|
confirm.require({
|
||||||
|
header: '删除用户 - ' + data.username,
|
||||||
|
message: '确定删除用户 ' + data.username + '(ID: ' + data.id + ') 吗?',
|
||||||
|
icon: 'pi pi-exclamation-triangle',
|
||||||
|
rejectProps: {
|
||||||
|
label: '取消',
|
||||||
|
severity: 'secondary',
|
||||||
|
outlined: true
|
||||||
|
},
|
||||||
|
acceptProps: {
|
||||||
|
label: '删除',
|
||||||
|
severity: 'danger'
|
||||||
|
},
|
||||||
|
accept: () => {
|
||||||
|
userDelete(data.id)
|
||||||
|
},
|
||||||
|
reject: () => { }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const userDelete = async (id) => {
|
||||||
|
await api.AdminUserDelete(id).then(res => {
|
||||||
|
toast.add({ severity: 'success', summary: '用户删除成功', life: 3000 })
|
||||||
|
getUsers()
|
||||||
|
}).catch(err => {
|
||||||
|
toast.add({ severity: 'error', summary: '用户删除失败', detail: err.response.data.msg, life: 3000 })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getUsers()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
282
frontend/src/views/users/InstanceCreate.vue
Normal file
282
frontend/src/views/users/InstanceCreate.vue
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
<template>
|
||||||
|
<SectionBanner label="创建实例" icon="pi pi-plus text-purple-600">
|
||||||
|
<Button severity="contrast" :icon="refreshIcon" label="刷新" rounded @click="getServersList" />
|
||||||
|
</SectionBanner>
|
||||||
|
|
||||||
|
<div class="card rounded-2xl">
|
||||||
|
<DataView :value="data">
|
||||||
|
<template #list="slotProps">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div v-for="(item, index) in slotProps.items" :key="index">
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-center p-6 gap-4"
|
||||||
|
:class="{ 'border-t border-surface-200 dark:border-surface-700': index !== 0 }">
|
||||||
|
<div class="flex flex-col md:flex-row justify-between md:items-center flex-1 gap-6">
|
||||||
|
<div class="flex flex-row md:flex-col justify-between items-start gap-2">
|
||||||
|
<div>
|
||||||
|
<span class="font-medium text-surface-500 dark:text-surface-400 text-sm">{{ item.name }}</span>
|
||||||
|
<div class="text-lg font-medium mt-2">{{ item.gpu_type }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="item.gpu_num - item.gpu_used > 0" class="flex felx-col space-x-2">
|
||||||
|
<Tag severity="success" value="可用"></Tag>
|
||||||
|
<Tag :value="'剩余 ' + (item.gpu_num - item.gpu_used) + ' 卡'"></Tag>
|
||||||
|
<Tag severity="info" :value="item.price + ' 计点/小时/GPU'"></Tag>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex felx-col">
|
||||||
|
<Tag severity="danger" value="售罄"></Tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-3 w-[480px]">
|
||||||
|
<div class="flex flex-row md:flex-col justify-end items-start gap-2 mt-5">
|
||||||
|
<div>
|
||||||
|
<span class="font-medium text-surface-500 dark:text-surface-400 text-sm">每 GPU 分配</span>
|
||||||
|
<div class="text-sm font-medium mt-2">CPU: {{ item.cpu_count_per_gpu }} 核</div>
|
||||||
|
<div class="text-sm font-medium mt-2">内存: {{ item.memory_per_gpu }} GB</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row md:flex-col justify-end items-start gap-2 mt-5">
|
||||||
|
<div>
|
||||||
|
<span class="font-medium text-surface-500 dark:text-surface-400 text-sm">硬盘</span>
|
||||||
|
<div class="text-sm font-medium mt-2">数据盘: 50 GB</div>
|
||||||
|
<div class="text-sm font-medium mt-2">
|
||||||
|
可扩容: {{ item.volume_total - item.volume_used }} GB
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row md:flex-col justify-end items-start gap-2 mt-5">
|
||||||
|
<div>
|
||||||
|
<span class="font-medium text-surface-500 dark:text-surface-400 text-sm">其他</span>
|
||||||
|
<div class="text-sm font-medium mt-2">GPU驱动版本: {{ item.gpu_driver_version }}</div>
|
||||||
|
<div class="text-sm font-medium mt-2">CUDA版本: ≤ {{ item.gpu_cuda_version }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col md:items-end gap-8">
|
||||||
|
<span class="text-xl font-medium mt-2">
|
||||||
|
空闲GPU:
|
||||||
|
<span class="!font-extrabold text-rose-500">
|
||||||
|
{{ item.gpu_num - item.gpu_used }}
|
||||||
|
</span>
|
||||||
|
/ {{ item.gpu_num }}
|
||||||
|
</span>
|
||||||
|
<div class="flex flex-row-reverse md:flex-row gap-2 -mt-2">
|
||||||
|
<Button icon="pi pi-plus" label="创建实例" :disabled="item.disabled" @click="clickCreate($event, item)"
|
||||||
|
class="flex-auto md:flex-initial whitespace-nowrap"></Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</DataView>
|
||||||
|
|
||||||
|
<Paginator class="ml-auto mt-4" :rows="10" :totalRecords="total" :rowsPerPageOptions="[10, 20, 30]"
|
||||||
|
@page="changePage">
|
||||||
|
<template #start>
|
||||||
|
共 {{ total }} 个
|
||||||
|
</template>
|
||||||
|
</Paginator>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Drawer v-model:visible="instanceCreateVisible" header="创建实例" position="right" :showCloseIcon="false" class="!w-96">
|
||||||
|
<Fluid class="flex flex-col gap-4 w-full">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label>主机名称</label>
|
||||||
|
<InputText v-model="selectServer.name" type="text" disabled />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label>GPU类型</label>
|
||||||
|
<InputText v-model="selectServer.gpu_type" type="text" disabled />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label>CPU 核数</label>
|
||||||
|
<InputGroup>
|
||||||
|
<InputNumber v-model="instanceCreateForm.cpu_count" disabled />
|
||||||
|
<InputGroupAddon>核</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label>内存</label>
|
||||||
|
<InputGroup>
|
||||||
|
<InputNumber v-model="instanceCreateForm.memory" disabled />
|
||||||
|
<InputGroupAddon>GB</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label>GPU 数量</label>
|
||||||
|
<SelectButton v-model="instanceCreateForm.gpu_count" :options="instanceCreateForm.options"
|
||||||
|
optionDisabled="disabled" optionLabel="label" optionValue="value" aria-labelledby="basic" :allowEmpty="false"
|
||||||
|
@change="instanceCreateGpuChange" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label>数据盘</label>
|
||||||
|
<InputNumber v-model="instanceCreateForm.volume_size" showButtons buttonLayout="horizontal" :step="1"
|
||||||
|
suffix=" GB" :min="50" :max="selectServer.volume_total - selectServer.volume_used">
|
||||||
|
<template #incrementbuttonicon>
|
||||||
|
<span class="pi pi-plus" />
|
||||||
|
</template>
|
||||||
|
<template #decrementbuttonicon>
|
||||||
|
<span class="pi pi-minus" />
|
||||||
|
</template>
|
||||||
|
</InputNumber>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label>镜像</label>
|
||||||
|
<Select v-model="instanceCreateForm.image_name" :options="imagesList" placeholder="请选择镜像" class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label>GPU 驱动版本</label>
|
||||||
|
<InputText v-model="selectServer.gpu_driver_version" type="text" disabled />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label>CUDA 版本</label>
|
||||||
|
<InputText v-model="selectServer.gpu_cuda_version" type="text" disabled />
|
||||||
|
</div>
|
||||||
|
</Fluid>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex items-center gap-4 w-52 ml-auto">
|
||||||
|
<Button label="取消" class="flex-auto" severity="secondary" @click="instanceCreateVisible = false"></Button>
|
||||||
|
<Button label="确定" class="flex-auto" @click="instanceCreate"></Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import SectionBanner from '@/components/SectionBanner.vue';
|
||||||
|
|
||||||
|
import api from '@/api';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const instanceCreateRef = ref(null)
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const offset = ref(0)
|
||||||
|
const limit = ref(10)
|
||||||
|
|
||||||
|
const data = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
|
||||||
|
const imagesMap = ref({})
|
||||||
|
const imagesList = ref([])
|
||||||
|
|
||||||
|
const refreshIcon = ref('pi pi-refresh')
|
||||||
|
|
||||||
|
const instanceCreateVisible = ref(false)
|
||||||
|
const selectServer = ref({
|
||||||
|
price: 0,
|
||||||
|
price_volume: 0
|
||||||
|
})
|
||||||
|
const instanceCreateForm = ref({
|
||||||
|
server_id: 0,
|
||||||
|
image_name: '',
|
||||||
|
gpu_count: 1,
|
||||||
|
volume_size: 50
|
||||||
|
})
|
||||||
|
|
||||||
|
const changePage = async (event) => {
|
||||||
|
limit.value = event.rows
|
||||||
|
offset.value = event.first
|
||||||
|
await getServersList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getServersList = async () => {
|
||||||
|
refreshIcon.value = 'pi pi-spin pi-refresh'
|
||||||
|
await api.UserServerList({ offset: offset.value, limit: limit.value }).then((res) => {
|
||||||
|
data.value = res.data.data.result
|
||||||
|
for (let i = 0; i < data.value.length; i++) {
|
||||||
|
data.value[i].key = i
|
||||||
|
}
|
||||||
|
total.value = res.data.data.total
|
||||||
|
for (let i = 0; i < data.value.length; i++) {
|
||||||
|
if (data.value[i].gpu_used >= data.value[i].gpu_num) {
|
||||||
|
data.value[i].disabled = true
|
||||||
|
} else {
|
||||||
|
data.value[i].disabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refreshIcon.value = 'pi pi-refresh'
|
||||||
|
}).catch((err) => {
|
||||||
|
toast.add({ severity: 'error', summary: '获取主机列表失败', detail: err.response.data.msg, life: 3000 })
|
||||||
|
refreshIcon.value = 'pi pi-refresh'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getImages = async () => {
|
||||||
|
await api.UserImages().then((res) => {
|
||||||
|
imagesMap.value = res.data.data.result
|
||||||
|
imagesList.value = Object.keys(imagesMap.value).map(key => (key)).sort()
|
||||||
|
}).catch((err) => {
|
||||||
|
toast.add({ severity: 'error', summary: '获取镜像列表失败', detail: err.response.data.msg, life: 3000 })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const clickCreate = (event, record) => {
|
||||||
|
selectServer.value = record
|
||||||
|
instanceCreateForm.value = {
|
||||||
|
server_id: selectServer.value.id,
|
||||||
|
image_name: '',
|
||||||
|
gpu_count: 1,
|
||||||
|
volume_size: 50,
|
||||||
|
cpu_count: selectServer.value.cpu_count_per_gpu,
|
||||||
|
memory: selectServer.value.memory_per_gpu
|
||||||
|
}
|
||||||
|
instanceCreateForm.value.options = []
|
||||||
|
for (let i = 0; i < selectServer.value.gpu_num; i++) {
|
||||||
|
const tmp = {
|
||||||
|
label: i + 1,
|
||||||
|
value: i + 1,
|
||||||
|
disabled: i + 1 > selectServer.value.gpu_num - selectServer.value.gpu_used
|
||||||
|
}
|
||||||
|
instanceCreateForm.value.options.push(tmp)
|
||||||
|
}
|
||||||
|
console.log(selectServer.value)
|
||||||
|
|
||||||
|
instanceCreateVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const instanceCreateGpuChange = (event) => {
|
||||||
|
instanceCreateForm.value.cpu_count = selectServer.value.cpu_count_per_gpu * event.value
|
||||||
|
instanceCreateForm.value.memory = selectServer.value.memory_per_gpu * event.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const instanceCreate = async () => {
|
||||||
|
// console.log(instanceCreateForm.value)
|
||||||
|
if (instanceCreateForm.value.server_id === 0 || instanceCreateForm.value.server_id === null || instanceCreateForm.value.server_id === undefined) {
|
||||||
|
toast.add({ severity: 'error', summary: '请选择主机', life: 3000 })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (instanceCreateForm.value.volume_size < 0) {
|
||||||
|
toast.add({ severity: 'error', summary: '数据盘大小不能小于0', life: 3000 })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (instanceCreateForm.value.image_name === '') {
|
||||||
|
toast.add({ severity: 'error', summary: '请选择镜像', life: 3000 })
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
instanceCreateForm.value.image_name = imagesMap.value[instanceCreateForm.value.image_name]
|
||||||
|
}
|
||||||
|
|
||||||
|
api.UserInstancesCreate(instanceCreateForm.value).then((res) => {
|
||||||
|
toast.add({ severity: 'success', summary: '创建成功', detail: '实例创建成功', life: 3000 })
|
||||||
|
instanceCreateVisible.value = false
|
||||||
|
router.push('/instances')
|
||||||
|
}).catch((error) => {
|
||||||
|
toast.add({ severity: 'error', summary: '创建失败', detail: error.response.data.msg, life: 3000 })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await getServersList()
|
||||||
|
await getImages()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
611
frontend/src/views/users/InstanceList.vue
Normal file
611
frontend/src/views/users/InstanceList.vue
Normal file
@ -0,0 +1,611 @@
|
|||||||
|
<template>
|
||||||
|
<SectionBanner label="实例列表" icon="pi pi-desktop text-blue-600">
|
||||||
|
<Button icon="pi pi-plus" label="创建实例" rounded @click="router.push('/instances/create')" />
|
||||||
|
<Button severity="contrast" :icon="refreshIcon" label="刷新" rounded @click="getInstances" />
|
||||||
|
</SectionBanner>
|
||||||
|
|
||||||
|
<Message icon="pi pi-info-circle" closable pt:content:class="bg-primary text-white rounded" pt:icon:class="m-3 mr-0"
|
||||||
|
pt:text:class="m-3">
|
||||||
|
请勿长时间占用实例,使用完毕请及时释放
|
||||||
|
</Message>
|
||||||
|
|
||||||
|
<div class="card mt-7 rounded-2xl">
|
||||||
|
<DataTable :value="data" pt:header:class="font-black">
|
||||||
|
<template #empty>
|
||||||
|
<div class="text-center mt-2 mb-2 text-secondary">
|
||||||
|
未创建实例
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<Column field="id" frozen class="font-bold"></Column>
|
||||||
|
<Column header="主机名称" class="min-w-40">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<span class="text-primary font-bold" @click="getServerDetailEvent($event, data.server_id)"
|
||||||
|
@mouseenter="getServerDetailEvent($event, data.server_id)">{{ data.server_name }}</span>
|
||||||
|
<br />
|
||||||
|
<EditLabel v-model="data.label" class="mt-1" emptyText="设置备注"
|
||||||
|
:save="(label) => instanceModifyLabel(data.id, label)" />
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column header="状态" class="min-w-28 font-bold">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<Tag v-if="data.status == statusRunning" icon="pi pi-play" value="运行中" />
|
||||||
|
<Tag v-else-if="data.status == statusPaused" severity="warn" icon="pi pi-pause" value="暂停" />
|
||||||
|
<Tag v-else-if="data.status == statusStoped" severity="secondary" icon="pi pi-stop-circle" value="已停止" />
|
||||||
|
|
||||||
|
<Tag v-else-if="data.status == statusReady" severity="success" icon="pi pi-spin pi-spinner" value="准备中" />
|
||||||
|
<Tag v-else-if="data.status == statusStarting" severity="info" icon="pi pi-spin pi-spinner" value="启动中" />
|
||||||
|
<Tag v-else-if="data.status == statusStopping" severity="info" icon="pi pi-spin pi-spinner" value="停止中" />
|
||||||
|
<Tag v-else-if="data.status == statusPausing" severity="info" icon="pi pi-spin pi-spinner" value="暂停中" />
|
||||||
|
<Tag v-else-if="data.status == statusRestarting" severity="info" icon="pi pi-spin pi-spinner" value="重启中" />
|
||||||
|
<Tag v-else-if="data.status == statusModifying" severity="success" icon="pi pi-spin pi-spinner" value="调整中" />
|
||||||
|
<Tag v-else-if="data.status == statusDeleting" severity="danger" icon="pi pi-spin pi-spinner" value="删除中" />
|
||||||
|
|
||||||
|
<Tag v-else-if="data.status == statusFail" severity="danger" icon="pi pi-exclamation-triangle" value="错误" />
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column header="规格详情" class="min-w-36 font-bold">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<span v-if="data.gpu_count !== 0">{{ data.gpu_type }} * {{ data.gpu_count }}</span>
|
||||||
|
<span v-else>无卡模式</span>
|
||||||
|
<br />
|
||||||
|
<span class="text-primary" @click="getInstanceDetail($event, data)"
|
||||||
|
@mouseenter="getInstanceDetail($event, data)">查看详情</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column header="SSH 信息">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<span class="text-surface-900 dark:text-surface-0 font-medium mr-2 mb-1 md:mb-0">地址:</span>
|
||||||
|
<div v-if="data.status === statusRunning" class="mt-1 text-muted-color">{{ data.ssh_address }}
|
||||||
|
<CopyIcon class="ml-1" :text="data.ssh_address" />
|
||||||
|
</div>
|
||||||
|
<div v-else class="mt-1 text-muted-color">暂无</div>
|
||||||
|
<span class="text-surface-900 dark:text-surface-0 font-medium mr-2 mb-1 md:mb-0">密码:</span>
|
||||||
|
<div v-if="data.status === statusRunning" class="mt-1 text-muted-color">********
|
||||||
|
<CopyIcon class="ml-1" :text="data.ssh_passwd" />
|
||||||
|
</div>
|
||||||
|
<div v-else class="mt-1 text-muted-color">暂无</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="create_at" header="创建时间"></Column>
|
||||||
|
<Column>
|
||||||
|
<template #body="{ data }">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button v-if="data.status == statusRunning" icon="pi pi-inbox" aria-label="Filter" as="a"
|
||||||
|
:href="'http://' + data.jupyter_address + '/lab'" target="_blank" v-tooltip.top="'Jupter Lab'" />
|
||||||
|
<Button v-else icon="pi pi-inbox" aria-label="Filter" v-tooltip.top="'Jupter Lab'" disabled />
|
||||||
|
<Button v-if="data.status == statusRunning" severity="contrast" icon="pi pi-chart-bar" as="a"
|
||||||
|
:href="'http://' + data.grafana_address + '/public-dashboards/2c510f203876465ba76617510ce3e219'"
|
||||||
|
target="_blank" v-tooltip.top="'监控'" />
|
||||||
|
<Button v-else severity="contrast" icon="pi pi-chart-bar" v-tooltip.top="'监控'" disabled />
|
||||||
|
<Button icon="pi pi-ellipsis-h" severity="secondary" aria-label="Bookmark"
|
||||||
|
@click="showMenu($event, data)" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
|
||||||
|
<Paginator class="ml-auto mt-4" :rows="10" :totalRecords="total" :rowsPerPageOptions="[10, 20, 30]"
|
||||||
|
@page="changePage">
|
||||||
|
<template #start>
|
||||||
|
共 {{ total }} 个
|
||||||
|
</template>
|
||||||
|
</Paginator>
|
||||||
|
|
||||||
|
<Menu ref="instanceMenu" :model="instanceMenuItems" :popup="true" />
|
||||||
|
|
||||||
|
<Popover ref="serverDetailRef">
|
||||||
|
<Fluid v-if="serverDetail" class="rounded flex flex-col gap-4 w-96">
|
||||||
|
<Fieldset legend="主机名称">
|
||||||
|
<span>{{ serverDetail.name }}</span>
|
||||||
|
</Fieldset>
|
||||||
|
<Fieldset legend="GPU空闲/总量">
|
||||||
|
<span>{{ serverDetail.gpu_num - serverDetail.gpu_used }} / {{ serverDetail.gpu_num }}</span>
|
||||||
|
</Fieldset>
|
||||||
|
<Fieldset legend="数据盘可扩容">
|
||||||
|
<span>{{ serverDetail.volume_total - serverDetail.volume_used }} GB</span>
|
||||||
|
</Fieldset>
|
||||||
|
<div class="flex flex-col md:flex-row gap-4">
|
||||||
|
<Fieldset class="flex flex-wrap gap-2 w-full" legend="GPU驱动版本">
|
||||||
|
<span>{{ serverDetail.gpu_driver_version }}</span>
|
||||||
|
</Fieldset>
|
||||||
|
<Fieldset class="flex flex-wrap gap-2 w-full" legend="CUDA版本">
|
||||||
|
<span>≤ {{ serverDetail.gpu_cuda_version }}</span>
|
||||||
|
</Fieldset>
|
||||||
|
</div>
|
||||||
|
</Fluid>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<Popover ref="instanceDetailRef">
|
||||||
|
<Fluid v-if="instanceDetail" class="rounded flex flex-col gap-4 w-full">
|
||||||
|
<Fieldset legend="镜像">
|
||||||
|
<span>{{ imagesValueMap[instanceDetail.image_name] || instanceDetail.image_name }}</span>
|
||||||
|
</Fieldset>
|
||||||
|
<Fieldset legend="GPU">
|
||||||
|
<span v-if="instanceDetail.gpu_count !== 0">{{ instanceDetail.gpu_type }} * {{ instanceDetail.gpu_count
|
||||||
|
}}</span>
|
||||||
|
<span v-else>无卡模式</span>
|
||||||
|
</Fieldset>
|
||||||
|
<div class="flex flex-col md:flex-row gap-4">
|
||||||
|
<Fieldset class="flex flex-wrap gap-2 w-full" legend="CPU">
|
||||||
|
<span v-if="instanceDetail.gpu_count !== 0">{{ instanceDetail.cpu_count_per_gpu * instanceDetail.gpu_count
|
||||||
|
}}
|
||||||
|
核</span>
|
||||||
|
<span v-else>1 核</span>
|
||||||
|
</Fieldset>
|
||||||
|
<Fieldset class="flex flex-wrap gap-2 w-full" legend="内存">
|
||||||
|
<span v-if="instanceDetail.gpu_count !== 0">{{ instanceDetail.memory_per_gpu * instanceDetail.gpu_count }}
|
||||||
|
GB</span>
|
||||||
|
<span v-else>2 GB</span>
|
||||||
|
</Fieldset>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col md:flex-row gap-4">
|
||||||
|
<Fieldset class="flex flex-wrap gap-2 w-full" legend="系统盘">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<span>30G</span>
|
||||||
|
<i class="pi pi-info-circle mt-1" v-tooltip.top="'本地盘,快速'"></i>
|
||||||
|
</div>
|
||||||
|
</Fieldset>
|
||||||
|
<Fieldset class="flex flex-wrap gap-2 w-full" legend="数据盘">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<span> 50 + {{ instanceDetail.volume_size - 50 }} GB</span>
|
||||||
|
<i class="pi pi-info-circle mt-1" v-tooltip.top="'本地盘,快速,免费 50G,可扩容/缩容'"></i>
|
||||||
|
</div>
|
||||||
|
</Fieldset>
|
||||||
|
</div>
|
||||||
|
</Fluid>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Drawer v-model:visible="instanceModifyVisible" header="修改实例配置" position="right" :dismissable="false"
|
||||||
|
:showCloseIcon="false" class="!w-96">
|
||||||
|
<Fluid class="flex flex-col gap-4 w-full">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label>实例 ID</label>
|
||||||
|
<InputText v-model="instanceDetail.id" type="text" disabled />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label>主机名称</label>
|
||||||
|
<InputText v-model="instanceDetail.server_name" type="text" disabled />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label>GPU类型</label>
|
||||||
|
<InputText v-model="serverDetail.gpu_type" type="text" disabled />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label>CPU 核数</label>
|
||||||
|
<InputGroup>
|
||||||
|
<InputNumber v-model="instanceConfiguration.cpu_count" disabled />
|
||||||
|
<InputGroupAddon>核</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label>内存</label>
|
||||||
|
<InputGroup>
|
||||||
|
<InputNumber v-model="instanceConfiguration.memory" disabled />
|
||||||
|
<InputGroupAddon>GB</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label>无卡模式</label>
|
||||||
|
<ToggleSwitch v-model="cpu_only_mode" @change="instanceModifyCpuonlyChange" />
|
||||||
|
</div>
|
||||||
|
<div v-show="!instanceConfiguration.cpu_only" class="flex flex-col gap-2">
|
||||||
|
<label>GPU 数量</label>
|
||||||
|
<SelectButton v-model="instanceConfiguration.gpu_count" :options="instanceConfiguration.options"
|
||||||
|
optionDisabled="disabled" optionLabel="label" optionValue="value" aria-labelledby="basic" :allowEmpty="false"
|
||||||
|
@change="instanceModifyGpuChange" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label>数据盘</label>
|
||||||
|
<InputNumber v-model="instanceConfiguration.volume_size" showButtons buttonLayout="horizontal" :step="1"
|
||||||
|
suffix=" GB" :min="50"
|
||||||
|
:max="serverDetail.volume_total - serverDetail.volume_used + instanceDetail.volume_size">
|
||||||
|
<template #incrementbuttonicon>
|
||||||
|
<span class="pi pi-plus" />
|
||||||
|
</template>
|
||||||
|
<template #decrementbuttonicon>
|
||||||
|
<span class="pi pi-minus" />
|
||||||
|
</template>
|
||||||
|
</InputNumber>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label>GPU 驱动版本</label>
|
||||||
|
<InputText v-model="serverDetail.gpu_driver_version" type="text" disabled />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label>CUDA 版本</label>
|
||||||
|
<InputText v-model="serverDetail.gpu_cuda_version" type="text" disabled />
|
||||||
|
</div>
|
||||||
|
</Fluid>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex items-center gap-4 w-52 ml-auto">
|
||||||
|
<Button label="取消" class="flex-auto" severity="secondary" @click="instanceModifyVisible = false"></Button>
|
||||||
|
<Button label="确定" class="flex-auto" @click="instanceModify"></Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Drawer>
|
||||||
|
|
||||||
|
<ConfirmDialog></ConfirmDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import CopyIcon from '@/components/CopyIcon.vue';
|
||||||
|
import EditLabel from '@/components/EditLabel.vue';
|
||||||
|
import SectionBanner from '@/components/SectionBanner.vue';
|
||||||
|
|
||||||
|
import api from '@/api';
|
||||||
|
import { formatDate } from '@/utils/time.js';
|
||||||
|
import { useConfirm } from "primevue/useconfirm";
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import { nextTick, onMounted, ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const statusFail = ref(-1)
|
||||||
|
const statusRunning = ref(0)
|
||||||
|
const statusPaused = ref(1)
|
||||||
|
const statusStoped = ref(2)
|
||||||
|
const statusReady = ref(3)
|
||||||
|
const statusStarting = ref(4)
|
||||||
|
const statusStopping = ref(5)
|
||||||
|
const statusPausing = ref(6)
|
||||||
|
const statusRestarting = ref(7)
|
||||||
|
const statusModifying = ref(8)
|
||||||
|
const statusDeleting = ref(9)
|
||||||
|
|
||||||
|
const statusIng = [statusReady.value, statusStarting.value, statusStopping.value, statusPausing.value, statusRestarting.value, statusModifying.value, statusDeleting.value]
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const toast = useToast()
|
||||||
|
const confirm = useConfirm()
|
||||||
|
|
||||||
|
const instanceMenu = ref(null)
|
||||||
|
const serverDetailRef = ref(null)
|
||||||
|
const serverDetail = ref(null)
|
||||||
|
const instanceDetailRef = ref(null)
|
||||||
|
const instanceDetail = ref(null)
|
||||||
|
|
||||||
|
const offset = ref(0)
|
||||||
|
const limit = ref(10)
|
||||||
|
|
||||||
|
const data = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const imagesValueMap = ref({})
|
||||||
|
|
||||||
|
const instanceModifyVisible = ref(false)
|
||||||
|
const instanceConfiguration = ref({})
|
||||||
|
const cpu_only_mode = ref(false)
|
||||||
|
|
||||||
|
const globalRefresh = ref(false)
|
||||||
|
|
||||||
|
const refreshIcon = ref('pi pi-refresh')
|
||||||
|
|
||||||
|
const instanceMenuItemsTemplate = ref([
|
||||||
|
{ label: '无卡模式开机', icon: 'pi pi-power-off !text-emerald-600', command: () => { cpuOnlyMode(instanceDetail.value.id) } },
|
||||||
|
{ label: '开机', icon: 'pi pi-play !text-blue-600', command: () => { instanceStart(instanceDetail.value.id) } },
|
||||||
|
{ label: '关机', icon: 'pi pi-stop', command: () => { instanceStop(instanceDetail.value.id) } },
|
||||||
|
{ label: '暂停', icon: 'pi pi-pause !text-amber-600', command: () => { instancePause(instanceDetail.value.id) } },
|
||||||
|
{ label: '重启实例', icon: 'pi pi-refresh !text-sky-500', command: () => { instanceRestart(instanceDetail.value.id) } },
|
||||||
|
{ label: '调整配置', icon: 'pi pi-sliders-h !text-indigo-500', command: () => { openInstanceModify() } },
|
||||||
|
{ label: '删除实例', icon: 'pi pi-trash !text-red-500', command: () => { openInstanceDelete() } },
|
||||||
|
])
|
||||||
|
const instanceMenuItems = ref([])
|
||||||
|
|
||||||
|
const changePage = async (event) => {
|
||||||
|
limit.value = event.rows
|
||||||
|
offset.value = event.first
|
||||||
|
await getInstances()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getInstances = async () => {
|
||||||
|
// data.value = []
|
||||||
|
if (globalRefresh.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
globalRefresh.value = true
|
||||||
|
refreshIcon.value = 'pi pi-spin pi-refresh'
|
||||||
|
await api.UserInstancesList({ offset: offset.value, limit: limit.value }).then(res => {
|
||||||
|
data.value = res.data.data.result
|
||||||
|
let refresh = false
|
||||||
|
for (let i = 0; i < data.value.length; i++) {
|
||||||
|
if (statusIng.indexOf(data.value[i].status) !== -1) {
|
||||||
|
refresh = true
|
||||||
|
}
|
||||||
|
data.value[i].create_at = formatDate(data.value[i].create_at)
|
||||||
|
}
|
||||||
|
total.value = res.data.data.total || 0
|
||||||
|
refreshIcon.value = 'pi pi-refresh'
|
||||||
|
if (refresh) {
|
||||||
|
setTimeout(() => {
|
||||||
|
getInstances()
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
globalRefresh.value = false
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
toast.add({ severity: 'error', summary: '获取实例列表失败', detail: err.response.data.msg, life: 3000 });
|
||||||
|
refreshIcon.value = 'pi pi-refresh'
|
||||||
|
globalRefresh.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getImages = async () => {
|
||||||
|
await api.UserImages().then((res) => {
|
||||||
|
imagesValueMap.value = Object.entries(res.data.data.result).reduce((acc, [key, value]) => {
|
||||||
|
acc[value] = key
|
||||||
|
return acc
|
||||||
|
}, {});
|
||||||
|
}).catch((error) => {
|
||||||
|
Message.error('获取镜像列表失败')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getInstanceDetail = (event, instance) => {
|
||||||
|
instanceDetailRef.value.hide()
|
||||||
|
|
||||||
|
if (instanceDetail.value?.id === instance.id) {
|
||||||
|
instanceDetail.value = null
|
||||||
|
} else {
|
||||||
|
instanceDetail.value = instance
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
instanceDetailRef.value.show(event)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getServerDetail = async (id) => {
|
||||||
|
await api.UserServerDetail(id).then(res => {
|
||||||
|
serverDetail.value = res.data.data.result
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getServerDetailEvent = (event, serverId) => {
|
||||||
|
serverDetailRef.value.hide()
|
||||||
|
|
||||||
|
if (serverDetail.value?.id === serverId) {
|
||||||
|
serverDetail.value = null
|
||||||
|
} else {
|
||||||
|
nextTick(() => {
|
||||||
|
serverDetailRef.value.show(event)
|
||||||
|
});
|
||||||
|
getServerDetail(serverId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const showMenu = (event, instance) => {
|
||||||
|
instanceDetail.value = instance
|
||||||
|
instanceMenuItems.value = []
|
||||||
|
instanceMenuItemsTemplate.value.forEach(item => {
|
||||||
|
let newItem = { ...item }
|
||||||
|
switch (item.label) {
|
||||||
|
case '无卡模式开机':
|
||||||
|
if (instanceDetail.value.cpu_only === true || instanceDetail.value.status !== statusStoped.value) {
|
||||||
|
newItem.disabled = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case '开机':
|
||||||
|
if (instanceDetail.value.status === statusRunning.value || statusIng.indexOf(instanceDetail.value.status) !== -1) {
|
||||||
|
newItem.disabled = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case '关机':
|
||||||
|
if (instanceDetail.value.status === statusStoped.value || statusIng.indexOf(instanceDetail.value.status) !== -1) {
|
||||||
|
newItem.disabled = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case '暂停':
|
||||||
|
if (instanceDetail.value.status === statusPaused.value || instanceDetail.value.status === statusStoped.value || statusIng.indexOf(instanceDetail.value.status) !== -1) {
|
||||||
|
newItem.disabled = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case '重启实例':
|
||||||
|
if (statusIng.indexOf(instanceDetail.value.status) !== -1) {
|
||||||
|
newItem.disabled = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case '调整配置':
|
||||||
|
if (instanceDetail.value.status !== statusStoped.value) {
|
||||||
|
newItem.disabled = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case '删除实例':
|
||||||
|
if (instanceDetail.value.status === statusDeleting.value) {
|
||||||
|
newItem.disabled = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
instanceMenuItems.value.push(newItem)
|
||||||
|
})
|
||||||
|
|
||||||
|
instanceMenu.value.show(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cpuOnlyMode = async (id) => {
|
||||||
|
toast.add({ severity: 'info', summary: '切换为无卡模式', detail: '正在切换为无卡模式', life: 3000 });
|
||||||
|
setTimeout(() => {
|
||||||
|
getInstances()
|
||||||
|
}, 100);
|
||||||
|
await api.UserInstancesModify(id, { cpu_only: true }).then(async (res) => {
|
||||||
|
toast.add({ severity: 'success', summary: '切换为无卡模式', detail: '已切换为无卡模式', life: 3000 });
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
toast.add({ severity: 'error', summary: '切换为无卡模式失败', detail: err.response.data.msg, life: 3000 });
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const instanceStart = async (id) => {
|
||||||
|
toast.add({ severity: 'info', summary: '开机', detail: '正在开机', life: 3000 });
|
||||||
|
setTimeout(() => {
|
||||||
|
getInstances()
|
||||||
|
}, 100);
|
||||||
|
await api.UserInstancesAction(id, { action: 1 }).then(async (res) => {
|
||||||
|
toast.add({ severity: 'success', summary: '开机', detail: '实例已开机', life: 3000 });
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
toast.add({ severity: 'error', summary: '开机失败', detail: err.response.data.msg, life: 3000 });
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const instancePause = async (id) => {
|
||||||
|
toast.add({ severity: 'info', summary: '暂停实例', detail: '正在暂停实例', life: 3000 });
|
||||||
|
setTimeout(() => {
|
||||||
|
getInstances()
|
||||||
|
}, 100);
|
||||||
|
await api.UserInstancesAction(id, { action: 2 }).then(async (res) => {
|
||||||
|
toast.add({ severity: 'success', summary: '暂停实例', detail: '实例已暂停', life: 3000 });
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
toast.add({ severity: 'error', summary: '暂停实例失败', detail: err.response.data.msg, life: 3000 });
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const instanceStop = async (id) => {
|
||||||
|
toast.add({ severity: 'info', summary: '停止实例', detail: '正在停止实例', life: 3000 });
|
||||||
|
setTimeout(() => {
|
||||||
|
getInstances()
|
||||||
|
}, 100);
|
||||||
|
await api.UserInstancesAction(id, { action: 3 }).then(async (res) => {
|
||||||
|
toast.add({ severity: 'success', summary: '停止实例', detail: '实例已停止', life: 3000 });
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
toast.add({ severity: 'error', summary: '停止实例失败', detail: err.response.data.msg, life: 3000 });
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const instanceRestart = async (id) => {
|
||||||
|
toast.add({ severity: 'info', summary: '重启实例', detail: '正在重启实例', life: 3000 });
|
||||||
|
setTimeout(() => {
|
||||||
|
getInstances()
|
||||||
|
}, 100);
|
||||||
|
await api.UserInstancesAction(id, { action: 4 }).then(async (res) => {
|
||||||
|
toast.add({ severity: 'success', summary: '重启实例', detail: '实例已重启', life: 3000 });
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
toast.add({ severity: 'error', summary: '重启实例失败', detail: err.response.data.msg, life: 3000 });
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const instanceModify = async () => {
|
||||||
|
toast.add({ severity: 'info', summary: '调整配置', detail: '正在调整配置', life: 3000 });
|
||||||
|
setTimeout(() => {
|
||||||
|
getInstances()
|
||||||
|
}, 100);
|
||||||
|
await api.UserInstancesModify(instanceDetail.value.id, instanceConfiguration.value).then(async (res) => {
|
||||||
|
toast.add({ severity: 'success', summary: '调整配置', detail: '已调整配置', life: 3000 });
|
||||||
|
instanceModifyVisible.value = false
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
toast.add({ severity: 'error', summary: '调整配置失败', detail: err.response.data.msg, life: 3000 });
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const instanceDelete = async (id) => {
|
||||||
|
toast.add({ severity: 'info', summary: '释放实例', detail: '正在释放实例', life: 3000 });
|
||||||
|
setTimeout(() => {
|
||||||
|
getInstances()
|
||||||
|
}, 100);
|
||||||
|
await api.UserInstancesDelete(id).then(async (res) => {
|
||||||
|
toast.add({ severity: 'success', summary: '释放实例', detail: '实例已释放', life: 3000 });
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
toast.add({ severity: 'error', summary: '释放实例失败', detail: err.response.data.msg, life: 3000 });
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const openInstanceModify = async () => {
|
||||||
|
instanceConfiguration.value.gpu_count = instanceDetail.value.gpu_count
|
||||||
|
instanceConfiguration.value.volume_size = instanceDetail.value.volume_size
|
||||||
|
instanceConfiguration.value.cpu_only = instanceDetail.value.cpu_only
|
||||||
|
await getServerDetail(instanceDetail.value.server_id)
|
||||||
|
instanceConfiguration.value.options = []
|
||||||
|
for (let i = 0; i < serverDetail.value.gpu_num; i++) {
|
||||||
|
const tmp = {
|
||||||
|
label: i + 1,
|
||||||
|
value: i + 1,
|
||||||
|
disabled: i + 1 > serverDetail.value.gpu_num - serverDetail.value.gpu_used
|
||||||
|
}
|
||||||
|
instanceConfiguration.value.options.push(tmp)
|
||||||
|
}
|
||||||
|
if (serverDetail.value.gpu_num - serverDetail.value.gpu_used < instanceConfiguration.value.gpu_count) {
|
||||||
|
instanceConfiguration.value.gpu_count = serverDetail.value.gpu_num - serverDetail.value.gpu_used
|
||||||
|
}
|
||||||
|
instanceConfiguration.value.cpu_count = serverDetail.value.cpu_count_per_gpu * instanceConfiguration.value.gpu_count
|
||||||
|
instanceConfiguration.value.memory = serverDetail.value.memory_per_gpu * instanceConfiguration.value.gpu_count
|
||||||
|
if (serverDetail.value.gpu_num === serverDetail.value.gpu_used || instanceDetail.value.cpu_only) {
|
||||||
|
instanceConfiguration.value.gpu_count = 0
|
||||||
|
instanceConfiguration.value.cpu_only = true
|
||||||
|
cpu_only_mode.value = true
|
||||||
|
instanceConfiguration.value.cpu_count = 1
|
||||||
|
instanceConfiguration.value.memory = 2
|
||||||
|
}
|
||||||
|
instanceModifyVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const instanceModifyGpuChange = (event) => {
|
||||||
|
instanceConfiguration.value.cpu_count = serverDetail.value.cpu_count_per_gpu * event.value
|
||||||
|
instanceConfiguration.value.memory = serverDetail.value.memory_per_gpu * event.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const instanceModifyCpuonlyChange = () => {
|
||||||
|
if (!cpu_only_mode.value && serverDetail.value.gpu_num === serverDetail.value.gpu_used) {
|
||||||
|
toast.add({ severity: 'error', summary: '无法调整显卡资源', detail: '宿主机显卡资源不足', life: 3000 });
|
||||||
|
cpu_only_mode.value = true
|
||||||
|
}
|
||||||
|
if (cpu_only_mode.value) {
|
||||||
|
instanceConfiguration.value.cpu_only = true
|
||||||
|
instanceConfiguration.value.cpu_count = 1
|
||||||
|
instanceConfiguration.value.memory = 2
|
||||||
|
} else {
|
||||||
|
instanceConfiguration.value.cpu_only = false
|
||||||
|
instanceConfiguration.value.gpu_count = instanceDetail.value.gpu_count
|
||||||
|
if (instanceConfiguration.value.gpu_count === 0) {
|
||||||
|
instanceConfiguration.value.gpu_count = 1
|
||||||
|
}
|
||||||
|
if (serverDetail.value.gpu_num - serverDetail.value.gpu_used < instanceConfiguration.value.gpu_count) {
|
||||||
|
instanceConfiguration.value.gpu_count = serverDetail.value.gpu_num - serverDetail.value.gpu_used
|
||||||
|
}
|
||||||
|
instanceConfiguration.value.cpu_count = serverDetail.value.cpu_count_per_gpu * instanceConfiguration.value.gpu_count
|
||||||
|
instanceConfiguration.value.memory = serverDetail.value.memory_per_gpu * instanceConfiguration.value.gpu_count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openInstanceDelete = () => {
|
||||||
|
confirm.require({
|
||||||
|
header: '确认删除 实例ID: ' + instanceDetail.value.id,
|
||||||
|
message: '实例删除后,数据将无法恢复,请确认删除',
|
||||||
|
icon: 'pi pi-info-circle',
|
||||||
|
rejectProps: {
|
||||||
|
label: '取消',
|
||||||
|
severity: 'secondary',
|
||||||
|
outlined: true
|
||||||
|
},
|
||||||
|
acceptProps: {
|
||||||
|
label: '删除',
|
||||||
|
severity: 'danger'
|
||||||
|
},
|
||||||
|
accept: async () => {
|
||||||
|
await instanceDelete(instanceDetail.value.id)
|
||||||
|
},
|
||||||
|
reject: () => { }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const instanceModifyLabel = (id, label) => {
|
||||||
|
api.UserInstancesModifyLabel(id, { label: label }).then(async (res) => {
|
||||||
|
toast.add({ severity: 'success', summary: '修改备注成功', detail: '已保存备注', life: 3000 });
|
||||||
|
await getInstances()
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
toast.add({ severity: 'error', summary: '修改备注失败', detail: err.response.data.msg, life: 3000 });
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getInstances()
|
||||||
|
getImages()
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
3
frontend/src/views/users/Settings.vue
Normal file
3
frontend/src/views/users/Settings.vue
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
个人设置
|
||||||
|
</template>
|
||||||
15
frontend/tailwind.config.js
Normal file
15
frontend/tailwind.config.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
darkMode: ['selector', '[class*="app-dark"]'],
|
||||||
|
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
|
||||||
|
plugins: [require('tailwindcss-primeui')],
|
||||||
|
theme: {
|
||||||
|
screens: {
|
||||||
|
sm: '576px',
|
||||||
|
md: '768px',
|
||||||
|
lg: '992px',
|
||||||
|
xl: '1200px',
|
||||||
|
'2xl': '1920px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
36
frontend/vite.config.mjs
Normal file
36
frontend/vite.config.mjs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { fileURLToPath, URL } from 'node:url';
|
||||||
|
|
||||||
|
import { PrimeVueResolver } from '@primevue/auto-import-resolver';
|
||||||
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
import Components from 'unplugin-vue-components/vite';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
outDir: '../routers/index/web',
|
||||||
|
},
|
||||||
|
optimizeDeps: {
|
||||||
|
noDiscovery: true
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
Components({
|
||||||
|
resolvers: [PrimeVueResolver()]
|
||||||
|
})
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://127.0.0.1:34567/api',
|
||||||
|
// changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/api/, '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
65
go.mod
Normal file
65
go.mod
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
module megrez
|
||||||
|
|
||||||
|
go 1.23.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/kataras/iris/v12 v12.2.11
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
gorm.io/driver/postgres v1.5.9
|
||||||
|
gorm.io/gorm v1.25.12
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/BurntSushi/toml v1.3.2 // indirect
|
||||||
|
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect
|
||||||
|
github.com/CloudyKit/jet/v6 v6.2.0 // indirect
|
||||||
|
github.com/Joker/jade v1.1.3 // indirect
|
||||||
|
github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 // indirect
|
||||||
|
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||||
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
|
github.com/fatih/structs v1.1.0 // indirect
|
||||||
|
github.com/flosch/pongo2/v4 v4.0.2 // indirect
|
||||||
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
|
github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0 // indirect
|
||||||
|
github.com/gorilla/css v1.0.0 // indirect
|
||||||
|
github.com/iris-contrib/schema v0.0.6 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
|
github.com/jackc/pgx/v5 v5.7.1 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
|
github.com/kataras/blocks v0.0.8 // indirect
|
||||||
|
github.com/kataras/golog v0.1.11 // indirect
|
||||||
|
github.com/kataras/pio v0.0.13 // indirect
|
||||||
|
github.com/kataras/sitemap v0.0.6 // indirect
|
||||||
|
github.com/kataras/tunnel v0.0.4 // indirect
|
||||||
|
github.com/klauspost/compress v1.17.7 // indirect
|
||||||
|
github.com/mailgun/raymond/v2 v2.0.48 // indirect
|
||||||
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.26 // indirect
|
||||||
|
github.com/redis/go-redis/v9 v9.5.1 // indirect
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
|
github.com/schollz/closestmatch v2.1.0+incompatible // indirect
|
||||||
|
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||||
|
github.com/tdewolff/minify/v2 v2.20.19 // indirect
|
||||||
|
github.com/tdewolff/parse/v2 v2.7.12 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||||
|
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||||
|
github.com/yosssi/ace v0.0.5 // indirect
|
||||||
|
golang.org/x/crypto v0.27.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect
|
||||||
|
golang.org/x/net v0.24.0 // indirect
|
||||||
|
golang.org/x/sync v0.8.0 // indirect
|
||||||
|
golang.org/x/sys v0.25.0 // indirect
|
||||||
|
golang.org/x/text v0.18.0 // indirect
|
||||||
|
golang.org/x/time v0.5.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.33.0 // indirect
|
||||||
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
)
|
||||||
211
go.sum
Normal file
211
go.sum
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||||
|
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||||
|
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c=
|
||||||
|
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
|
||||||
|
github.com/CloudyKit/jet/v6 v6.2.0 h1:EpcZ6SR9n28BUGtNJSvlBqf90IpjeFr36Tizxhn/oME=
|
||||||
|
github.com/CloudyKit/jet/v6 v6.2.0/go.mod h1:d3ypHeIRNo2+XyqnGA8s+aphtcVpjP5hPwP/Lzo7Ro4=
|
||||||
|
github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc=
|
||||||
|
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
|
||||||
|
github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk=
|
||||||
|
github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM=
|
||||||
|
github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 h1:KkH3I3sJuOLP3TjA/dfr4NAY8bghDwnXiU7cTKxQqo0=
|
||||||
|
github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06/go.mod h1:7erjKLwalezA0k99cWs5L11HWOAPNjdUZ6RxH1BXbbM=
|
||||||
|
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||||
|
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||||
|
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
|
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||||
|
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||||
|
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
|
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
|
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
|
||||||
|
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
|
||||||
|
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||||
|
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||||
|
github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw=
|
||||||
|
github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8=
|
||||||
|
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||||
|
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||||
|
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||||
|
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0 h1:4gjrh/PN2MuWCCElk8/I4OCKRKWCCo2zEct3VKCbibU=
|
||||||
|
github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||||
|
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||||
|
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||||
|
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||||
|
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||||
|
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||||
|
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||||
|
github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
|
||||||
|
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
|
||||||
|
github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go=
|
||||||
|
github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE=
|
||||||
|
github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw=
|
||||||
|
github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
|
||||||
|
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
|
github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM=
|
||||||
|
github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg=
|
||||||
|
github.com/kataras/golog v0.1.11 h1:dGkcCVsIpqiAMWTlebn/ZULHxFvfG4K43LF1cNWSh20=
|
||||||
|
github.com/kataras/golog v0.1.11/go.mod h1:mAkt1vbPowFUuUGvexyQ5NFW6djEgGyxQBIARJ0AH4A=
|
||||||
|
github.com/kataras/iris/v12 v12.2.11 h1:sGgo43rMPfzDft8rjVhPs6L3qDJy3TbBrMD/zGL1pzk=
|
||||||
|
github.com/kataras/iris/v12 v12.2.11/go.mod h1:uMAeX8OqG9vqdhyrIPv8Lajo/wXTtAF43wchP9WHt2w=
|
||||||
|
github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM=
|
||||||
|
github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM=
|
||||||
|
github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY=
|
||||||
|
github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4=
|
||||||
|
github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA=
|
||||||
|
github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw=
|
||||||
|
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
|
||||||
|
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||||
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw=
|
||||||
|
github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18=
|
||||||
|
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
|
||||||
|
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||||
|
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8=
|
||||||
|
github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo=
|
||||||
|
github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
|
||||||
|
github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk=
|
||||||
|
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
|
||||||
|
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||||
|
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||||
|
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||||
|
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/tdewolff/minify/v2 v2.20.19 h1:tX0SR0LUrIqGoLjXnkIzRSIbKJ7PaNnSENLD4CyH6Xo=
|
||||||
|
github.com/tdewolff/minify/v2 v2.20.19/go.mod h1:ulkFoeAVWMLEyjuDz1ZIWOA31g5aWOawCFRp9R/MudM=
|
||||||
|
github.com/tdewolff/parse/v2 v2.7.12 h1:tgavkHc2ZDEQVKy1oWxwIyh5bP4F5fEh/JmBwPP/3LQ=
|
||||||
|
github.com/tdewolff/parse/v2 v2.7.12/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
|
||||||
|
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
|
||||||
|
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo=
|
||||||
|
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||||
|
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||||
|
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||||
|
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||||
|
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
|
||||||
|
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||||
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||||
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||||
|
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||||
|
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||||
|
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=
|
||||||
|
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
|
||||||
|
github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA=
|
||||||
|
github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0=
|
||||||
|
github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
|
||||||
|
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
|
||||||
|
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
|
||||||
|
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
|
||||||
|
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||||
|
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||||
|
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8=
|
||||||
|
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
|
||||||
|
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||||
|
golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||||
|
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||||
|
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||||
|
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||||
|
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
|
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
|
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||||
|
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
|
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
|
||||||
|
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||||
|
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||||
|
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||||
|
moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs=
|
||||||
|
moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE=
|
||||||
11
libs/crypto/base64.go
Normal file
11
libs/crypto/base64.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package crypto
|
||||||
|
|
||||||
|
import "encoding/base64"
|
||||||
|
|
||||||
|
func Base64Encode(src []byte) string {
|
||||||
|
return base64.StdEncoding.EncodeToString(src)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Base64Decode(src string) ([]byte, error) {
|
||||||
|
return base64.StdEncoding.DecodeString(src)
|
||||||
|
}
|
||||||
45
libs/crypto/hex.go
Normal file
45
libs/crypto/hex.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const letters = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
|
||||||
|
var src = rand.NewSource(time.Now().UnixNano())
|
||||||
|
|
||||||
|
const (
|
||||||
|
// 6 bits to represent a letter index
|
||||||
|
letterIdBits = 6
|
||||||
|
// All 1-bits as many as letterIdBits
|
||||||
|
letterIdMask = 1<<letterIdBits - 1
|
||||||
|
letterIdMax = 63 / letterIdBits
|
||||||
|
)
|
||||||
|
|
||||||
|
func Hex(n int) string {
|
||||||
|
sb := strings.Builder{}
|
||||||
|
sb.Grow(n)
|
||||||
|
// A rand.Int63() generates 63 random bits, enough for letterIdMax letters!
|
||||||
|
for i, cache, remain := n-1, src.Int63(), letterIdMax; i >= 0; {
|
||||||
|
if remain == 0 {
|
||||||
|
cache, remain = src.Int63(), letterIdMax
|
||||||
|
}
|
||||||
|
if idx := int(cache & letterIdMask); idx < len(letters) {
|
||||||
|
sb.WriteByte(letters[idx])
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
cache >>= letterIdBits
|
||||||
|
remain--
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func HexLowercase(n int) string {
|
||||||
|
return strings.ToLower(Hex(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
func HexUpper(n int) string {
|
||||||
|
return strings.ToUpper(Hex(n))
|
||||||
|
}
|
||||||
13
libs/crypto/sha256.go
Normal file
13
libs/crypto/sha256.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Sha256(str string) string {
|
||||||
|
hash := sha256.New()
|
||||||
|
hash.Write([]byte(str))
|
||||||
|
bytes := hash.Sum(nil)
|
||||||
|
return hex.EncodeToString(bytes)
|
||||||
|
}
|
||||||
24
libs/crypto/uuid.go
Normal file
24
libs/crypto/uuid.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerateUUID 生成一个新的 UUID
|
||||||
|
func GenerateUUID() (string, error) {
|
||||||
|
newUUID, err := uuid.NewUUID()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return newUUID.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateUUIDWithoutHyphen() (string, error) {
|
||||||
|
newUUID, err := uuid.NewUUID()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return strings.ReplaceAll(newUUID.String(), "-", ""), nil
|
||||||
|
}
|
||||||
225
libs/logger/logger.go
Normal file
225
libs/logger/logger.go
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"container/list"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DEBUG = "DEBUG"
|
||||||
|
INFO = "INFO"
|
||||||
|
WARN = "WARN"
|
||||||
|
ERROR = "ERROR"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Reset = "\033[0m"
|
||||||
|
Red = "\033[31m"
|
||||||
|
Green = "\033[32m"
|
||||||
|
Yellow = "\033[33m"
|
||||||
|
Blue = "\033[34m"
|
||||||
|
Magenta = "\033[35m"
|
||||||
|
Cyan = "\033[36m"
|
||||||
|
White = "\033[37m"
|
||||||
|
BlueBold = "\033[34;1m"
|
||||||
|
MagentaBold = "\033[35;1m"
|
||||||
|
RedBold = "\033[31;1m"
|
||||||
|
YellowBold = "\033[33;1m"
|
||||||
|
)
|
||||||
|
|
||||||
|
var levelArry = [4]string{DEBUG, INFO, WARN, ERROR}
|
||||||
|
var colorArry = [4]string{MagentaBold, BlueBold, YellowBold, RedBold}
|
||||||
|
|
||||||
|
var levelMap = map[string]int{
|
||||||
|
DEBUG: 0,
|
||||||
|
INFO: 1,
|
||||||
|
WARN: 2,
|
||||||
|
ERROR: 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoggerStruct struct {
|
||||||
|
level string
|
||||||
|
// logger *log.Logger
|
||||||
|
model string
|
||||||
|
function string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Interface interface {
|
||||||
|
LogMode(logger.LogLevel) Interface
|
||||||
|
Info(context.Context, string, ...interface{})
|
||||||
|
Warn(context.Context, string, ...interface{})
|
||||||
|
Error(context.Context, string, ...interface{})
|
||||||
|
Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type logetContent struct {
|
||||||
|
format string
|
||||||
|
v []interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type logQueueStruct struct {
|
||||||
|
*list.List
|
||||||
|
Mu sync.Mutex
|
||||||
|
RWLock sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
var logQueue logQueueStruct
|
||||||
|
var logLogger *log.Logger
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
logQueue = logQueueStruct{
|
||||||
|
List: list.New(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func logOutput() {
|
||||||
|
if !logQueue.Mu.TryLock() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for logQueue.Len() > 0 {
|
||||||
|
logQueue.RWLock.RLock()
|
||||||
|
e := logQueue.Front()
|
||||||
|
logQueue.RWLock.RUnlock()
|
||||||
|
switch e.Value.(type) {
|
||||||
|
case logetContent:
|
||||||
|
content := e.Value.(logetContent)
|
||||||
|
logLogger.Printf(content.format, content.v...)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
logQueue.RWLock.Lock()
|
||||||
|
logQueue.Remove(e)
|
||||||
|
logQueue.RWLock.Unlock()
|
||||||
|
}
|
||||||
|
logQueue.Mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogger(level string, args ...any) (*LoggerStruct, error) {
|
||||||
|
level = strings.ToUpper(level)
|
||||||
|
if _, ok := levelMap[level]; !ok {
|
||||||
|
return nil, errors.New("level not found")
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
filename := "data/logs/backend.log"
|
||||||
|
if len(args) > 0 {
|
||||||
|
if args[0] == "stdout" {
|
||||||
|
logLogger = log.New(io.MultiWriter(os.Stdout), "", log.Ldate|log.Ltime|log.Lmicroseconds)
|
||||||
|
return &LoggerStruct{
|
||||||
|
level: level,
|
||||||
|
// logger: log.New(io.MultiWriter(os.Stdout), "", log.Ldate|log.Ltime|log.Lmicroseconds),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
filename = args[0].(string)
|
||||||
|
}
|
||||||
|
logFile, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND|os.O_SYNC, os.FileMode(0775))
|
||||||
|
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
err = os.MkdirAll("data/logs", os.FileMode(0775))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
logFile, _ = os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND|os.O_SYNC, os.FileMode(0775))
|
||||||
|
}
|
||||||
|
|
||||||
|
logLogger = log.New(io.MultiWriter(logFile, os.Stdout), "", log.Ldate|log.Ltime|log.Lmicroseconds)
|
||||||
|
return &LoggerStruct{
|
||||||
|
level: level,
|
||||||
|
// logger: log.New(io.MultiWriter(logPipe, os.Stdout), "", log.Ldate|log.Ltime),
|
||||||
|
// logger: log.New(io.MultiWriter(logFile, os.Stdout), "", log.Ldate|log.Ltime|log.Lmicroseconds),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LoggerStruct) Close() {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
for logQueue.Len() > 0 {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
cancel()
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LoggerStruct) Clone() *LoggerStruct {
|
||||||
|
return &LoggerStruct{
|
||||||
|
level: l.level,
|
||||||
|
// logger: log.New(io.MultiWriter(logFile, os.Stdout), "", log.Ldate|log.Ltime|log.Lmicroseconds),
|
||||||
|
model: l.model,
|
||||||
|
function: l.function,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LoggerStruct) SetModel(model string) {
|
||||||
|
l.model = model
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LoggerStruct) SetFunction(function string) {
|
||||||
|
l.function = function
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LoggerStruct) SetLevel(level string) {
|
||||||
|
l.level = strings.ToUpper(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LoggerStruct) Fatal(v ...interface{}) {
|
||||||
|
l.Error("%v", v...)
|
||||||
|
l.Close()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LoggerStruct) Print(level, format string, v ...interface{}) {
|
||||||
|
if levelMap[level] >= levelMap[l.level] {
|
||||||
|
logQueue.RWLock.Lock()
|
||||||
|
if l.model != "" && l.function != "" {
|
||||||
|
// l.logger.Printf("- "+colorArry[levelMap[level]]+level+Reset+" - "+Blue+"["+l.model+"."+l.function+"]"+Reset+" - "+format, v...)
|
||||||
|
logQueue.PushBack(logetContent{
|
||||||
|
format: "- " + colorArry[levelMap[level]] + level + Reset + " - " + Blue + "[" + l.model + "." + l.function + "]" + Reset + " - " + format,
|
||||||
|
v: v,
|
||||||
|
})
|
||||||
|
} else if l.model != "" {
|
||||||
|
// l.logger.Printf("- "+colorArry[levelMap[level]]+level+Reset+" - "+Blue+"["+l.model+"]"+Reset+" - "+format, v...)
|
||||||
|
logQueue.PushBack(logetContent{
|
||||||
|
format: "- " + colorArry[levelMap[level]] + level + Reset + " - " + Blue + "[" + l.model + "]" + Reset + " - " + format,
|
||||||
|
v: v,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// l.logger.Printf("- "+colorArry[levelMap[level]]+level+Reset+" - "+format, v...)
|
||||||
|
logQueue.PushBack(logetContent{
|
||||||
|
format: "- " + colorArry[levelMap[level]] + level + Reset + " - " + format,
|
||||||
|
v: v,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
logQueue.RWLock.Unlock()
|
||||||
|
go logOutput()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LoggerStruct) Println(level string, v ...interface{}) {
|
||||||
|
l.Print(level, "%v", v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LoggerStruct) Debug(format string, v ...interface{}) {
|
||||||
|
l.Print(DEBUG, format, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LoggerStruct) Info(format string, v ...interface{}) {
|
||||||
|
l.Print(INFO, format, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LoggerStruct) Warn(format string, v ...interface{}) {
|
||||||
|
l.Print(WARN, format, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LoggerStruct) Error(format string, v ...interface{}) {
|
||||||
|
l.Print(ERROR, format, v...)
|
||||||
|
}
|
||||||
48
libs/logger/logger_test.go
Normal file
48
libs/logger/logger_test.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package logger_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"megrez/libs/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLogger(t *testing.T) {
|
||||||
|
t.Log("TestLogger")
|
||||||
|
l, err := logger.NewLogger(logger.DEBUG, "stdout")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
l.Info("TestInfo")
|
||||||
|
l.Warn("TestWarn")
|
||||||
|
l.Error("TestError")
|
||||||
|
l.Debug("TestDebug")
|
||||||
|
// l.Fatal("Fatal")
|
||||||
|
|
||||||
|
t.Log(("SetModel"))
|
||||||
|
l.SetModel("Logger")
|
||||||
|
l.Info("SetModel TestInfo")
|
||||||
|
l.Warn("SetModel TestWarn")
|
||||||
|
l.Error("SetModel TestError")
|
||||||
|
l.Debug("SetModel TestDebug")
|
||||||
|
|
||||||
|
t.Log(("SetFunction"))
|
||||||
|
l.SetFunction("TestLogger")
|
||||||
|
l.Info("SetFunction TestInfo")
|
||||||
|
l.Warn("SetFunction TestWarn")
|
||||||
|
l.Error("SetFunction TestError")
|
||||||
|
l.Debug("SetFunction TestDebug")
|
||||||
|
|
||||||
|
l.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkLogger(b *testing.B) {
|
||||||
|
b.Log("BenchmarkLogger")
|
||||||
|
l, err := logger.NewLogger(logger.DEBUG, "stdout")
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
go l.Info("BenchmarkLogger: %d", i)
|
||||||
|
}
|
||||||
|
l.Close()
|
||||||
|
}
|
||||||
281
libs/request/request.go
Normal file
281
libs/request/request.go
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
package request
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client ==> 客户端实例
|
||||||
|
type Client struct {
|
||||||
|
Request *Request
|
||||||
|
Cookies []*http.Cookie
|
||||||
|
Result Result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request ==> 请求体
|
||||||
|
type Request struct {
|
||||||
|
Url string
|
||||||
|
Method string
|
||||||
|
Data io.Reader
|
||||||
|
ContentType string
|
||||||
|
Authorization string
|
||||||
|
UserAgent string
|
||||||
|
Header map[string]string
|
||||||
|
Timeout time.Duration
|
||||||
|
// The proxy type is determined by the URL scheme. "http",
|
||||||
|
// "https", and "socks5" are supported. If the scheme is empty,
|
||||||
|
//
|
||||||
|
// If Proxy is nil or nil *URL, no proxy is used.
|
||||||
|
ProxyUrl url.URL
|
||||||
|
Redirect bool
|
||||||
|
MaxRedirects int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result ==> 结果集
|
||||||
|
type Result struct {
|
||||||
|
Header http.Header
|
||||||
|
Location *url.URL
|
||||||
|
Body []byte
|
||||||
|
Status int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRequest ==> 新建请求
|
||||||
|
func NewRequest() *Client {
|
||||||
|
return &Client{
|
||||||
|
Request: &Request{
|
||||||
|
Method: "GET",
|
||||||
|
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
|
||||||
|
Header: make(map[string]string),
|
||||||
|
Redirect: false,
|
||||||
|
MaxRedirects: 10,
|
||||||
|
},
|
||||||
|
Result: Result{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do ==> 执行请求
|
||||||
|
func (c *Client) Do() *Client {
|
||||||
|
//HTTP请求构造
|
||||||
|
request, _ := http.NewRequest(c.Request.Method, c.Request.Url, c.Request.Data)
|
||||||
|
request.Header.Set("Content-Type", c.Request.ContentType)
|
||||||
|
if c.Request.Authorization != "" {
|
||||||
|
request.Header.Set("Authorization", c.Request.Authorization)
|
||||||
|
}
|
||||||
|
if c.Request.UserAgent != "" {
|
||||||
|
request.Header.Set("User-Agent", c.Request.UserAgent)
|
||||||
|
}
|
||||||
|
if len(c.Cookies) != 0 {
|
||||||
|
for _, cookie := range c.Cookies {
|
||||||
|
request.AddCookie(cookie)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 支持自定义Header
|
||||||
|
for k, v := range c.Request.Header {
|
||||||
|
request.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
var client *http.Client
|
||||||
|
if c.Request.ProxyUrl == (url.URL{}) {
|
||||||
|
client = &http.Client{
|
||||||
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
|
if !c.Request.Redirect {
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
}
|
||||||
|
if len(via) >= c.Request.MaxRedirects {
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
client = &http.Client{
|
||||||
|
Transport: &http.Transport{Proxy: http.ProxyURL(&c.Request.ProxyUrl)},
|
||||||
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
|
if !c.Request.Redirect {
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
}
|
||||||
|
if len(via) >= c.Request.MaxRedirects {
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.Request.Timeout != 0 {
|
||||||
|
client.Timeout = c.Request.Timeout
|
||||||
|
}
|
||||||
|
res, err := client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
// fmt.Println(err)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
if len(res.Cookies()) > 1 {
|
||||||
|
c.Cookies = append(c.Cookies, res.Cookies()...)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
c.Result.Status = res.StatusCode
|
||||||
|
c.Result.Body, _ = io.ReadAll(res.Body)
|
||||||
|
c.Result.Header = res.Header
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ==> 定义请求方式
|
||||||
|
func (c *Client) Get() *Client {
|
||||||
|
c.Request.Method = "GET"
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post ==> 定义请求方式
|
||||||
|
func (c *Client) Post() *Client {
|
||||||
|
c.Request.Method = "POST"
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put ==> 定义请求方式
|
||||||
|
func (c *Client) Put() *Client {
|
||||||
|
c.Request.Method = "PUT"
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch ==> 定义请求方式
|
||||||
|
func (c *Client) Patch() *Client {
|
||||||
|
c.Request.Method = "PATCH"
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete ==> 定义请求方式
|
||||||
|
func (c *Client) Delete() *Client {
|
||||||
|
c.Request.Method = "DELETE"
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUrl ==> 定义请求目标
|
||||||
|
func (c *Client) SetUrl(url ...any) *Client {
|
||||||
|
c.Request.Url = fmt.Sprintf(url[0].(string), url[1:]...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMethod ==> 定义请求方法
|
||||||
|
func (c *Client) SetMethod(method string) *Client {
|
||||||
|
c.Request.Method = method
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetContentType ==> 定义内容类型
|
||||||
|
func (c *Client) SetContentType(contentType string) *Client {
|
||||||
|
c.Request.ContentType = contentType
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserAgent ==> 定义用户代理
|
||||||
|
func (c *Client) SetUserAgent(userAgent string) *Client {
|
||||||
|
c.Request.UserAgent = userAgent
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBody ==> 定义请求内容
|
||||||
|
func (c *Client) SetBody(body io.Reader) *Client {
|
||||||
|
c.Request.Data = body
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SerHeaders ==> 定义请求头列表
|
||||||
|
func (c *Client) SetHeaders(headers map[string]string) *Client {
|
||||||
|
c.Request.Header = headers
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHeader ==> 定义请求头
|
||||||
|
func (c *Client) SetHeader(key, value string) *Client {
|
||||||
|
c.Request.Header[key] = value
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAuthorization ==> 定义身份验证
|
||||||
|
func (c *Client) SetAuthorization(credentials string) *Client {
|
||||||
|
c.Request.Authorization = credentials
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTimeOut ==> 设置会话超时上限
|
||||||
|
func (c *Client) SetTimeout(timeout time.Duration) *Client {
|
||||||
|
c.Request.Timeout = timeout
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCookie ==> 设置Cookie
|
||||||
|
func (c *Client) SetCookie(cookie *http.Cookie) *Client {
|
||||||
|
c.Cookies = append(c.Cookies, cookie)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCookies ==> 设置Cookies
|
||||||
|
func (c *Client) SetCookies(cookies string) *Client {
|
||||||
|
cookielist := strings.Split(cookies, "; ")
|
||||||
|
for _, cookie := range cookielist {
|
||||||
|
cookiekv := strings.Split(cookie, "=")
|
||||||
|
c.SetCookie(&http.Cookie{
|
||||||
|
Name: cookiekv[0],
|
||||||
|
Value: strings.Join(cookiekv[1:], "="),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetProxy ==> 设置代理
|
||||||
|
func (c *Client) SetProxy(proxyUrl url.URL) *Client {
|
||||||
|
c.Request.ProxyUrl = proxyUrl
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRedirect ==> 设置重定向
|
||||||
|
func (c *Client) SetRedirect(redirect bool) *Client {
|
||||||
|
c.Request.Redirect = redirect
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMaxRedirects ==> 设置最大重定向次数
|
||||||
|
func (c *Client) SetMaxRedirects(maxRedirects int) *Client {
|
||||||
|
c.Request.MaxRedirects = maxRedirects
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatusCode ==> 获取请求状态码
|
||||||
|
func (c *Client) GetStatusCode() int {
|
||||||
|
return c.Result.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBody ==> 获取返回内容
|
||||||
|
func (c *Client) GetBody() []byte {
|
||||||
|
return c.Result.Body
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBody ==> 获取返回内容
|
||||||
|
func (c *Client) GetBodyString() string {
|
||||||
|
return string(c.Result.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHeaders ==> 获取返回头字典
|
||||||
|
func (c *Client) GetHeaders() http.Header {
|
||||||
|
return c.Result.Header
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHeader ==> 获取返回头
|
||||||
|
func (c *Client) GetHeader(key string) string {
|
||||||
|
return c.Result.Header.Get(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveToFile ==> 写出结果到文件
|
||||||
|
func (c *Client) SaveToFile(filepath string) (err error) {
|
||||||
|
// Write the body to file
|
||||||
|
err = os.WriteFile(filepath, c.GetBody(), 0777)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
53
main.go
Normal file
53
main.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"megrez/services/config"
|
||||||
|
"megrez/services/database"
|
||||||
|
"megrez/services/dispatcher"
|
||||||
|
"megrez/services/http"
|
||||||
|
"megrez/services/instanceController"
|
||||||
|
"megrez/services/logger"
|
||||||
|
"megrez/services/redis"
|
||||||
|
"megrez/services/system"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
configFilePath = "config.yml"
|
||||||
|
l = logger.Logger.Clone()
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.StringVar(&configFilePath, "c", "config.yml", "config file path")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
config.InitConfig(configFilePath)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
redis.Close()
|
||||||
|
|
||||||
|
l.Close()
|
||||||
|
}()
|
||||||
|
defer func() {
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
l.Error("Panic: %v", err)
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
n := runtime.Stack(buf, false)
|
||||||
|
l.Error("Stack trace: \n%s", buf[:n])
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}()
|
||||||
|
|
||||||
|
logger.InitLogger(config.GetLogLevel(), config.GetLogFile())
|
||||||
|
|
||||||
|
database.Connect()
|
||||||
|
redis.Connect()
|
||||||
|
instanceController.InitInstanceController()
|
||||||
|
dispatcher.Init()
|
||||||
|
|
||||||
|
go system.Check()
|
||||||
|
|
||||||
|
http.Start()
|
||||||
|
}
|
||||||
61
models/instance.go
Normal file
61
models/instance.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Status int
|
||||||
|
|
||||||
|
const (
|
||||||
|
InstanceFail Status = -1
|
||||||
|
InstanceRunning Status = 0
|
||||||
|
InstancePaused Status = 1
|
||||||
|
InstanceStopped Status = 2
|
||||||
|
|
||||||
|
InstanceReady Status = 3
|
||||||
|
InstanceStarting Status = 4
|
||||||
|
InstanceStopping Status = 5
|
||||||
|
InstancePausing Status = 6
|
||||||
|
InstanceRestarting Status = 7
|
||||||
|
InstanceModifying Status = 8
|
||||||
|
InstanceDeleting Status = 9
|
||||||
|
)
|
||||||
|
|
||||||
|
var instanceIngStatus = []Status{InstanceReady, InstanceStarting, InstanceStopping, InstancePausing, InstanceRestarting, InstanceModifying, InstanceDeleting}
|
||||||
|
|
||||||
|
type Instances struct {
|
||||||
|
ID uint `json:"id" gorm:"primary_key;autoIncrement;index"`
|
||||||
|
|
||||||
|
UserID uint `json:"user_id,omitempty" gorm:"not null"`
|
||||||
|
ServerID uint `json:"server_id" gorm:"not null"`
|
||||||
|
|
||||||
|
ImageName string `json:"image_name" gorm:"type:varchar(255);not null"`
|
||||||
|
ContainerName string `json:"container_name,omitempty" gorm:"type:varchar(255);not null"`
|
||||||
|
CpuOnly bool `json:"cpu_only" gorm:"not null"`
|
||||||
|
GpuCount int `json:"gpu_count" gorm:"not null"`
|
||||||
|
VolumeName string `json:"volume_name,omitempty" gorm:"type:varchar(255);not null"`
|
||||||
|
VolumeSize int `json:"volume_size" gorm:"not null"`
|
||||||
|
|
||||||
|
SshAddress string `json:"ssh_address" gorm:"type:varchar(255);not null"`
|
||||||
|
SshPasswd string `json:"ssh_passwd" gorm:"type:varchar(255);not null"`
|
||||||
|
JupyterAddress string `json:"jupyter_address" gorm:"type:varchar(255);not null"`
|
||||||
|
TensorBoardAddress string `json:"tensor_board_address" gorm:"type:varchar(255);not null"`
|
||||||
|
GrafanaAddress string `json:"grafana_address" gorm:"type:varchar(255);not null"`
|
||||||
|
Status Status `json:"status" gorm:"not null"` // -1: fail, 0: running, 1: paused, 2: stopped, 3: readying
|
||||||
|
|
||||||
|
Label string `json:"label" gorm:"type:varchar(255)"`
|
||||||
|
|
||||||
|
CreatedAt time.Time `json:"create_at"`
|
||||||
|
UpdatedAt time.Time `json:"update_at"`
|
||||||
|
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func InstanceIngStatusCheck(status Status) bool {
|
||||||
|
if slices.Index(instanceIngStatus, status) != -1 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
27
models/orders.go
Normal file
27
models/orders.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Orders struct {
|
||||||
|
ID uint `json:"id" gorm:"primary_key;autoIncrement;index"`
|
||||||
|
|
||||||
|
UserID uint `json:"user_id" gorm:"not null"`
|
||||||
|
ServerID uint `json:"server_id" gorm:"not null"`
|
||||||
|
StartTime time.Time `json:"start_time" gorm:"not null"`
|
||||||
|
EndTime time.Time `json:"end_time" gorm:"not null"`
|
||||||
|
|
||||||
|
GpuNum int `json:"gpu_num" gorm:"not null"`
|
||||||
|
DiskUsed float64 `json:"disk_used" gorm:"not null"`
|
||||||
|
|
||||||
|
PricePerHour float64 `json:"price_per_gpu_per_hour" gorm:"not null"`
|
||||||
|
PriceDiskPerHour float64 `json:"price_disk_per_hour" gorm:"not null"`
|
||||||
|
|
||||||
|
Price float64 `json:"price" gorm:"not null"`
|
||||||
|
|
||||||
|
CreatedAt time.Time `json:"create_at"`
|
||||||
|
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||||
|
}
|
||||||
32
models/servers.go
Normal file
32
models/servers.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Servers struct {
|
||||||
|
ID uint `json:"id" gorm:"primary_key;autoIncrement;index"`
|
||||||
|
|
||||||
|
Name string `json:"name" gorm:"type:varchar(255);not null"`
|
||||||
|
IP string `json:"ip,omitempty" gorm:"type:varchar(255);not null"`
|
||||||
|
Port int `json:"port,omitempty" gorm:"not null"`
|
||||||
|
Apikey string `json:"apikey,omitempty" gorm:"type:varchar(255);not null"`
|
||||||
|
GpuType string `json:"gpu_type" gorm:"type:varchar(255);not null"`
|
||||||
|
GpuNum int `json:"gpu_num" gorm:"not null"`
|
||||||
|
GpuDriverVersion string `json:"gpu_driver_version" gorm:"type:varchar(255);not null"`
|
||||||
|
GpuCudaVersion string `json:"gpu_cuda_version" gorm:"type:varchar(255);not null"`
|
||||||
|
|
||||||
|
CpuCountPerGpu int `json:"cpu_count_per_gpu" gorm:"not null"`
|
||||||
|
MemoryPerGpu int `json:"memory_per_gpu" gorm:"not null"` // Unit `GB`
|
||||||
|
VolumeTotal int `json:"volume_total" gorm:"not null"` // Unit `GB`
|
||||||
|
Price float64 `json:"price" gorm:"not null"` // 1 GPU Per Hour
|
||||||
|
PriceVolume float64 `json:"price_volume" gorm:"not null"` // 1GB Per Hour
|
||||||
|
|
||||||
|
GpuUsed int `json:"gpu_used" gorm:"not null,default:0"`
|
||||||
|
VolumeUsed int `json:"volume_used" gorm:"not null,default:0"`
|
||||||
|
|
||||||
|
CreatedAt time.Time `json:"create_at"`
|
||||||
|
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||||
|
}
|
||||||
6
models/system.go
Normal file
6
models/system.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type System struct {
|
||||||
|
Key string `json:"key" gorm:"primary_key;unique;index"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
32
models/users.go
Normal file
32
models/users.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"megrez/libs/crypto"
|
||||||
|
"megrez/services/config"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Users struct {
|
||||||
|
ID uint `json:"id" gorm:"primary_key;autoIncrement;index"`
|
||||||
|
|
||||||
|
Username string `json:"username" gorm:"type:varchar(255);uniqueIndex;unique;not null"`
|
||||||
|
Password string `json:"password,omitempty" gorm:"type:varchar(255);not null"`
|
||||||
|
Role int `json:"role" gorm:"not null,default:0"`
|
||||||
|
|
||||||
|
Email string `json:"email" gorm:"type:varchar(255);uniqueIndex;unique;not null"`
|
||||||
|
|
||||||
|
Balance float64 `json:"balance" gorm:"not null"`
|
||||||
|
|
||||||
|
CreatedAt time.Time `json:"create_at"`
|
||||||
|
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Users) PasswordHash(password string) string {
|
||||||
|
return crypto.Sha256(password + u.Email + config.GetSystemSalt())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Users) CheckPassword(password string) bool {
|
||||||
|
return u.Password == u.PasswordHash(password)
|
||||||
|
}
|
||||||
1
routers/.gitignore
vendored
Normal file
1
routers/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
web
|
||||||
35
routers/api/v1/admin/images/list.go
Normal file
35
routers/api/v1/admin/images/list.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package images
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"megrez/models"
|
||||||
|
"megrez/routers/api/v1/middleware"
|
||||||
|
"megrez/services/database"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
)
|
||||||
|
|
||||||
|
func listHandler(ctx iris.Context) {
|
||||||
|
l.SetFunction("listHandler")
|
||||||
|
|
||||||
|
res := make(map[string]string)
|
||||||
|
|
||||||
|
system := models.System{
|
||||||
|
Key: imagesKey,
|
||||||
|
}
|
||||||
|
result := database.DB.FirstOrCreate(&system)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("get system error: %v", result.Error)
|
||||||
|
middleware.Result(ctx, res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.Unmarshal([]byte(system.Value), &res)
|
||||||
|
if err != nil {
|
||||||
|
l.Error("unmarshal system error: %v", err)
|
||||||
|
middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware.Result(ctx, res)
|
||||||
|
}
|
||||||
48
routers/api/v1/admin/images/modify.go
Normal file
48
routers/api/v1/admin/images/modify.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package images
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"megrez/models"
|
||||||
|
"megrez/routers/api/v1/middleware"
|
||||||
|
"megrez/services/database"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
)
|
||||||
|
|
||||||
|
func modifyHandler(ctx iris.Context) {
|
||||||
|
l.SetFunction("modifyHandler")
|
||||||
|
|
||||||
|
req := make(map[string]string)
|
||||||
|
err := ctx.ReadJSON(&req)
|
||||||
|
if err != nil {
|
||||||
|
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
system := models.System{
|
||||||
|
Key: imagesKey,
|
||||||
|
}
|
||||||
|
result := database.DB.FirstOrCreate(&system)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("get system config error: %v", result.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
valueBytes, err := json.Marshal(req)
|
||||||
|
if err != nil {
|
||||||
|
l.Error("marshal system error: %v", err)
|
||||||
|
middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
system.Value = string(valueBytes)
|
||||||
|
result = database.DB.Save(&system)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("save system error: %v", result.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware.Success(ctx)
|
||||||
|
}
|
||||||
22
routers/api/v1/admin/images/routers.go
Normal file
22
routers/api/v1/admin/images/routers.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package images
|
||||||
|
|
||||||
|
import (
|
||||||
|
"megrez/routers/api/v1/middleware"
|
||||||
|
"megrez/services/logger"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12/core/router"
|
||||||
|
|
||||||
|
_logger "megrez/libs/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
const imagesKey = "images"
|
||||||
|
|
||||||
|
var l *_logger.LoggerStruct
|
||||||
|
|
||||||
|
func InitImages(party router.Party) {
|
||||||
|
l = logger.Logger.Clone()
|
||||||
|
l.SetModel("Http.API.V1.Admin.Images")
|
||||||
|
|
||||||
|
party.Get("/", listHandler)
|
||||||
|
party.Post("/", middleware.SuperAdminCheck, modifyHandler)
|
||||||
|
}
|
||||||
104
routers/api/v1/admin/instance/add.go
Normal file
104
routers/api/v1/admin/instance/add.go
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
package instances
|
||||||
|
|
||||||
|
import (
|
||||||
|
"megrez/libs/crypto"
|
||||||
|
"megrez/models"
|
||||||
|
"megrez/routers/api/v1/middleware"
|
||||||
|
"megrez/services/database"
|
||||||
|
"megrez/services/dispatcher"
|
||||||
|
"megrez/services/redis"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
)
|
||||||
|
|
||||||
|
type addReqStruct struct {
|
||||||
|
UserID uint `json:"user_id"`
|
||||||
|
ServerID uint `json:"server_id"`
|
||||||
|
|
||||||
|
ImageName string `json:"image_name"`
|
||||||
|
GpuCount int `json:"gpu_count"`
|
||||||
|
VolumeSize int `json:"volume_size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func addHandler(ctx iris.Context) {
|
||||||
|
l.SetFunction("addHandler")
|
||||||
|
|
||||||
|
var req addReqStruct
|
||||||
|
err := ctx.ReadJSON(&req)
|
||||||
|
if err != nil {
|
||||||
|
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.UserID == 0 || req.ServerID == 0 || req.ImageName == "" || req.GpuCount <= 0 || req.VolumeSize < 50 {
|
||||||
|
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
user := models.Users{
|
||||||
|
ID: req.UserID,
|
||||||
|
}
|
||||||
|
result := database.DB.First(&user)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("query user error: %v", result.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeAdminUserQueryError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
server := models.Servers{
|
||||||
|
ID: req.ServerID,
|
||||||
|
}
|
||||||
|
result = database.DB.First(&server)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("query server error: %v", result.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
remainGpu, err := redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(-req.GpuCount)).Result()
|
||||||
|
if err != nil {
|
||||||
|
l.Error("incrby gpu num error: %v", err)
|
||||||
|
middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
remainVolume, err := redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID)), int64(-req.VolumeSize-30)).Result()
|
||||||
|
if err != nil {
|
||||||
|
l.Error("incrby volume size error: %v", err)
|
||||||
|
middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if remainGpu < 0 || remainVolume < 0 {
|
||||||
|
redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(req.GpuCount))
|
||||||
|
redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID)), int64(req.VolumeSize+30))
|
||||||
|
middleware.Error(ctx, middleware.CodeResourceInsufficient, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
instance := models.Instances{
|
||||||
|
UserID: req.UserID,
|
||||||
|
ServerID: req.ServerID,
|
||||||
|
ImageName: req.ImageName,
|
||||||
|
GpuCount: req.GpuCount,
|
||||||
|
VolumeSize: req.VolumeSize,
|
||||||
|
|
||||||
|
SshPasswd: crypto.Hex(16),
|
||||||
|
|
||||||
|
Status: models.InstanceReady,
|
||||||
|
}
|
||||||
|
result = database.DB.Create(&instance)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("create instance error: %v", result.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeInternalCreateError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatcherData := dispatcher.Data{
|
||||||
|
Type: dispatcher.Add,
|
||||||
|
InstanceID: instance.ID,
|
||||||
|
}
|
||||||
|
dispatcher.Push(instance.ServerID, dispatcherData)
|
||||||
|
|
||||||
|
middleware.Success(ctx)
|
||||||
|
}
|
||||||
135
routers/api/v1/admin/instance/control.go
Normal file
135
routers/api/v1/admin/instance/control.go
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
package instances
|
||||||
|
|
||||||
|
import (
|
||||||
|
"megrez/models"
|
||||||
|
"megrez/routers/api/v1/middleware"
|
||||||
|
"megrez/services/database"
|
||||||
|
"megrez/services/dispatcher"
|
||||||
|
"megrez/services/instanceController"
|
||||||
|
"megrez/services/redis"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
)
|
||||||
|
|
||||||
|
type controlStruct struct {
|
||||||
|
Action instanceController.Action `json:"action"` // 1: start, 2: pause , 3: stop, 4: restart
|
||||||
|
}
|
||||||
|
|
||||||
|
func controlHandler(ctx iris.Context) {
|
||||||
|
id, err := ctx.Params().GetUint("id")
|
||||||
|
if err != nil {
|
||||||
|
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req controlStruct
|
||||||
|
err = ctx.ReadJSON(&req)
|
||||||
|
if err != nil {
|
||||||
|
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
instance := models.Instances{
|
||||||
|
ID: id,
|
||||||
|
}
|
||||||
|
result := database.DB.First(&instance)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("detail instance error: %v", result.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeInstanceDetailError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if models.InstanceIngStatusCheck(instance.Status) {
|
||||||
|
middleware.Error(ctx, middleware.CodeInstanceStatusError, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Action == instanceController.ActionStop && instance.Status != models.InstanceRunning && instance.Status != models.InstancePaused {
|
||||||
|
middleware.Error(ctx, middleware.CodeInstanceStatusError, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Action == instanceController.ActionPause && instance.Status != models.InstanceRunning {
|
||||||
|
middleware.Error(ctx, middleware.CodeInstanceStatusError, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Action == instanceController.ActionStart && instance.Status != models.InstanceStopped && instance.Status != models.InstancePaused {
|
||||||
|
middleware.Error(ctx, middleware.CodeInstanceStatusError, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
server := models.Servers{
|
||||||
|
ID: instance.ServerID,
|
||||||
|
}
|
||||||
|
result = database.DB.First(&server)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("query server error: %v", result.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
status := instance.Status
|
||||||
|
if status == models.InstanceStopped && (req.Action == instanceController.ActionStart || req.Action == instanceController.ActionRestart) {
|
||||||
|
remainGpu, err := redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(-instance.GpuCount)).Result()
|
||||||
|
if err != nil {
|
||||||
|
l.Error("incrby gpu num error: %v", err)
|
||||||
|
middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if remainGpu < 0 {
|
||||||
|
redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(instance.GpuCount))
|
||||||
|
middleware.Error(ctx, middleware.CodeResourceInsufficient, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch req.Action {
|
||||||
|
case instanceController.ActionStart:
|
||||||
|
result = database.DB.Model(&instance).Update("status", models.InstanceStarting)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("update instance status error: %v", result.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeInstanceStartError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
case instanceController.ActionPause:
|
||||||
|
result = database.DB.Model(&instance).Update("status", models.InstancePausing)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("update instance status error: %v", result.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeInstancePauseError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
case instanceController.ActionStop:
|
||||||
|
result = database.DB.Model(&instance).Update("status", models.InstanceStopping)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("update instance status error: %v", result.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeInstanceStopError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(instance.GpuCount))
|
||||||
|
|
||||||
|
case instanceController.ActionRestart:
|
||||||
|
result = database.DB.Model(&instance).Update("status", models.InstanceRestarting)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("update instance status error: %v", result.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeInstanceRestartError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatcherData := dispatcher.Data{
|
||||||
|
Type: dispatcher.Control,
|
||||||
|
InstanceID: instance.ID,
|
||||||
|
Status: status,
|
||||||
|
Action: req.Action,
|
||||||
|
}
|
||||||
|
dispatcher.Push(instance.ServerID, dispatcherData)
|
||||||
|
|
||||||
|
middleware.Success(ctx)
|
||||||
|
}
|
||||||
61
routers/api/v1/admin/instance/delete.go
Normal file
61
routers/api/v1/admin/instance/delete.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package instances
|
||||||
|
|
||||||
|
import (
|
||||||
|
"megrez/models"
|
||||||
|
"megrez/routers/api/v1/middleware"
|
||||||
|
"megrez/services/database"
|
||||||
|
"megrez/services/dispatcher"
|
||||||
|
"megrez/services/redis"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
)
|
||||||
|
|
||||||
|
func deleteHandler(ctx iris.Context) {
|
||||||
|
l.SetFunction("deleteHandler")
|
||||||
|
|
||||||
|
id, err := ctx.Params().GetUint("id")
|
||||||
|
if err != nil {
|
||||||
|
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
instance := models.Instances{
|
||||||
|
ID: id,
|
||||||
|
}
|
||||||
|
result := database.DB.First(&instance)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("get instance error: %v", result.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeInstanceDeleteError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
status := instance.Status
|
||||||
|
if models.InstanceIngStatusCheck(status) {
|
||||||
|
middleware.Error(ctx, middleware.CodeInstanceStatusError, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result = database.DB.Model(&instance).Update("status", models.InstanceDeleting)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("update instance status error: %v", result.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeServerSaveError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if status == models.InstanceRunning || status == models.InstancePaused {
|
||||||
|
redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(instance.ServerID)), int64(instance.GpuCount))
|
||||||
|
}
|
||||||
|
redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(instance.ServerID)), int64(instance.VolumeSize+30))
|
||||||
|
|
||||||
|
// TODO: Price calculation
|
||||||
|
|
||||||
|
dispatcherData := dispatcher.Data{
|
||||||
|
Type: dispatcher.Delete,
|
||||||
|
Status: status,
|
||||||
|
InstanceID: instance.ID,
|
||||||
|
}
|
||||||
|
dispatcher.Push(instance.ServerID, dispatcherData)
|
||||||
|
|
||||||
|
middleware.Success(ctx)
|
||||||
|
}
|
||||||
53
routers/api/v1/admin/instance/detail.go
Normal file
53
routers/api/v1/admin/instance/detail.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package instances
|
||||||
|
|
||||||
|
import (
|
||||||
|
"megrez/models"
|
||||||
|
"megrez/routers/api/v1/middleware"
|
||||||
|
"megrez/services/database"
|
||||||
|
"megrez/services/redis"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
)
|
||||||
|
|
||||||
|
func detailHandler(ctx iris.Context) {
|
||||||
|
l.SetFunction("detailHandler")
|
||||||
|
|
||||||
|
id, err := ctx.Params().GetUint("id")
|
||||||
|
if err != nil {
|
||||||
|
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
instance := models.Instances{
|
||||||
|
ID: id,
|
||||||
|
}
|
||||||
|
result := database.DB.Select("id", "user_id", "server_id", "cpu_only", "gpu_count", "volume_size", "ssh_address", "ssh_passwd", "tensor_board_address", "grafana_address", "status", "image_name", "created_at").First(&instance)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("detail instance error: %v", result.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeInstanceDetailError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
server := models.Servers{
|
||||||
|
ID: instance.ServerID,
|
||||||
|
}
|
||||||
|
result = database.DB.Select("name", "gpu_type", "gpu_num").First(&server)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("detail server error: %v", result.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeInstanceDetailError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
redis.RawDB.Get(ctx, "remain_gpu:server:"+strconv.Itoa(int(instance.ServerID))).Scan(&server.GpuUsed)
|
||||||
|
server.GpuUsed = server.GpuNum - server.GpuUsed
|
||||||
|
|
||||||
|
res := instanceStruct{
|
||||||
|
Instances: instance,
|
||||||
|
GpuType: server.GpuType,
|
||||||
|
GpuNum: server.GpuNum,
|
||||||
|
GpuUsed: server.GpuUsed,
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware.Result(ctx, res)
|
||||||
|
}
|
||||||
49
routers/api/v1/admin/instance/label.go
Normal file
49
routers/api/v1/admin/instance/label.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package instances
|
||||||
|
|
||||||
|
import (
|
||||||
|
"megrez/models"
|
||||||
|
"megrez/routers/api/v1/middleware"
|
||||||
|
"megrez/services/database"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
)
|
||||||
|
|
||||||
|
type labelReqStruct struct {
|
||||||
|
Label string `json:"label"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func labelHandler(ctx iris.Context) {
|
||||||
|
l.SetFunction("labelHandler")
|
||||||
|
|
||||||
|
id, err := ctx.Params().GetUint("id")
|
||||||
|
if err != nil {
|
||||||
|
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req labelReqStruct
|
||||||
|
err = ctx.ReadJSON(&req)
|
||||||
|
if err != nil {
|
||||||
|
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
instance := models.Instances{
|
||||||
|
ID: id,
|
||||||
|
}
|
||||||
|
result := database.DB.Where(&instance).First(&instance)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("query instance error: %v", result.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeInstanceQueryError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result = database.DB.Model(&instance).Update("label", req.Label)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("save instance error: %v", result.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeInstanceSaveError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware.Success(ctx)
|
||||||
|
}
|
||||||
88
routers/api/v1/admin/instance/list.go
Normal file
88
routers/api/v1/admin/instance/list.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package instances
|
||||||
|
|
||||||
|
import (
|
||||||
|
"megrez/models"
|
||||||
|
"megrez/routers/api/v1/middleware"
|
||||||
|
"megrez/services/database"
|
||||||
|
"megrez/services/redis"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
)
|
||||||
|
|
||||||
|
func listHandler(ctx iris.Context) {
|
||||||
|
l.SetFunction("listHandler")
|
||||||
|
var err error
|
||||||
|
|
||||||
|
offset := 0
|
||||||
|
limit := 20
|
||||||
|
offsetStr := ctx.URLParam("offset")
|
||||||
|
limitStr := ctx.URLParam("limit")
|
||||||
|
|
||||||
|
if offsetStr != "" {
|
||||||
|
offset, err = strconv.Atoi(offsetStr)
|
||||||
|
if err != nil {
|
||||||
|
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if limitStr != "" {
|
||||||
|
limit, err = strconv.Atoi(limitStr)
|
||||||
|
if err != nil {
|
||||||
|
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
var instances []models.Instances
|
||||||
|
totalResult := database.DB.Model(&models.Instances{}).Count(&total)
|
||||||
|
if totalResult.Error != nil {
|
||||||
|
l.Error("list instances error: %v", totalResult.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeInstanceListError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := database.DB.Limit(limit).Offset(offset).Select("id", "user_id", "server_id", "cpu_only", "gpu_count", "volume_size", "ssh_address", "ssh_passwd", "jupyter_address", "tensor_board_address", "grafana_address", "status", "image_name", "label", "created_at").Order("id").Find(&instances)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("list instances error: %v", result.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeInstanceListError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := make([]instanceStruct, len(instances))
|
||||||
|
for i, instance := range instances {
|
||||||
|
res[i] = instanceStruct{
|
||||||
|
Instances: instance,
|
||||||
|
}
|
||||||
|
user := models.Users{
|
||||||
|
ID: instance.UserID,
|
||||||
|
}
|
||||||
|
result := database.DB.Select("username").First(&user)
|
||||||
|
if result.Error == nil {
|
||||||
|
res[i].Username = user.Username
|
||||||
|
} else {
|
||||||
|
l.Error("query user %d error: %v", instance.UserID, result.Error)
|
||||||
|
}
|
||||||
|
server := models.Servers{
|
||||||
|
ID: instance.ServerID,
|
||||||
|
}
|
||||||
|
result = database.DB.Select("name", "gpu_type", "gpu_num", "cpu_count_per_gpu", "memory_per_gpu").First(&server)
|
||||||
|
if result.Error == nil {
|
||||||
|
|
||||||
|
redis.RawDB.Get(ctx, "remain_gpu:server:"+strconv.Itoa(int(instance.ServerID))).Scan(&server.GpuUsed)
|
||||||
|
server.GpuUsed = server.GpuNum - server.GpuUsed
|
||||||
|
|
||||||
|
res[i].ServerName = server.Name
|
||||||
|
res[i].GpuType = server.GpuType
|
||||||
|
res[i].GpuNum = server.GpuNum
|
||||||
|
res[i].GpuUsed = server.GpuUsed
|
||||||
|
res[i].CpuCountPerGpu = server.CpuCountPerGpu
|
||||||
|
res[i].MemoryPerGpu = server.MemoryPerGpu
|
||||||
|
} else {
|
||||||
|
l.Error("query server %d error: %v", instance.ServerID, result.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware.ResultWithTotal(ctx, res, total)
|
||||||
|
}
|
||||||
128
routers/api/v1/admin/instance/modify.go
Normal file
128
routers/api/v1/admin/instance/modify.go
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
package instances
|
||||||
|
|
||||||
|
import (
|
||||||
|
"megrez/models"
|
||||||
|
"megrez/routers/api/v1/middleware"
|
||||||
|
"megrez/services/database"
|
||||||
|
"megrez/services/dispatcher"
|
||||||
|
"megrez/services/redis"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
)
|
||||||
|
|
||||||
|
type modifyReqStruct struct {
|
||||||
|
CpuOnly bool `json:"cpu_only"`
|
||||||
|
GpuCount *int `json:"gpu_count"`
|
||||||
|
VolumeSize *int `json:"volume_size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func modifyHandler(ctx iris.Context) {
|
||||||
|
l.SetFunction("modifyHandler")
|
||||||
|
|
||||||
|
id, err := ctx.Params().GetUint("id")
|
||||||
|
if err != nil {
|
||||||
|
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req modifyReqStruct
|
||||||
|
err = ctx.ReadJSON(&req)
|
||||||
|
if err != nil {
|
||||||
|
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.GpuCount != nil {
|
||||||
|
if *req.GpuCount < 0 {
|
||||||
|
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.VolumeSize != nil {
|
||||||
|
if *req.VolumeSize < 50 {
|
||||||
|
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
instance := models.Instances{
|
||||||
|
ID: id,
|
||||||
|
}
|
||||||
|
result := database.DB.First(&instance)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("query instance error: %v", result.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeInstanceQueryError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if instance.Status != models.InstanceStopped {
|
||||||
|
middleware.Error(ctx, middleware.CodeInstanceStatusError, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.CpuOnly == instance.CpuOnly && req.CpuOnly {
|
||||||
|
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
server := models.Servers{
|
||||||
|
ID: instance.ServerID,
|
||||||
|
}
|
||||||
|
result = database.DB.First(&server)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("query server error: %v", result.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.GpuCount != nil {
|
||||||
|
remainGpu, err := redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(-*req.GpuCount)).Result()
|
||||||
|
if err != nil {
|
||||||
|
l.Error("incrby gpu num error: %v", err)
|
||||||
|
middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if remainGpu < 0 {
|
||||||
|
redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(*req.GpuCount))
|
||||||
|
middleware.Error(ctx, middleware.CodeResourceInsufficient, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.VolumeSize != nil {
|
||||||
|
remainVolume, err := redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID)), int64(instance.VolumeSize-*req.VolumeSize)).Result()
|
||||||
|
if err != nil {
|
||||||
|
l.Error("incrby volume size error: %v", err)
|
||||||
|
middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if remainVolume < 0 {
|
||||||
|
redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID)), int64(*req.VolumeSize-instance.VolumeSize))
|
||||||
|
middleware.Error(ctx, middleware.CodeResourceInsufficient, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
status := instance.Status
|
||||||
|
result = database.DB.Model(&instance).Update("status", models.InstanceModifying)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("update instance status error: %v", result.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeServerSaveError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatcherData := dispatcher.Data{
|
||||||
|
Type: dispatcher.Modify,
|
||||||
|
InstanceID: instance.ID,
|
||||||
|
Status: status,
|
||||||
|
|
||||||
|
CpuOnly: req.CpuOnly,
|
||||||
|
GpuCount: req.GpuCount,
|
||||||
|
VolumeSize: req.VolumeSize,
|
||||||
|
}
|
||||||
|
dispatcher.Push(instance.ServerID, dispatcherData)
|
||||||
|
|
||||||
|
middleware.Success(ctx)
|
||||||
|
}
|
||||||
37
routers/api/v1/admin/instance/routers.go
Normal file
37
routers/api/v1/admin/instance/routers.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package instances
|
||||||
|
|
||||||
|
import (
|
||||||
|
"megrez/models"
|
||||||
|
"megrez/routers/api/v1/middleware"
|
||||||
|
"megrez/services/logger"
|
||||||
|
|
||||||
|
_logger "megrez/libs/logger"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12/core/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
var l *_logger.LoggerStruct
|
||||||
|
|
||||||
|
type instanceStruct struct {
|
||||||
|
models.Instances
|
||||||
|
Username string `json:"username"`
|
||||||
|
ServerName string `json:"server_name"`
|
||||||
|
GpuType string `json:"gpu_type"`
|
||||||
|
GpuNum int `json:"gpu_num"`
|
||||||
|
GpuUsed int `json:"gpu_used"`
|
||||||
|
CpuCountPerGpu int `json:"cpu_count_per_gpu"`
|
||||||
|
MemoryPerGpu int `json:"memory_per_gpu"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitInstances(party router.Party) {
|
||||||
|
l = logger.Logger.Clone()
|
||||||
|
l.SetModel("Http.API.V1.Admin.Instance")
|
||||||
|
|
||||||
|
party.Get("/", listHandler)
|
||||||
|
party.Get("/{id:uint}", detailHandler)
|
||||||
|
party.Put("/{id:uint}", middleware.SuperAdminCheck, controlHandler)
|
||||||
|
party.Post("/", middleware.SuperAdminCheck, addHandler)
|
||||||
|
party.Post("/{id:uint}", middleware.SuperAdminCheck, modifyHandler)
|
||||||
|
party.Post("/{id:uint}/label", middleware.SuperAdminCheck, labelHandler)
|
||||||
|
party.Delete("/{id:uint}", middleware.SuperAdminCheck, deleteHandler)
|
||||||
|
}
|
||||||
20
routers/api/v1/admin/routers.go
Normal file
20
routers/api/v1/admin/routers.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"megrez/routers/api/v1/admin/images"
|
||||||
|
instances "megrez/routers/api/v1/admin/instance"
|
||||||
|
"megrez/routers/api/v1/admin/servers"
|
||||||
|
"megrez/routers/api/v1/admin/users"
|
||||||
|
"megrez/routers/api/v1/middleware"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12/core/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitAdmin(party router.Party) {
|
||||||
|
party.Use(middleware.Auth, middleware.AuthCheck, middleware.AdminCheck)
|
||||||
|
|
||||||
|
users.InitUser(party.Party("/users"))
|
||||||
|
servers.InitServer(party.Party("/servers"))
|
||||||
|
instances.InitInstances(party.Party("/instances"))
|
||||||
|
images.InitImages(party.Party("/images"))
|
||||||
|
}
|
||||||
56
routers/api/v1/admin/servers/add.go
Normal file
56
routers/api/v1/admin/servers/add.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package servers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"megrez/models"
|
||||||
|
"megrez/routers/api/v1/middleware"
|
||||||
|
"megrez/services/database"
|
||||||
|
"megrez/services/redis"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addHandler(ctx iris.Context) {
|
||||||
|
l.SetFunction("addHandler")
|
||||||
|
|
||||||
|
var s serverStruct
|
||||||
|
if err := ctx.ReadJSON(&s); err != nil {
|
||||||
|
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Name == "" || s.IP == "" || s.Port == 0 || s.Apikey == "" || s.GpuType == "" || s.GpuNum == 0 || s.GpuDirverVersion == "" || s.GpuCudaVersion == "" || s.CpuCpuntPerGpu == 0 || s.MemoryPerGpu == 0 || s.VolumeTotal == 0 {
|
||||||
|
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Debug("add server: %+v", s)
|
||||||
|
|
||||||
|
server := models.Servers{
|
||||||
|
Name: s.Name,
|
||||||
|
IP: s.IP,
|
||||||
|
Port: s.Port,
|
||||||
|
Apikey: s.Apikey,
|
||||||
|
GpuType: s.GpuType,
|
||||||
|
GpuNum: s.GpuNum,
|
||||||
|
GpuDriverVersion: s.GpuDirverVersion,
|
||||||
|
GpuCudaVersion: s.GpuCudaVersion,
|
||||||
|
|
||||||
|
CpuCountPerGpu: s.CpuCpuntPerGpu,
|
||||||
|
MemoryPerGpu: s.MemoryPerGpu,
|
||||||
|
VolumeTotal: s.VolumeTotal,
|
||||||
|
Price: s.Price,
|
||||||
|
PriceVolume: s.PriceVolume,
|
||||||
|
}
|
||||||
|
result := database.DB.Create(&server)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("add server error: %v", result.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeAdminServerAddEditError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(server.GpuNum))
|
||||||
|
redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID)), int64(server.VolumeTotal))
|
||||||
|
|
||||||
|
middleware.Success(ctx)
|
||||||
|
}
|
||||||
43
routers/api/v1/admin/servers/delete.go
Normal file
43
routers/api/v1/admin/servers/delete.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package servers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"megrez/models"
|
||||||
|
"megrez/routers/api/v1/middleware"
|
||||||
|
"megrez/services/database"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
)
|
||||||
|
|
||||||
|
func deleteHandler(ctx iris.Context) {
|
||||||
|
l.SetFunction("deleteHandler")
|
||||||
|
|
||||||
|
id, err := ctx.Params().GetUint("id")
|
||||||
|
if err != nil {
|
||||||
|
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var instances []models.Instances
|
||||||
|
result := database.DB.Where("server_id = ?", id).Find(&instances)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("get instances error: %v", result.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeAdminServerDeleteError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(instances) > 0 {
|
||||||
|
middleware.Error(ctx, middleware.CodeAdminServerInstanceError, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
server := models.Servers{
|
||||||
|
ID: id,
|
||||||
|
}
|
||||||
|
result = database.DB.Delete(&server)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("delete server error: %v", result.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeAdminServerDeleteError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware.Success(ctx)
|
||||||
|
}
|
||||||
39
routers/api/v1/admin/servers/detail.go
Normal file
39
routers/api/v1/admin/servers/detail.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package servers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"megrez/models"
|
||||||
|
"megrez/routers/api/v1/middleware"
|
||||||
|
"megrez/services/database"
|
||||||
|
"megrez/services/redis"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
)
|
||||||
|
|
||||||
|
func detailHandler(ctx iris.Context) {
|
||||||
|
l.SetFunction("detailHandler")
|
||||||
|
|
||||||
|
id, err := ctx.Params().GetUint("id")
|
||||||
|
if err != nil {
|
||||||
|
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
server := models.Servers{
|
||||||
|
ID: id,
|
||||||
|
}
|
||||||
|
result := database.DB.First(&server)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("detail server error: %v", result.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeAdminServerDetailError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
redis.RawDB.Get(ctx, "remain_gpu:server:"+strconv.Itoa(int(id))).Scan(&server.GpuUsed)
|
||||||
|
server.GpuUsed = server.GpuNum - server.GpuUsed
|
||||||
|
|
||||||
|
redis.RawDB.Get(ctx, "remain_volume:server:"+strconv.Itoa(int(id))).Scan(&server.VolumeUsed)
|
||||||
|
server.VolumeUsed = server.VolumeTotal - server.VolumeUsed
|
||||||
|
|
||||||
|
middleware.Result(ctx, server)
|
||||||
|
}
|
||||||
62
routers/api/v1/admin/servers/list.go
Normal file
62
routers/api/v1/admin/servers/list.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package servers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"megrez/models"
|
||||||
|
"megrez/routers/api/v1/middleware"
|
||||||
|
"megrez/services/database"
|
||||||
|
"megrez/services/redis"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
)
|
||||||
|
|
||||||
|
func listHandler(ctx iris.Context) {
|
||||||
|
l.SetFunction("listHandler")
|
||||||
|
var err error
|
||||||
|
|
||||||
|
offset := 0
|
||||||
|
limit := 20
|
||||||
|
offsetStr := ctx.URLParam("offset")
|
||||||
|
limitStr := ctx.URLParam("limit")
|
||||||
|
|
||||||
|
if offsetStr != "" {
|
||||||
|
offset, err = strconv.Atoi(offsetStr)
|
||||||
|
if err != nil {
|
||||||
|
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if limitStr != "" {
|
||||||
|
limit, err = strconv.Atoi(limitStr)
|
||||||
|
if err != nil {
|
||||||
|
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
var servers []models.Servers
|
||||||
|
totalResult := database.DB.Model(&models.Servers{}).Count(&total)
|
||||||
|
if totalResult.Error != nil {
|
||||||
|
l.Error("list servers error: %v", totalResult.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeAdminServerListError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := database.DB.Limit(limit).Offset(offset).Select("id", "name", "ip", "gpu_type", "gpu_num", "gpu_driver_version", "gpu_cuda_version", "cpu_count_per_gpu", "memory_per_gpu", "volume_total", "price", "price_volume", "gpu_used", "volume_used", "created_at").Order("id").Find(&servers)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("list servers error: %v", result.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeAdminServerListError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, server := range servers {
|
||||||
|
redis.RawDB.Get(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID))).Scan(&servers[i].GpuUsed)
|
||||||
|
servers[i].GpuUsed = server.GpuNum - servers[i].GpuUsed
|
||||||
|
|
||||||
|
redis.RawDB.Get(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID))).Scan(&servers[i].VolumeUsed)
|
||||||
|
servers[i].VolumeUsed = server.VolumeTotal - servers[i].VolumeUsed
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware.ResultWithTotal(ctx, servers, total)
|
||||||
|
}
|
||||||
84
routers/api/v1/admin/servers/modify.go
Normal file
84
routers/api/v1/admin/servers/modify.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package servers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"megrez/models"
|
||||||
|
"megrez/routers/api/v1/middleware"
|
||||||
|
"megrez/services/database"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
)
|
||||||
|
|
||||||
|
func modifyHandler(ctx iris.Context) {
|
||||||
|
l.SetFunction("modifyHandler")
|
||||||
|
|
||||||
|
id, err := ctx.Params().GetUint("id")
|
||||||
|
if err != nil {
|
||||||
|
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req serverStruct
|
||||||
|
if err := ctx.ReadJSON(&req); err != nil {
|
||||||
|
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
server := models.Servers{
|
||||||
|
ID: id,
|
||||||
|
}
|
||||||
|
result := database.DB.First(&server)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("query server error: %v", result.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeAdminServerAddEditError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Name != "" && req.Name != server.Name {
|
||||||
|
server.Name = req.Name
|
||||||
|
}
|
||||||
|
if req.IP != "" && req.IP != server.IP {
|
||||||
|
server.IP = req.IP
|
||||||
|
}
|
||||||
|
if req.Port != 0 && req.Port != server.Port {
|
||||||
|
server.Port = req.Port
|
||||||
|
}
|
||||||
|
if req.Apikey != "" && req.Apikey != server.Apikey {
|
||||||
|
server.Apikey = req.Apikey
|
||||||
|
}
|
||||||
|
if req.GpuType != "" && req.GpuType != server.GpuType {
|
||||||
|
server.GpuType = req.GpuType
|
||||||
|
}
|
||||||
|
if req.GpuNum != 0 && req.GpuNum != server.GpuNum {
|
||||||
|
server.GpuNum = req.GpuNum
|
||||||
|
}
|
||||||
|
if req.GpuDirverVersion != "" && req.GpuDirverVersion != server.GpuDriverVersion {
|
||||||
|
server.GpuDriverVersion = req.GpuDirverVersion
|
||||||
|
}
|
||||||
|
if req.GpuCudaVersion != "" && req.GpuCudaVersion != server.GpuCudaVersion {
|
||||||
|
server.GpuCudaVersion = req.GpuCudaVersion
|
||||||
|
}
|
||||||
|
if req.CpuCpuntPerGpu != 0 && req.CpuCpuntPerGpu != server.CpuCountPerGpu {
|
||||||
|
server.CpuCountPerGpu = req.CpuCpuntPerGpu
|
||||||
|
}
|
||||||
|
if req.MemoryPerGpu != 0 && req.MemoryPerGpu != server.MemoryPerGpu {
|
||||||
|
server.MemoryPerGpu = req.MemoryPerGpu
|
||||||
|
}
|
||||||
|
if req.VolumeTotal != 0 && req.VolumeTotal != server.VolumeTotal {
|
||||||
|
server.VolumeTotal = req.VolumeTotal
|
||||||
|
}
|
||||||
|
if req.Price != 0 && req.Price != server.Price {
|
||||||
|
server.Price = req.Price
|
||||||
|
}
|
||||||
|
if req.PriceVolume != 0 && req.PriceVolume != server.PriceVolume {
|
||||||
|
server.PriceVolume = req.PriceVolume
|
||||||
|
}
|
||||||
|
|
||||||
|
result = database.DB.Save(&server)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("save server error: %v", result.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeAdminServerAddEditError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware.Success(ctx)
|
||||||
|
}
|
||||||
40
routers/api/v1/admin/servers/routers.go
Normal file
40
routers/api/v1/admin/servers/routers.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package servers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/kataras/iris/v12/core/router"
|
||||||
|
|
||||||
|
"megrez/routers/api/v1/middleware"
|
||||||
|
"megrez/services/logger"
|
||||||
|
|
||||||
|
_logger "megrez/libs/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var l *_logger.LoggerStruct
|
||||||
|
|
||||||
|
type serverStruct struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
IP string `json:"ip,omitempty"`
|
||||||
|
Port int `json:"port,omitempty"`
|
||||||
|
Apikey string `json:"apikey,omitempty"`
|
||||||
|
GpuType string `json:"gpu_type,omitempty"`
|
||||||
|
GpuNum int `json:"gpu_num,omitempty"`
|
||||||
|
GpuDirverVersion string `json:"gpu_driver_version,omitempty"`
|
||||||
|
GpuCudaVersion string `json:"gpu_cuda_version,omitempty"`
|
||||||
|
|
||||||
|
CpuCpuntPerGpu int `json:"cpu_count_per_gpu,omitempty"`
|
||||||
|
MemoryPerGpu int `json:"memory_per_gpu,omitempty"` // Unit `GB`
|
||||||
|
VolumeTotal int `json:"volume_total,omitempty"` // Unit `GB`
|
||||||
|
Price float64 `json:"price,omitempty"` // 1 GPU Per Hour
|
||||||
|
PriceVolume float64 `json:"price_volume,omitempty"` // 1GB Per Hour
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitServer(party router.Party) {
|
||||||
|
l = logger.Logger.Clone()
|
||||||
|
l.SetModel("Http.API.V1.Admin.Servers")
|
||||||
|
|
||||||
|
party.Get("/", listHandler)
|
||||||
|
party.Get("/{id:uint}", detailHandler)
|
||||||
|
party.Post("/", middleware.SuperAdminCheck, addHandler)
|
||||||
|
party.Post("/{id:uint}", middleware.SuperAdminCheck, modifyHandler)
|
||||||
|
party.Delete("/{id:uint}", middleware.SuperAdminCheck, deleteHandler)
|
||||||
|
}
|
||||||
1
routers/api/v1/admin/users/add.go
Normal file
1
routers/api/v1/admin/users/add.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package users
|
||||||
49
routers/api/v1/admin/users/delete.go
Normal file
49
routers/api/v1/admin/users/delete.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package users
|
||||||
|
|
||||||
|
import (
|
||||||
|
"megrez/models"
|
||||||
|
"megrez/routers/api/v1/middleware"
|
||||||
|
"megrez/services/database"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
)
|
||||||
|
|
||||||
|
func deleteHandler(ctx iris.Context) {
|
||||||
|
l.SetFunction("deleteHandler")
|
||||||
|
|
||||||
|
id, err := ctx.Params().GetUint("id")
|
||||||
|
if err != nil {
|
||||||
|
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if id == 1 {
|
||||||
|
middleware.Error(ctx, middleware.CodeAdminUserDeleteError, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var instances []models.Instances
|
||||||
|
result := database.DB.Where("user_id = ?", id).Find(&instances)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("get user instances error: %v", result.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeAdminUserDeleteError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(instances) > 0 {
|
||||||
|
middleware.Error(ctx, middleware.CodeAdminUserInstanceNoEmpty, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user := models.Users{
|
||||||
|
ID: id,
|
||||||
|
}
|
||||||
|
result = database.DB.Delete(&user)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("delete user error: %v", result.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeAdminUserDeleteError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware.Success(ctx)
|
||||||
|
}
|
||||||
31
routers/api/v1/admin/users/detail.go
Normal file
31
routers/api/v1/admin/users/detail.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package users
|
||||||
|
|
||||||
|
import (
|
||||||
|
"megrez/models"
|
||||||
|
"megrez/routers/api/v1/middleware"
|
||||||
|
"megrez/services/database"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
)
|
||||||
|
|
||||||
|
func detailHandler(ctx iris.Context) {
|
||||||
|
l.SetFunction("detailHandler")
|
||||||
|
|
||||||
|
id, err := ctx.Params().GetUint("id")
|
||||||
|
if err != nil {
|
||||||
|
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user := models.Users{
|
||||||
|
ID: id,
|
||||||
|
}
|
||||||
|
result := database.DB.Select("id", "username", "email", "role", "balance", "created_at").First(&user)
|
||||||
|
if result.Error != nil {
|
||||||
|
l.Error("detail user error: %v", result.Error)
|
||||||
|
middleware.Error(ctx, middleware.CodeAdminUserDetailError, iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware.Result(ctx, user)
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user