mirror of
https://github.com/Mujinniao/Anonima.git
synced 2025-12-19 00:14:40 +08:00
Delete picx directory
This commit is contained in:
674
picx/LICENSE
674
picx/LICENSE
@@ -1,674 +0,0 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 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 General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<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 General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
@@ -1,75 +0,0 @@
|
||||
<a href="https://picx.xpoet.cn" >
|
||||
<img width="100" align="right" alt="PicX" src="https://cdn.staticaly.com/gh/XPoet/image-hosting@master/PicX/picx-logo.png">
|
||||
</a>
|
||||
|
||||
# PicX 图床
|
||||
|
||||
[](https://github.com/XPoet)
|
||||
[](https://github.com/XPoet/picx/releases)
|
||||
[](https://github.com/XPoet/picx/blob/master/LICENSE)
|
||||
[](https://github.com/XPoet/picx)
|
||||
[](https://github.com/XPoet/picx/issues)
|
||||
[](https://github.com/XPoet/picx/actions/workflows/deploy.yml)
|
||||
[](https://github.com/lin-123/javascript)
|
||||
|
||||
**[PicX](https://picx.xpoet.cn)** 是一款基于 GitHub API 开发的具有 CDN 加速功能的图床管理工具
|
||||
|
||||
|
||||
|
||||
> 在线使用入口 **https://picx.xpoet.cn**
|
||||
|
||||
**灵魂拷问,你为图床问题烦恼过吗?**
|
||||
|
||||
- 用 Hexo、VuePress、Hugo 等静态博客写文章,不知图片保存到哪里去...
|
||||
- 特意花钱租云服务器托管图片,太贵划不来,而且上传配置好繁琐...
|
||||
- 网上复制的心仪图片的链接,用着用着某一天就失效了...
|
||||
- 使用其他的付费图床,速度慢,容量小,还限时、限流量...
|
||||
- 想找一款真正免费、稳定、不限容量、访问速度还很快的图床...
|
||||
- ......
|
||||
|
||||
那么,快来试试 **[PicX 图床](https://picx.xpoet.cn)** 吧,专为技术博主打造,谁用谁知道~
|
||||
|
||||
只需选择一个 GitHub 仓库作为图床,然后在 **[PicX 官网](https://picx.xpoet.cn/)** 完成 Token 绑定和相应配置就能使用了。
|
||||
|
||||
**[PicX 图床](https://picx.xpoet.cn)** 浏览器在线使用,免下载&安装,如此简单。
|
||||
|
||||
🆓 免费 🆓 🏆 稳定 🏆 🚀 极速 🚀 🔒 安全 🔒
|
||||
|
||||
## 功能特性 | Features
|
||||
|
||||
- [x] 支持 **拖拽**、**复制粘贴**、**选择文件** 等方式进行选择图片
|
||||
- [x] 支持图片 **重命名**、**哈希化**(确保图片名唯一)和 **设置命名前缀**
|
||||
- [x] 支持 **批量上传图片**、**批量删除图片** 和 **批量复制图片外链**
|
||||
- [x] 支持 **多级目录** 管理 (创建多级目录 / 查看多级目录图片)
|
||||
- [x] 支持 **一键复制** 图片外链和 **一键转换 Markdown 格式**
|
||||
- [x] 支持 **图床管理**(对仓库图片的 **增删改查**)
|
||||
- [x] 支持 **图片压缩** (内置三款压缩算法,可在上传前自动压缩,效果极佳)
|
||||
- [x] 支持 **暗夜模式** (自由切换 / 自动切换)
|
||||
- [x] 支持 **PWA**
|
||||
- [ ] i18n
|
||||
- [ ] 设置图片水印
|
||||
- [ ] 支持其他 Git 厂商 (例如:Gitee / Coding)
|
||||
|
||||
## 使用教程 | Using the tutorial
|
||||
|
||||
官方文档 >> https://picx-docs.xpoet.cn
|
||||
|
||||
## 快速开始 | Get start
|
||||
|
||||
通过阅读官方文档的快速开始教程,可帮助你迅速上手 PicX 图床
|
||||
|
||||
https://picx-docs.xpoet.cn/tutorial/get-start.html
|
||||
|
||||
## 贡献 | Contribution
|
||||
|
||||
欢迎各种形式的贡献,包括但不限于:美化界面、增加功能、改进代码、 修复 Bug 等
|
||||
|
||||
## 反馈 | Feedback
|
||||
|
||||
在使用过程中,如遇问题,请仔细阅读 **[官方文档](https://picx-docs.xpoet.cn)** ,或给作者提 **[Issue](https://github.com/XPoet/picx/issues)**
|
||||
|
||||
## 许可 | License
|
||||
|
||||
**[GPL-3.0](https://github.com/XPoet/picx/blob/master/LICENSE)**
|
||||
|
||||
Copyright © 2020-Present PicX Dev Team
|
||||
@@ -1 +0,0 @@
|
||||
module.exports = { extends: ['@commitlint/config-conventional'] }
|
||||
@@ -1,18 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="./logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/@fancyapps/ui/dist/fancybox.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/@fancyapps/ui@4.0/dist/fancybox.umd.js"></script>
|
||||
<title>PicX 图床神器</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,63 +0,0 @@
|
||||
{
|
||||
"name": "picx",
|
||||
"version": "2.1.0",
|
||||
"private": false,
|
||||
"author": "XPoet",
|
||||
"license": "GPL-3.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/XPoet/picx/issues"
|
||||
},
|
||||
"homepage": "https://github.com/XPoet/picx#readme",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"release": "git push --tag && git push -u",
|
||||
"format": "prettier --write",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{vue,js,ts}": "eslint --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.0.4",
|
||||
"@yireen/squoosh-browser": "^1.0.7",
|
||||
"axios": "^0.21.4",
|
||||
"element-plus": "^2.2.5",
|
||||
"vue": "^3.2.18",
|
||||
"vue-router": "^4.0.11",
|
||||
"vuex": "^4.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^12.1.1",
|
||||
"@commitlint/config-conventional": "^12.1.1",
|
||||
"@types/node": "^15.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^4.22.0",
|
||||
"@typescript-eslint/parser": "^4.22.0",
|
||||
"@vitejs/plugin-vue": "^1.1.5",
|
||||
"@vue/compiler-sfc": "^3.2.18",
|
||||
"commitizen": "^4.2.3",
|
||||
"cz-conventional-changelog": "^3.3.0",
|
||||
"cz-customizable": "^6.3.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb-base": "^14.2.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"eslint-plugin-vue": "^9.1.1",
|
||||
"husky": "^6.0.0",
|
||||
"lint-staged": "^10.5.4",
|
||||
"prettier": "^2.2.1",
|
||||
"stylus": "^0.54.8",
|
||||
"typescript": "^4.1.5",
|
||||
"unplugin-auto-import": "^0.8.7",
|
||||
"unplugin-vue-components": "^0.19.6",
|
||||
"vite": "~2.7.13",
|
||||
"vite-plugin-pwa": "^0.11.2"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "./node_modules/cz-conventional-changelog"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
picx.xpoet.cn
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 36 KiB |
@@ -1,74 +0,0 @@
|
||||
<template>
|
||||
<el-config-provider :size="size" :z-index="zIndex" :locale="locale">
|
||||
<main-container />
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, reactive, toRefs } from 'vue'
|
||||
import { ElConfigProvider } from 'element-plus'
|
||||
import zhCn from 'element-plus/lib/locale/lang/zh-cn'
|
||||
import mainContainer from '@/components/main-container/main-container.vue'
|
||||
import setTheme from '@/utils/set-theme-mode'
|
||||
import { useStore } from '@/store'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'App',
|
||||
components: {
|
||||
ElConfigProvider,
|
||||
mainContainer
|
||||
},
|
||||
setup() {
|
||||
const store = useStore()
|
||||
|
||||
const data = reactive({
|
||||
zIndex: 3000,
|
||||
size: 'small', // large | default | small
|
||||
locale: zhCn
|
||||
})
|
||||
|
||||
const elementPlusSizeHandle = (width: number) => {
|
||||
if (width <= 600) {
|
||||
store.dispatch('SET_USER_SETTINGS', {
|
||||
elementPlusSize: 'small'
|
||||
})
|
||||
data.size = 'small'
|
||||
} else if (width <= 800) {
|
||||
store.dispatch('SET_USER_SETTINGS', {
|
||||
elementPlusSize: 'default'
|
||||
})
|
||||
data.size = 'default'
|
||||
} else {
|
||||
store.dispatch('SET_USER_SETTINGS', {
|
||||
elementPlusSize: 'large'
|
||||
})
|
||||
data.size = 'large'
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTheme()
|
||||
elementPlusSizeHandle(window.innerWidth)
|
||||
window.addEventListener('resize', (e: any) => {
|
||||
elementPlusSizeHandle(e.target.innerWidth)
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
...toRefs(data)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
#app {
|
||||
font-family Avenir, Helvetica, Arial, sans-serif
|
||||
-webkit-font-smoothing antialiased
|
||||
-moz-osx-font-smoothing grayscale
|
||||
box-sizing border-box
|
||||
position relative
|
||||
width 100%
|
||||
height 100%
|
||||
}
|
||||
</style>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB |
@@ -1,82 +0,0 @@
|
||||
import { computed } from 'vue'
|
||||
import axios from '@/utils/axios'
|
||||
import { store } from '@/store'
|
||||
import { getFileSuffix, isImage } from '@/utils/file-handle-helper'
|
||||
import structureImageObject from '@/utils/image-helper'
|
||||
|
||||
const userConfigInfo = computed(() => store.getters.getUserConfigInfo).value
|
||||
|
||||
/**
|
||||
* 获取指定路径(path)下的目录列表
|
||||
* @param path 路径
|
||||
*/
|
||||
export const getDirListByPath = (path: string = '') => {
|
||||
return new Promise((resolve) => {
|
||||
axios
|
||||
.get(
|
||||
`/repos/${userConfigInfo.owner}/${userConfigInfo.selectedRepos}/contents/${path}`,
|
||||
{
|
||||
params: {
|
||||
ref: userConfigInfo.selectedBranch
|
||||
}
|
||||
}
|
||||
)
|
||||
.then((res: any) => {
|
||||
if (res && res.status === 200 && res.data.length > 0) {
|
||||
resolve(
|
||||
res.data
|
||||
.filter((v: any) => v.type === 'dir')
|
||||
.map((x: any) => ({
|
||||
value: x.name,
|
||||
label: x.name
|
||||
}))
|
||||
)
|
||||
} else {
|
||||
resolve(null)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
resolve(null)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定路径(path)下的目录和图片
|
||||
* @param path
|
||||
*/
|
||||
export const getContentByReposPath = (path: string = '') => {
|
||||
return new Promise((resolve) => {
|
||||
axios
|
||||
.get(
|
||||
`/repos/${userConfigInfo.owner}/${userConfigInfo.selectedRepos}/contents/${path}`,
|
||||
{
|
||||
params: {
|
||||
ref: userConfigInfo.selectedBranch
|
||||
}
|
||||
}
|
||||
)
|
||||
.then((res: any) => {
|
||||
if (res && res.status === 200 && res.data.length > 0) {
|
||||
res.data
|
||||
.filter((v: any) => v.type === 'dir')
|
||||
.forEach((x: any) => store.dispatch('DIR_IMAGE_LIST_ADD_DIR', x.path))
|
||||
|
||||
setTimeout(() => {
|
||||
res.data
|
||||
.filter((v: any) => v.type === 'file' && isImage(getFileSuffix(v.name)))
|
||||
.forEach((x: any) =>
|
||||
store.dispatch('DIR_IMAGE_LIST_ADD_IMAGE', structureImageObject(x, path))
|
||||
)
|
||||
}, 100)
|
||||
|
||||
resolve(true)
|
||||
} else {
|
||||
resolve(null)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
resolve(null)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export enum deleteStatusEnum {
|
||||
deleted = 'deleted',
|
||||
allDeleted = 'allDeleted',
|
||||
deleteFail = 'deleteFail'
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
export interface DirModel {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export enum DirModeEnum {
|
||||
autoDir = 'autoDir',
|
||||
newDir = 'newDir',
|
||||
rootDir = 'rootDir',
|
||||
reposDir = 'reposDir'
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
enum ExternalLinkType {
|
||||
staticaly = 'staticaly',
|
||||
jsdelivr = 'jsdelivr',
|
||||
github = 'github',
|
||||
cloudflare = 'cloudflare'
|
||||
}
|
||||
|
||||
export default ExternalLinkType
|
||||
@@ -1,6 +0,0 @@
|
||||
const PICX_PREFIX = 'PICX_'
|
||||
|
||||
export const PICX_CONFIG = `${PICX_PREFIX}CONFIG`
|
||||
export const PICX_UPLOADED = `${PICX_PREFIX}UPLOADED`
|
||||
export const PICX_MANAGEMENT = `${PICX_PREFIX}MANAGEMENT_MULTI`
|
||||
export const PICX_SETTINGS = `${PICX_PREFIX}SETTINGS`
|
||||
@@ -1,68 +0,0 @@
|
||||
export enum UploadStatusEnum {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
uploaded = 'uploaded',
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
allUploaded = 'allUploaded',
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
uploadFail = 'uploadFail'
|
||||
}
|
||||
|
||||
export interface UploadedImageModel {
|
||||
type: string
|
||||
uuid: string
|
||||
sha: string
|
||||
dir: string
|
||||
path: string
|
||||
name: string
|
||||
size: any
|
||||
deleting: boolean
|
||||
is_transform_md: boolean
|
||||
checked: boolean
|
||||
github_url: string
|
||||
jsdelivr_cdn_url: string
|
||||
staticaly_cdn_url: string
|
||||
cloudflare_cdn_url: string
|
||||
}
|
||||
|
||||
export interface ToUploadImageModel {
|
||||
uuid: string
|
||||
|
||||
uploadStatus: {
|
||||
progress: number
|
||||
uploading: boolean
|
||||
}
|
||||
|
||||
imgData: {
|
||||
base64Content: string
|
||||
base64Url: string
|
||||
}
|
||||
|
||||
fileInfo: {
|
||||
compressedSize?: number | undefined
|
||||
originSize?: number | undefined
|
||||
size: number | undefined
|
||||
lastModified: number | undefined
|
||||
}
|
||||
|
||||
filename: {
|
||||
name: string
|
||||
hash: string
|
||||
suffix: string
|
||||
prefixName: string
|
||||
now: string
|
||||
initName: string
|
||||
newName: string
|
||||
isHashRename: boolean
|
||||
isRename: boolean
|
||||
isPrefix: boolean
|
||||
}
|
||||
|
||||
externalLink: {
|
||||
github: string
|
||||
jsdelivr: string
|
||||
staticaly: string
|
||||
cloudflare: string
|
||||
}
|
||||
|
||||
uploadedImg?: UploadedImageModel
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { DirModeEnum, DirModel } from './dir.model'
|
||||
|
||||
export interface ReposModel {
|
||||
value: string
|
||||
label: string
|
||||
desc?: string
|
||||
}
|
||||
|
||||
export interface BranchModel {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export enum BranchModeEnum {
|
||||
newBranch = 'newBranch',
|
||||
reposBranch = 'reposBranch'
|
||||
}
|
||||
|
||||
export interface UserConfigInfoModel {
|
||||
token: string
|
||||
owner: string
|
||||
email: string
|
||||
name: string
|
||||
avatarUrl: string
|
||||
selectedRepos: string
|
||||
reposList: ReposModel[]
|
||||
selectedBranch: string
|
||||
branchMode: BranchModeEnum
|
||||
branchList: BranchModel[]
|
||||
dirMode: DirModeEnum
|
||||
selectedDir: string
|
||||
selectedDirList: string[]
|
||||
dirList: DirModel[]
|
||||
loggingStatus: boolean
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { CompressEncoderMap } from '../../utils/compress'
|
||||
import ExternalLinkType from '@/common/model/external-link.model'
|
||||
|
||||
export interface UserSettingsModel {
|
||||
defaultHash: boolean
|
||||
defaultMarkdown: boolean
|
||||
defaultPrefix: boolean
|
||||
prefixName: string
|
||||
themeMode: 'auto' | 'light' | 'dark'
|
||||
autoLightThemeTime: string[]
|
||||
isCompress: boolean
|
||||
compressEncoder: CompressEncoderMap
|
||||
elementPlusSize: 'large' | 'default' | 'small'
|
||||
externalLinkType: ExternalLinkType
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
export declare type Recordable<T = any> = Record<string, T>
|
||||
|
||||
export declare interface ViteEnv {
|
||||
VITE_PORT?: number
|
||||
VITE_USE_PWA?: boolean
|
||||
VITE_PUBLIC_PATH?: string
|
||||
VITE_GLOB_APP_TITLE?: string
|
||||
VITE_GLOB_APP_SHORT_NAME?: string
|
||||
VITE_OPEN_BROWSER?: boolean
|
||||
VITE_CORS?: boolean
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
@import "../../style/base.styl"
|
||||
|
||||
.copy-external-link-box {
|
||||
position relative
|
||||
width 100%
|
||||
height 100%
|
||||
box-sizing border-box
|
||||
display flex
|
||||
justify-content space-between
|
||||
align-items flex-end
|
||||
padding 2rem
|
||||
|
||||
.markdown-icon-box {
|
||||
width 26rem
|
||||
height 20rem
|
||||
position relative
|
||||
cursor pointer
|
||||
|
||||
.markdown-icon {
|
||||
path {
|
||||
fill var(--markdown-icon-color)
|
||||
}
|
||||
|
||||
&.active {
|
||||
path {
|
||||
fill var(--markdown-icon-active-color)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.btn-box {
|
||||
position relative
|
||||
box-sizing border-box
|
||||
height 100%
|
||||
display flex
|
||||
justify-content space-between
|
||||
|
||||
.btn-item {
|
||||
height 20rem
|
||||
box-sizing border-box
|
||||
border-radius 5rem
|
||||
font-size 12rem
|
||||
cursor pointer
|
||||
transition all 0.3s ease
|
||||
}
|
||||
|
||||
.copy-url {
|
||||
box-sizing border-box
|
||||
border 1rem solid var(--default-text-color)
|
||||
color var(--default-text-color)
|
||||
padding 1rem 2rem
|
||||
margin-right 4rem
|
||||
|
||||
&:hover {
|
||||
background var(--default-text-color)
|
||||
color var(--background-color)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
<template>
|
||||
<div class="copy-external-link-box">
|
||||
<div>
|
||||
<el-tooltip
|
||||
:content="img.is_transform_md ? '点击转换普通外链' : '点击转换 Markdown 格式外链'"
|
||||
placement="top"
|
||||
>
|
||||
<div
|
||||
class="markdown-icon-box flex-center"
|
||||
@click="img.is_transform_md = !img.is_transform_md"
|
||||
>
|
||||
<svg
|
||||
v-if="!img.is_transform_md"
|
||||
t="1631782798077"
|
||||
class="markdown-icon"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="2861"
|
||||
width="26"
|
||||
height="26"
|
||||
>
|
||||
<path
|
||||
d="M92 192C42.24 192 0 232.128 0 282.016v459.968C0 791.904 42.24 832 92 832h840C981.76 832 1024 791.872 1024 741.984V282.016C1024 232.16 981.76 192 932 192z m0 64h840c16.512 0 28 12.256 28 26.016v459.968c0 13.76-11.52 26.016-28 26.016H92C75.488 768 64 755.744 64 741.984V282.016c0-13.76 11.52-25.984 28-25.984zM160 352v320h96v-212.992l96 127.008 96-127.04V672h96V352h-96l-96 128-96-128z m544 0v160h-96l144 160 144-160h-96v-160z"
|
||||
p-id="2862"
|
||||
fill="#808080"
|
||||
></path>
|
||||
</svg>
|
||||
<svg
|
||||
v-if="img.is_transform_md"
|
||||
t="1631784688556"
|
||||
class="markdown-icon active"
|
||||
viewBox="0 0 1280 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="3242"
|
||||
width="26"
|
||||
height="26"
|
||||
>
|
||||
<path
|
||||
d="M1187.6 118.2H92.4C41.4 118.2 0 159.6 0 210.4v603c0 51 41.4 92.4 92.4 92.4h1095.4c51 0 92.4-41.4 92.2-92.2V210.4c0-50.8-41.4-92.2-92.4-92.2zM677 721.2H554v-240l-123 153.8-123-153.8v240H184.6V302.8h123l123 153.8 123-153.8h123v418.4z m270.6 6.2L763 512H886V302.8h123V512H1132z"
|
||||
p-id="3243"
|
||||
fill="#3c3c3c"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="btn-box">
|
||||
<el-tooltip content="点击复制 Staticaly CDN 外链" placement="top">
|
||||
<span
|
||||
class="btn-item copy-url flex-center"
|
||||
@click="copyLink(externalLinkType.staticaly)"
|
||||
>
|
||||
Staticaly
|
||||
</span>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="点击复制 Cloudflare CDN 外链" placement="top">
|
||||
<span
|
||||
class="btn-item copy-url flex-center"
|
||||
@click="copyLink(externalLinkType.cloudflare)"
|
||||
>
|
||||
Cloudflare
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, computed, ref, onUpdated } from 'vue'
|
||||
import ExternalLinkType from '@/common/model/external-link.model'
|
||||
import { store } from '@/store'
|
||||
import { copyExternalLink } from '@/utils/external-link-handler'
|
||||
import { UploadedImageModel } from '@/common/model/upload.model'
|
||||
|
||||
const props = defineProps({
|
||||
imgObj: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
})
|
||||
|
||||
const userSettings = computed(() => store.getters.getUserSettings).value
|
||||
|
||||
let img = ref(props.imgObj as UploadedImageModel).value
|
||||
const externalLinkType = ExternalLinkType
|
||||
|
||||
const copyLink = (type: ExternalLinkType) => {
|
||||
copyExternalLink(img, type)
|
||||
}
|
||||
|
||||
onUpdated(() => {
|
||||
img = props.imgObj
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
img.is_transform_md = userSettings.defaultMarkdown
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import "copy-external-link.styl"
|
||||
</style>
|
||||
@@ -1,47 +0,0 @@
|
||||
@import '../../style/base.styl'
|
||||
|
||||
.folder-card {
|
||||
position relative
|
||||
width 110rem
|
||||
height 106rem
|
||||
display flex
|
||||
align-items center
|
||||
flex-direction column
|
||||
justify-content flex-start
|
||||
cursor pointer
|
||||
box-sizing border-box
|
||||
padding 3rem
|
||||
user-select none
|
||||
|
||||
&:hover {
|
||||
background var(--second-background-color)
|
||||
}
|
||||
|
||||
.icon {
|
||||
display flex
|
||||
align-items center
|
||||
justify-content center
|
||||
width 50rem
|
||||
height 50rem
|
||||
|
||||
svg {
|
||||
width 100%
|
||||
height 100%
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.text {
|
||||
width 90%
|
||||
font-size 14rem
|
||||
margin-top 5rem
|
||||
text-align center
|
||||
overflow hidden
|
||||
text-overflow ellipsis
|
||||
display -webkit-box
|
||||
-webkit-box-orient vertical
|
||||
-webkit-line-clamp 2
|
||||
word-wrap break-word
|
||||
word-break break-all
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
<template>
|
||||
<div class="folder-card" @dblclick="dblclickFolder">
|
||||
<el-tooltip
|
||||
v-if="mode === 'dir'"
|
||||
effect="dark"
|
||||
content="双击进入下一级目录"
|
||||
placement="top"
|
||||
>
|
||||
<div class="icon">
|
||||
<svg
|
||||
t="1639999626518"
|
||||
class="icon"
|
||||
viewBox="0 0 1228 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="3575"
|
||||
width="200"
|
||||
height="200"
|
||||
>
|
||||
<path
|
||||
d="M1196.987733 212.5824v540.0576c0 39.594667-34.474667 71.3728-76.765866 71.3728H323.242667c-51.780267 0-88.746667-46.762667-73.250134-92.808533l126.737067-375.808H70.417067C31.675733 355.362133 0 326.4512 0 291.089067V98.372267C0 63.044267 31.675733 34.0992 70.417067 34.0992h378.811733c26.7264 0 51.029333 13.9264 63.010133 35.703467l39.048534 71.406933H1120.256c42.257067 0 76.8 32.119467 76.8 71.3728"
|
||||
fill="#5398DF"
|
||||
p-id="3576"
|
||||
></path>
|
||||
<path
|
||||
d="M1128.721067 997.853867H68.266667a68.266667 68.266667 0 0 1-68.266667-68.266667V280.3712a68.266667 68.266667 0 0 1 68.266667-68.266667h1060.4544a68.266667 68.266667 0 0 1 68.266666 68.266667V929.5872a68.266667 68.266667 0 0 1-68.266666 68.266667"
|
||||
fill="#85BCFF"
|
||||
p-id="3577"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
|
||||
<div class="icon" v-if="mode === 'back'">
|
||||
<svg
|
||||
t="1640264285200"
|
||||
class="icon"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="29312"
|
||||
width="200"
|
||||
height="200"
|
||||
>
|
||||
<path
|
||||
d="M426.666667 384V213.333333l-298.666667 298.666667 298.666667 298.666667v-174.933334c213.333333 0 362.666667 68.266667 469.333333 217.6-42.666667-213.333333-170.666667-426.666667-469.333333-469.333333z"
|
||||
p-id="29313"
|
||||
fill="#85BCFF"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text" v-if="mode === 'dir'">{{ folderObj.dir }}</div>
|
||||
<div class="text" v-if="mode === 'back'">双击返回</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { useStore } from '@/store'
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const userConfigInfo = computed(() => store.getters.getUserConfigInfo).value
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const props = defineProps({
|
||||
folderObj: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'dir'
|
||||
}
|
||||
})
|
||||
|
||||
const emits = defineEmits(['update:modelValue'])
|
||||
|
||||
const dblclickFolder = () => {
|
||||
const { folderObj, mode } = props
|
||||
|
||||
if (mode === 'back') {
|
||||
const currentDir = userConfigInfo.selectedDir
|
||||
|
||||
if (currentDir === '/') {
|
||||
return
|
||||
}
|
||||
|
||||
const currentDirList = currentDir.split('/')
|
||||
|
||||
if (currentDirList.length === 1) {
|
||||
userConfigInfo.selectedDir = '/'
|
||||
} else if (currentDirList.length > 1) {
|
||||
currentDirList.length -= 1
|
||||
userConfigInfo.selectedDir = currentDirList.join('/')
|
||||
}
|
||||
} else {
|
||||
userConfigInfo.selectedDir = folderObj.dirPath
|
||||
}
|
||||
|
||||
const { selectedDir } = userConfigInfo
|
||||
|
||||
if (selectedDir === '/') {
|
||||
userConfigInfo.selectedDirList = []
|
||||
userConfigInfo.dirMode = 'rootDir'
|
||||
} else {
|
||||
userConfigInfo.selectedDirList = selectedDir.split('/')
|
||||
userConfigInfo.dirMode = 'reposDir'
|
||||
}
|
||||
store.dispatch('USER_CONFIG_INFO_NOT_PERSIST')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import 'folder-card.styl';
|
||||
</style>
|
||||
@@ -1,92 +0,0 @@
|
||||
@import "../../style/base.styl"
|
||||
|
||||
.header {
|
||||
width 100%
|
||||
height 100%
|
||||
background var(--background-color)
|
||||
padding 0 20rem
|
||||
box-sizing border-box
|
||||
display flex
|
||||
justify-content space-between
|
||||
align-items center
|
||||
|
||||
.header-left {
|
||||
height 100%
|
||||
display flex
|
||||
justify-content flex-start
|
||||
|
||||
.brand {
|
||||
height 100%
|
||||
display flex
|
||||
justify-content flex-start
|
||||
align-items center
|
||||
cursor pointer
|
||||
|
||||
.logo {
|
||||
width 46rem
|
||||
height 46rem
|
||||
margin-right 10rem
|
||||
|
||||
img {
|
||||
width 100%
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
.title {
|
||||
font-size 36rem
|
||||
font-weight bold
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.website-count {
|
||||
box-sizing border-box
|
||||
display flex
|
||||
align-items flex-end
|
||||
font-size 14rem
|
||||
margin-left 10rem
|
||||
padding-bottom 12rem
|
||||
cursor pointer
|
||||
|
||||
+picx-mobile() {
|
||||
display none
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.header-right {
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
.username {
|
||||
font-size: 16rem;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 38rem;
|
||||
height: 38rem;
|
||||
color: var(--default-text-color);
|
||||
border-radius: 50%;
|
||||
border: 1rem solid var(--default-text-color);
|
||||
margin-left: 10rem;
|
||||
padding: 1rem;
|
||||
box-sizing: border-box;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
<template>
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<div class="brand" @click="router.push('/')">
|
||||
<div class="logo">
|
||||
<img src="../../assets/logo.png" alt="PicX" />
|
||||
</div>
|
||||
<div class="title">PicX</div>
|
||||
</div>
|
||||
<div class="website-count" @click="goGitHubRepo">
|
||||
<el-tooltip content="感觉好用,点 Star 支持作者(* ̄︶ ̄)" placement="bottom">
|
||||
<site-count :isuv="false" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<div class="user-info" @click="onUserInfoClick">
|
||||
<div class="username">
|
||||
{{ userConfigInfo.owner ? userConfigInfo.owner : defaultUsername }}
|
||||
</div>
|
||||
|
||||
<div class="avatar" v-if="!userConfigInfo?.avatarUrl">
|
||||
<el-icon :size="22"><UserFilled /></el-icon>
|
||||
</div>
|
||||
|
||||
<el-dropdown
|
||||
trigger="click"
|
||||
@command="handleCommand"
|
||||
v-if="userConfigInfo?.avatarUrl"
|
||||
>
|
||||
<span class="el-dropdown-link">
|
||||
<span class="avatar">
|
||||
<img :src="userConfigInfo?.avatarUrl" :alt="userConfigInfo?.owner" />
|
||||
</span>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="logout"> 退出登录 </el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, computed, toRefs } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useStore } from '@/store'
|
||||
import siteCount from '@/components/site-count/site-count.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'header-content',
|
||||
|
||||
components: {
|
||||
siteCount
|
||||
},
|
||||
|
||||
setup() {
|
||||
const router = useRouter()
|
||||
const store = useStore()
|
||||
|
||||
const reactiveData = reactive({
|
||||
defaultUsername: '未登录',
|
||||
userConfigInfo: computed(() => store.state.userConfigInfoModule.userConfigInfo)
|
||||
})
|
||||
|
||||
const onUserInfoClick = () => {
|
||||
if (
|
||||
!reactiveData.userConfigInfo.loggingStatus &&
|
||||
router.currentRoute.value.path !== '/config'
|
||||
) {
|
||||
router.push('/config')
|
||||
}
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
store.dispatch('LOGOUT')
|
||||
router.push('/config')
|
||||
}
|
||||
|
||||
const handleCommand = (command: string) => {
|
||||
// eslint-disable-next-line default-case
|
||||
switch (command) {
|
||||
case 'upload':
|
||||
router.push('/')
|
||||
break
|
||||
|
||||
case 'config':
|
||||
router.push('/config')
|
||||
break
|
||||
|
||||
case 'management':
|
||||
router.push('/management')
|
||||
break
|
||||
|
||||
case 'logout':
|
||||
logout()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const goGitHubRepo = () => {
|
||||
window.open('https://github.com/XPoet/picx')
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(reactiveData),
|
||||
router,
|
||||
onUserInfoClick,
|
||||
handleCommand,
|
||||
goGitHubRepo
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import "header-content.styl"
|
||||
</style>
|
||||
@@ -1,142 +0,0 @@
|
||||
@import "../../style/base.styl"
|
||||
|
||||
$infoBoxHeight = 56rem
|
||||
|
||||
.image-card {
|
||||
position relative
|
||||
width 100%
|
||||
height 100%
|
||||
box-shadow 1rem 2rem 3rem var(--shadow-color)
|
||||
box-sizing border-box
|
||||
padding-bottom $infoBoxHeight
|
||||
user-select none
|
||||
|
||||
&.checked, &:hover {
|
||||
box-shadow 0 0 10rem #666
|
||||
}
|
||||
|
||||
&.listing {
|
||||
display flex
|
||||
justify-content flex-start
|
||||
align-items center
|
||||
padding 5rem
|
||||
border-radius $box-border-radius
|
||||
|
||||
.image-box {
|
||||
height 45rem
|
||||
width 45rem
|
||||
}
|
||||
|
||||
.info-box {
|
||||
position relative
|
||||
width 80%
|
||||
}
|
||||
|
||||
:deep(.el-loading-mask) {
|
||||
.el-loading-spinner {
|
||||
margin-top -25rem
|
||||
|
||||
.circular {
|
||||
height 24rem
|
||||
width 24rem
|
||||
}
|
||||
|
||||
.el-loading-text {
|
||||
margin 0
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.image-box {
|
||||
position relative
|
||||
width 100%
|
||||
height 100%
|
||||
|
||||
img {
|
||||
width 100%
|
||||
height 100%
|
||||
object-fit cover
|
||||
cursor pointer
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.info-box {
|
||||
width 100%
|
||||
height $infoBoxHeight
|
||||
position absolute
|
||||
bottom 0
|
||||
left 0
|
||||
|
||||
.image-info {
|
||||
width 100%
|
||||
height 100%
|
||||
padding 5rem
|
||||
color var(--default-text-color)
|
||||
box-sizing border-box
|
||||
display flex
|
||||
flex-direction column
|
||||
justify-content space-between
|
||||
|
||||
.rename-input {
|
||||
height 20rem
|
||||
display flex
|
||||
margin-bottom 4rem
|
||||
}
|
||||
|
||||
.filename {
|
||||
height 16rem
|
||||
overflow hidden
|
||||
text-overflow ellipsis
|
||||
white-space nowrap
|
||||
font-size 14rem
|
||||
margin-bottom 6rem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.operation-box {
|
||||
position absolute
|
||||
top 10rem
|
||||
right 8rem
|
||||
width calc(100% - 16rem)
|
||||
display flex
|
||||
justify-content space-between
|
||||
|
||||
.operation-left {
|
||||
.picked-btn {
|
||||
i {
|
||||
font-weight bold
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.operation-right {
|
||||
display flex
|
||||
}
|
||||
|
||||
.operation-btn {
|
||||
width 32rem
|
||||
height 32rem
|
||||
border-radius 50%
|
||||
box-shadow 0 0 6rem #555
|
||||
cursor pointer
|
||||
background var(--background-color)
|
||||
display flex
|
||||
justify-content center
|
||||
align-items center
|
||||
margin-right 8rem
|
||||
font-size 18rem
|
||||
|
||||
&:last-child {
|
||||
margin-right 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,287 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="image-card"
|
||||
:class="{ listing: listing, checked: imageObj.checked }"
|
||||
v-loading="imageObj.deleting"
|
||||
element-loading-text="删除中..."
|
||||
@mouseenter="isShowDelBtn = true"
|
||||
@mouseleave="isShowDelBtn = false"
|
||||
>
|
||||
<div class="image-box">
|
||||
<img data-fancybox="gallery" :src="imgUrl" />
|
||||
</div>
|
||||
<div class="info-box">
|
||||
<div class="image-info">
|
||||
<el-input
|
||||
size="small"
|
||||
v-if="renameValue && props.modelValue === props.index"
|
||||
class="rename-input"
|
||||
v-model="renameValue"
|
||||
@blur="renameInputBlur"
|
||||
@keydown.enter.prevent="updateRename"
|
||||
ref="renameInputRef"
|
||||
></el-input>
|
||||
<div class="filename" v-else>{{ imageObj.name }}</div>
|
||||
<div class="image-operation">
|
||||
<copy-external-link :img-obj="imageObj" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="operation-box"
|
||||
v-show="isShowDelBtn || dropdownVisible || imageObj.checked"
|
||||
>
|
||||
<div class="operation-left">
|
||||
<div
|
||||
v-if="isManagementPage"
|
||||
:class="[imageObj.checked ? 'picked-btn' : 'pick-btn', 'operation-btn']"
|
||||
@click="trigglePick(imageObj)"
|
||||
>
|
||||
<el-icon v-if="imageObj.checked"><Check /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="operation-right">
|
||||
<el-dropdown size="default" trigger="click" @visible-change="visibleChange">
|
||||
<div class="operation-btn">
|
||||
<el-icon><MoreFilled /></el-icon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="deleteImageTips(imageObj)">
|
||||
删除
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.self="renameImage(imageObj)">
|
||||
重命名
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="viewImageProperties(imageObj)">
|
||||
属性
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import type { ElInput } from 'element-plus'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useStore } from '@/store'
|
||||
import axios from '@/utils/axios'
|
||||
import { UploadedImageModel } from '@/common/model/upload.model'
|
||||
import { getBase64ByImageUrl, getImage } from '@/utils/rename-image'
|
||||
import { uploadImage_single } from '@/utils/upload-helper'
|
||||
import { getFilename, getFileSize, getFileSuffix } from '@/utils/file-handle-helper'
|
||||
import ExternalLinkType from '@/common/model/external-link.model'
|
||||
|
||||
const props = defineProps({
|
||||
listing: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
imageObj: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
isUploaded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
index: {
|
||||
type: Number
|
||||
},
|
||||
modelValue: {
|
||||
type: Number
|
||||
}
|
||||
})
|
||||
|
||||
const emits = defineEmits(['update:modelValue'])
|
||||
|
||||
const store = useStore()
|
||||
const router = useRoute()
|
||||
const userConfigInfo = computed(() => store.getters.getUserConfigInfo).value
|
||||
const userSettings = computed(() => store.getters.getUserSettings).value
|
||||
const isManagementPage = computed(() => {
|
||||
return router.path === '/management'
|
||||
})
|
||||
|
||||
const imgUrl = computed(() => {
|
||||
switch (userSettings.externalLinkType) {
|
||||
case ExternalLinkType.jsdelivr:
|
||||
return props.imageObj.jsdelivr_cdn_url
|
||||
case ExternalLinkType.staticaly:
|
||||
return props.imageObj.staticaly_cdn_url
|
||||
case ExternalLinkType.cloudflare:
|
||||
return props.imageObj.cloudflare_cdn_url
|
||||
default:
|
||||
return props.imageObj.github_url
|
||||
}
|
||||
})
|
||||
|
||||
const renameInputRef = ref<InstanceType<typeof ElInput>>()
|
||||
|
||||
const isShowDelBtn = ref(false)
|
||||
|
||||
const renameValue = ref('')
|
||||
|
||||
const dropdownVisible = ref<Boolean>(false)
|
||||
|
||||
const doDeleteImage = (
|
||||
imageObj: UploadedImageModel,
|
||||
isRename: boolean = false
|
||||
): Promise<Boolean> => {
|
||||
if (!isRename) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
imageObj.deleting = true
|
||||
}
|
||||
const { owner, selectedRepos } = userConfigInfo
|
||||
|
||||
return new Promise((resolve) => {
|
||||
axios
|
||||
.delete(`/repos/${owner}/${selectedRepos}/contents/${imageObj.path}`, {
|
||||
data: {
|
||||
owner,
|
||||
repo: selectedRepos,
|
||||
path: imageObj.path,
|
||||
message: 'Delete picture via PicX(https://github.com/XPoet/picx)',
|
||||
sha: imageObj.sha
|
||||
}
|
||||
})
|
||||
.then((res) => {
|
||||
console.log('[deleteImage] ', res)
|
||||
if (res && res.status === 200) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
imageObj.deleting = false
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
ElMessage.success(`${isRename ? '更新' : '删除'}成功!`)
|
||||
store.dispatch('UPLOADED_LIST_REMOVE', imageObj.uuid)
|
||||
store.dispatch('DIR_IMAGE_LIST_REMOVE', imageObj)
|
||||
resolve(true)
|
||||
} else {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
imageObj.deleting = false
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const deleteImageTips = (imageObj: UploadedImageModel) => {
|
||||
ElMessageBox.confirm(
|
||||
`
|
||||
<div>此操作将会永久删除图片:</div>
|
||||
<strong>${imageObj.name}</strong>
|
||||
`,
|
||||
`删除提示`,
|
||||
{
|
||||
dangerouslyUseHTMLString: true,
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
doDeleteImage(imageObj)
|
||||
})
|
||||
.catch(() => {
|
||||
console.log('取消删除')
|
||||
})
|
||||
}
|
||||
|
||||
const trigglePick = (imageObj: UploadedImageModel) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
imageObj.checked = !imageObj.checked
|
||||
store.commit('IMAGE_CARD', { imageObj })
|
||||
}
|
||||
|
||||
const renameImage = async (imgObj: UploadedImageModel) => {
|
||||
emits('update:modelValue', props.index)
|
||||
renameValue.value = getFilename(imgObj.name)
|
||||
await nextTick(() => {
|
||||
const temp = setTimeout(() => {
|
||||
renameInputRef.value?.focus()
|
||||
clearTimeout(temp)
|
||||
}, 150)
|
||||
})
|
||||
}
|
||||
|
||||
const renameInputBlur = () => {
|
||||
emits('update:modelValue', undefined)
|
||||
}
|
||||
|
||||
const updateRename = async () => {
|
||||
renameInputBlur()
|
||||
|
||||
const { imageObj }: any = props
|
||||
|
||||
if (renameValue.value === getFilename(imageObj.name) || !renameValue.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const renameFn = async () => {
|
||||
const loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: '正在重命名...'
|
||||
})
|
||||
|
||||
const suffix = getFileSuffix(imageObj.name)
|
||||
|
||||
const imgInfo = {
|
||||
name: renameValue.value + imageObj.name.substring(imageObj.name.indexOf('.')),
|
||||
size: imageObj.size,
|
||||
lastModified: Date.now(),
|
||||
type: `image/${suffix}`
|
||||
}
|
||||
|
||||
const base64 = await getBase64ByImageUrl(imgUrl.value, suffix)
|
||||
|
||||
if (base64) {
|
||||
const newImgObj = getImage(base64, imgInfo)
|
||||
if (newImgObj) {
|
||||
const isUploadSuccess = await uploadImage_single(userConfigInfo, newImgObj)
|
||||
|
||||
if (isUploadSuccess) {
|
||||
renameValue.value = ''
|
||||
await doDeleteImage(imageObj, true)
|
||||
await store.dispatch('UPLOADED_LIST_REMOVE', newImgObj.uuid)
|
||||
}
|
||||
}
|
||||
}
|
||||
loading.close()
|
||||
}
|
||||
|
||||
ElMessageBox.confirm(`该图片重命名为 ${renameValue.value} ?`, `提示`, {
|
||||
type: 'warning'
|
||||
})
|
||||
.then(async () => {
|
||||
await renameFn()
|
||||
})
|
||||
.catch(() => {
|
||||
console.log('取消图片重命名')
|
||||
})
|
||||
}
|
||||
|
||||
const visibleChange = (e: boolean) => {
|
||||
dropdownVisible.value = e
|
||||
}
|
||||
|
||||
const viewImageProperties = (imgObj: UploadedImageModel) => {
|
||||
ElMessageBox.confirm(
|
||||
`
|
||||
<div>图片名称:<strong>${imgObj.name}</strong></div>
|
||||
<div>图片大小:<strong>${getFileSize(imgObj.size)} KB</strong></div>
|
||||
`,
|
||||
`属性`,
|
||||
{
|
||||
showCancelButton: false,
|
||||
showConfirmButton: false,
|
||||
dangerouslyUseHTMLString: true,
|
||||
type: 'info'
|
||||
}
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import 'image-card.styl';
|
||||
</style>
|
||||
@@ -1,52 +0,0 @@
|
||||
@import "../../style/base.styl"
|
||||
|
||||
.selector-wrapper {
|
||||
padding 4rem 12rem
|
||||
width 100%
|
||||
box-sizing border-box
|
||||
display flex
|
||||
justify-content space-between
|
||||
align-items center
|
||||
border-bottom 1rem solid var(--third-background-color)
|
||||
|
||||
|
||||
.selector-left-box {
|
||||
display flex
|
||||
align-items center
|
||||
|
||||
:deep(.el-checkbox) {
|
||||
font-weight unset
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__label ) {
|
||||
line-height unset
|
||||
}
|
||||
|
||||
.cancel-select-btn {
|
||||
color #576b95
|
||||
cursor pointer
|
||||
}
|
||||
|
||||
div.item {
|
||||
margin-left 8rem
|
||||
}
|
||||
}
|
||||
|
||||
.selector-right-box {
|
||||
.btn-icon {
|
||||
cursor: pointer;
|
||||
font-size: 22rem;
|
||||
margin-left: 10rem;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.temp-batch-externalink {
|
||||
opacity 0
|
||||
position absolute
|
||||
left -9999rem
|
||||
top -9999rem
|
||||
width 0
|
||||
height 0
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
<template>
|
||||
<div class="selector-wrapper" v-if="getImageCardCheckedNum">
|
||||
<div class="selector-left-box">
|
||||
<el-checkbox
|
||||
:label="checked ? '取消全选' : '全选'"
|
||||
v-model="checked"
|
||||
@change="triggleFullCheck"
|
||||
></el-checkbox>
|
||||
<div class="item">已选择 {{ getImageCardCheckedNum }} 张图片</div>
|
||||
<div class="item cancel-select-btn" @click="cancelPick">取消选择</div>
|
||||
</div>
|
||||
<div class="selector-right-box">
|
||||
<el-tooltip placement="top" content="批量复制外链">
|
||||
<el-icon class="btn-icon" @click="copyLink"><CopyDocument /></el-icon>
|
||||
</el-tooltip>
|
||||
<el-tooltip placement="top" content="批量删除图片">
|
||||
<el-icon class="btn-icon" @click="batchDeleteImage"><Delete /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, watch, ref } from 'vue'
|
||||
import { useStore } from '@/store'
|
||||
import { UploadedImageModel } from '@/common/model/upload.model'
|
||||
import { batchCopyExternalLink } from '@/utils/external-link-handler'
|
||||
import { delelteBatchImage } from '@/utils/delete-image-card'
|
||||
import { deleteStatusEnum } from '@/common/model/delete.model'
|
||||
|
||||
const props = defineProps({
|
||||
currentDirImageList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const emits = defineEmits(['update:initImageList'])
|
||||
|
||||
const store = useStore()
|
||||
const checked = ref(false)
|
||||
|
||||
const getImageCardCheckedArr = computed(() => store.getters.getImageCardCheckedArr)
|
||||
const userConfigInfo = computed(() => store.getters.getUserConfigInfo).value
|
||||
const userSettings = computed(() => store.getters.getUserSettings).value
|
||||
const getImageCardCheckedNum = computed(() => getImageCardCheckedArr.value.length || 0)
|
||||
|
||||
watch(
|
||||
() => getImageCardCheckedNum.value,
|
||||
(newVal) => {
|
||||
const newValCheckedNum = props.currentDirImageList.length
|
||||
checked.value = newVal === newValCheckedNum
|
||||
}
|
||||
)
|
||||
|
||||
function copyLink() {
|
||||
batchCopyExternalLink(getImageCardCheckedArr.value, userSettings.externalLinkType)
|
||||
}
|
||||
|
||||
function cancelPick() {
|
||||
props.currentDirImageList.forEach((item: UploadedImageModel) => {
|
||||
if (item.checked) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
item.checked = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function batchDeleteImage() {
|
||||
if (getImageCardCheckedArr.value?.length > 0) {
|
||||
ElMessageBox.confirm('是否批量删除已选中的图片?', '删除提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
.then(async () => {
|
||||
const res = await delelteBatchImage(getImageCardCheckedArr.value, userConfigInfo)
|
||||
if (res === deleteStatusEnum.allDeleted) {
|
||||
ElMessage.success('批量删除成功!')
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log('取消批量删除')
|
||||
})
|
||||
} else {
|
||||
ElMessage.warning('请先选择图片')
|
||||
}
|
||||
}
|
||||
|
||||
function triggleFullCheck() {
|
||||
let checkedImgArr: Array<UploadedImageModel> = []
|
||||
props.currentDirImageList.forEach((item: UploadedImageModel) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
item.checked = checked.value
|
||||
})
|
||||
checkedImgArr = props.currentDirImageList as any
|
||||
store.commit('REPLACE_IMAGE_CARD', { checkedImgArr })
|
||||
}
|
||||
onMounted(() => {
|
||||
triggleFullCheck()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import 'image-selector.styl';
|
||||
</style>
|
||||
@@ -1,75 +0,0 @@
|
||||
$transition-duration = 0.3s
|
||||
$transition-delay = 0s
|
||||
|
||||
.image-viewer {
|
||||
position fixed
|
||||
left 0
|
||||
top 0
|
||||
width 100%
|
||||
height 100%
|
||||
display flex
|
||||
align-items center
|
||||
justify-content center
|
||||
background rgba(0, 0, 0, 0)
|
||||
visibility hidden
|
||||
z-index 1000
|
||||
padding 6%
|
||||
box-sizing border-box
|
||||
transition-property visibility, background
|
||||
transition-delay $transition-delay, $transition-delay
|
||||
transition-duration $transition-duration, $transition-duration
|
||||
transition-timing-function ease, ease
|
||||
|
||||
&.active {
|
||||
background rgba(0, 0, 0, 0.6)
|
||||
visibility visible
|
||||
|
||||
.image-box {
|
||||
transform scale(1)
|
||||
padding 2rem
|
||||
|
||||
.image-info {
|
||||
display block
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
.image-box {
|
||||
position relative;
|
||||
width 60%
|
||||
height 100%
|
||||
display flex
|
||||
flex-direction column
|
||||
justify-content center
|
||||
align-items center
|
||||
transform scale(0)
|
||||
transition-property transform
|
||||
transition-delay $transition-delay
|
||||
transition-duration $transition-duration
|
||||
transition-timing-function ease
|
||||
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
width 80%
|
||||
}
|
||||
|
||||
.img {
|
||||
cursor zoom-out
|
||||
max-width 100%
|
||||
max-height 100%
|
||||
}
|
||||
|
||||
.image-info {
|
||||
display none
|
||||
padding 10rem
|
||||
|
||||
.item {
|
||||
margin 0 6rem
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="image-viewer"
|
||||
:class="{ active: imageViewer.isShow }"
|
||||
@click="imageViewer.isShow = false"
|
||||
>
|
||||
<div class="image-box" v-if="imageViewer?.imgInfo?.url">
|
||||
<img class="img" :src="imageViewer?.imgInfo?.url" />
|
||||
<div class="image-info" v-if="imageViewer.imgInfo">
|
||||
<el-tag class="item" size="small" v-if="imageViewer.imgInfo.name">
|
||||
图片名:{{ imageViewer.imgInfo.name }}
|
||||
</el-tag>
|
||||
<el-tag class="item" size="small" v-if="imageViewer.imgInfo.size">
|
||||
图片大小:{{ parseFileSize(imageViewer.imgInfo.size) }}
|
||||
</el-tag>
|
||||
<el-tag class="item" size="small" v-if="imageViewer.imgInfo.lastModified">
|
||||
最后修改时间:{{ formatLastModified(imageViewer.imgInfo.lastModified) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { useStore } from '@/store'
|
||||
import { getFileSize } from '@/utils/file-handle-helper'
|
||||
import TimeHelper from '@/utils/time-helper'
|
||||
|
||||
const store = useStore()
|
||||
const imageViewer = computed(() => store.getters.getImageViewer)
|
||||
|
||||
function parseFileSize(size: number) {
|
||||
return `${getFileSize(size)} KB`
|
||||
}
|
||||
|
||||
function formatLastModified(t: number) {
|
||||
return TimeHelper.formatTimestamp(t)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import "image-viewer.styl"
|
||||
</style>
|
||||
@@ -1,73 +0,0 @@
|
||||
@import "../../style/base.styl"
|
||||
|
||||
$top-height = 60rem
|
||||
$left-side-width = 80rem
|
||||
|
||||
.main-container {
|
||||
position absolute
|
||||
box-sizing border-box
|
||||
width 100%
|
||||
height 100%
|
||||
background var(--second-background-color)
|
||||
padding-top $top-height
|
||||
font-size 15rem
|
||||
|
||||
|
||||
|
||||
.top {
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
box-sizing border-box
|
||||
width 100%
|
||||
height $top-height
|
||||
}
|
||||
|
||||
|
||||
.bottom {
|
||||
position relative
|
||||
box-sizing border-box
|
||||
width 100%
|
||||
height 100%
|
||||
padding-top $component-interval
|
||||
|
||||
|
||||
.container {
|
||||
position relative
|
||||
box-sizing border-box
|
||||
width 100%
|
||||
height 100%
|
||||
padding-left $left-side-width
|
||||
|
||||
|
||||
.left {
|
||||
position absolute
|
||||
box-sizing border-box
|
||||
width $left-side-width
|
||||
height 100%
|
||||
top 0
|
||||
left 0
|
||||
}
|
||||
|
||||
|
||||
.right {
|
||||
position relative
|
||||
width 100%
|
||||
height 100%
|
||||
box-sizing border-box
|
||||
padding 0 $component-interval 0 $component-interval
|
||||
|
||||
|
||||
.content {
|
||||
position relative
|
||||
box-sizing border-box
|
||||
width 100%
|
||||
height 100%
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
<template>
|
||||
<main class="main-container" @click="changeUploadAreaActive">
|
||||
<div class="top">
|
||||
<header-content />
|
||||
</div>
|
||||
|
||||
<div class="bottom">
|
||||
<div class="container">
|
||||
<div class="left">
|
||||
<nav-content />
|
||||
</div>
|
||||
|
||||
<div class="right">
|
||||
<div class="content">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<image-viewer></image-viewer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted } from 'vue'
|
||||
import headerContent from '@/components/header-content/header-content.vue'
|
||||
import navContent from '@/components/nav-content/nav-content.vue'
|
||||
import imageViewer from '@/components/image-viewer/image-viewer.vue'
|
||||
import { useStore } from '@/store'
|
||||
import userConfigInfoModel from '@/utils/set-theme-mode'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'main-container',
|
||||
|
||||
components: {
|
||||
headerContent,
|
||||
navContent,
|
||||
imageViewer
|
||||
},
|
||||
|
||||
setup() {
|
||||
const store = useStore()
|
||||
|
||||
const changeUploadAreaActive = (e: any) => {
|
||||
store.commit(
|
||||
'CHANGE_UPLOAD_AREA_ACTIVE',
|
||||
e.target.classList.contains('active-upload')
|
||||
)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
userConfigInfoModel()
|
||||
})
|
||||
|
||||
return {
|
||||
changeUploadAreaActive
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import "main-container.styl"
|
||||
</style>
|
||||
@@ -1,40 +0,0 @@
|
||||
@import "../../style/base.styl"
|
||||
|
||||
.nav {
|
||||
position relative
|
||||
width 100%
|
||||
height 100%
|
||||
box-sizing border-box
|
||||
background var(--background-color)
|
||||
|
||||
ul.nav-list {
|
||||
padding 0
|
||||
margin 0
|
||||
|
||||
li.nav-item {
|
||||
box-sizing border-box
|
||||
width 100%
|
||||
height 76rem
|
||||
cursor pointer
|
||||
|
||||
&.active {
|
||||
font-weight bold
|
||||
background var(--second-background-color)
|
||||
}
|
||||
|
||||
.nav-content {
|
||||
display flex
|
||||
flex-direction column
|
||||
justify-content center
|
||||
align-items center
|
||||
|
||||
.nav-name {
|
||||
margin-top 5rem
|
||||
font-size 12rem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
<template>
|
||||
<aside class="nav">
|
||||
<ul class="nav-list">
|
||||
<li
|
||||
class="nav-item flex-center"
|
||||
v-for="(navItem, index) in navList"
|
||||
:key="index"
|
||||
:class="{ active: navItem.isActive }"
|
||||
@click="navClick(navItem)"
|
||||
v-show="navItem.isShow"
|
||||
>
|
||||
<div class="nav-content">
|
||||
<el-icon :size="navIconSize">
|
||||
<component :is="navItem.icon"></component>
|
||||
</el-icon>
|
||||
<span class="nav-name">{{ navItem.name }}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, watch, computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useStore } from '@/store'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useStore()
|
||||
|
||||
const userConfigInfo = computed(() => store.getters.getUserConfigInfo).value
|
||||
const userSettings = computed(() => store.getters.getUserSettings).value
|
||||
|
||||
const navIconSize = computed(() => {
|
||||
switch (userSettings.elementPlusSize) {
|
||||
case 'small':
|
||||
return 22
|
||||
case 'large':
|
||||
return 30
|
||||
default:
|
||||
return 26
|
||||
}
|
||||
})
|
||||
|
||||
const navList = ref([
|
||||
{
|
||||
name: '图床配置',
|
||||
icon: 'edit',
|
||||
isActive: false,
|
||||
path: '/config',
|
||||
isShow: true
|
||||
},
|
||||
{
|
||||
name: '上传图片',
|
||||
icon: 'upload',
|
||||
isActive: false,
|
||||
path: '/upload',
|
||||
isShow: true
|
||||
},
|
||||
{
|
||||
name: '图床管理',
|
||||
icon: 'box',
|
||||
isActive: false,
|
||||
path: '/management',
|
||||
isShow: true
|
||||
},
|
||||
{
|
||||
name: '我的设置',
|
||||
icon: 'setting',
|
||||
isActive: false,
|
||||
path: '/settings',
|
||||
isShow: true
|
||||
},
|
||||
{
|
||||
name: '使用教程',
|
||||
icon: 'magic-stick',
|
||||
isActive: false,
|
||||
path: '/tutorials',
|
||||
isShow: true
|
||||
},
|
||||
{
|
||||
name: '帮助反馈',
|
||||
icon: 'chat-dot-round',
|
||||
isActive: false,
|
||||
path: '/about',
|
||||
isShow: true
|
||||
}
|
||||
])
|
||||
|
||||
const navClick = (e: any) => {
|
||||
const { path } = e
|
||||
|
||||
if (path === '/management') {
|
||||
if (userConfigInfo.selectedRepos === '') {
|
||||
ElMessage.warning('请选择一个仓库!')
|
||||
router.push('/config')
|
||||
return
|
||||
}
|
||||
|
||||
if (userConfigInfo.selectedDir === '') {
|
||||
ElMessage.warning('目录不能为空!')
|
||||
router.push('/config')
|
||||
return
|
||||
}
|
||||
}
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
const changeNavActive = (currentPath: string) => {
|
||||
navList.value.forEach((v) => {
|
||||
const temp = v
|
||||
temp.isActive = v.path === currentPath
|
||||
return temp
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => router.currentRoute.value,
|
||||
(_n) => {
|
||||
changeNavActive(_n.path)
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => userConfigInfo.loggingStatus,
|
||||
(_n) => {
|
||||
navList.value.forEach((v: any) => {
|
||||
// eslint-disable-next-line default-case
|
||||
switch (v.path) {
|
||||
case '/management':
|
||||
case '/settings':
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
v.isShow = _n
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
router.isReady().then(() => {
|
||||
changeNavActive(router.currentRoute.value.path)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import "nav-content.styl"
|
||||
</style>
|
||||
@@ -1,16 +0,0 @@
|
||||
.selected-info-bar-box {
|
||||
height 100%
|
||||
display flex
|
||||
align-items center
|
||||
justify-content flex-start
|
||||
font-size 12rem
|
||||
box-sizing border-box
|
||||
|
||||
.info-item {
|
||||
margin-right 8rem
|
||||
|
||||
&:last-child {
|
||||
margin-right 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
<template>
|
||||
<div class="selected-info-bar-box" v-if="userConfigInfo.selectedRepos">
|
||||
<span class="info-item">
|
||||
仓库:
|
||||
<el-tag>
|
||||
{{ userConfigInfo.selectedRepos }}
|
||||
</el-tag>
|
||||
</span>
|
||||
<span class="info-item" v-if="userConfigInfo.selectedBranch">
|
||||
分支:
|
||||
<el-tag>
|
||||
{{ userConfigInfo.selectedBranch }}
|
||||
</el-tag>
|
||||
</span>
|
||||
<span class="info-item" v-if="userConfigInfo.selectedDir">
|
||||
目录:
|
||||
<el-tag>
|
||||
{{ userConfigInfo.selectedDir }}
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { useStore } from '@/store'
|
||||
|
||||
const store = useStore()
|
||||
const userConfigInfo = computed(() => store.getters.getUserConfigInfo)
|
||||
const userSettings = computed(() => store.getters.getUserSettings)
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import "selected-info-bar.styl"
|
||||
</style>
|
||||
@@ -1,58 +0,0 @@
|
||||
<template>
|
||||
<span class="site-count" ref="siteCountDom" v-show="isShow">
|
||||
超过
|
||||
<span id="busuanzi_value_site_uv" class="uv" v-show="isuv"></span>
|
||||
<span id="busuanzi_value_site_pv" class="pv" v-show="!isuv"></span>
|
||||
次被使用
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, ref, Ref } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'site-count',
|
||||
|
||||
props: {
|
||||
isuv: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
setup(props, ctx) {
|
||||
const siteCountDom: Ref = ref<null | HTMLElement>(null)
|
||||
const isShow: Ref<boolean> = ref(false)
|
||||
|
||||
const getInnerText = (dom, isuv) => {
|
||||
return dom.querySelector(`.${isuv ? 'u' : 'p'}v`).innerText
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const script: any = document.createElement('script')
|
||||
script.async = true
|
||||
script.src = '//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js'
|
||||
siteCountDom.value.appendChild(script)
|
||||
|
||||
script.onload = () => {
|
||||
const tempT = setTimeout(() => {
|
||||
if (getInnerText(siteCountDom.value, props.isuv)) {
|
||||
isShow.value = true
|
||||
}
|
||||
clearTimeout(tempT)
|
||||
}, 1500)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
siteCountDom,
|
||||
isShow
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="stylus">
|
||||
.site-count {
|
||||
transition all 0.2s ease-in
|
||||
}
|
||||
</style>
|
||||
@@ -1,211 +0,0 @@
|
||||
@import "../../style/base.styl"
|
||||
|
||||
$info-item-height = 68rem
|
||||
$info-item-border = 1rem
|
||||
$info-item-padding = 5rem
|
||||
$compressed-file-background-color = #228eff
|
||||
$image-width = $info-item-height - ($info-item-border * 2)
|
||||
|
||||
.to-upload-image-list-card {
|
||||
position relative
|
||||
width 100%
|
||||
box-sizing border-box
|
||||
margin-top 6rem
|
||||
|
||||
.header {
|
||||
width 100%
|
||||
height 30rem
|
||||
box-sizing border-box
|
||||
font-size 12rem
|
||||
display flex
|
||||
align-items center
|
||||
justify-content space-between
|
||||
padding-bottom 6rem
|
||||
}
|
||||
|
||||
.body {
|
||||
width 100%
|
||||
height 100%
|
||||
max-height 170rem
|
||||
overflow-y auto
|
||||
box-sizing border-box
|
||||
padding 10rem
|
||||
border 1rem solid var(--border-color)
|
||||
margin-top 10rem
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width 5rem
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius 2rem
|
||||
}
|
||||
|
||||
.image-uploading-info-box {
|
||||
position relative
|
||||
width 100%
|
||||
box-sizing border-box
|
||||
padding 0
|
||||
margin 0
|
||||
|
||||
.image-uploading-info-item {
|
||||
position relative
|
||||
box-sizing border-box
|
||||
width 100%
|
||||
height $info-item-height
|
||||
border $info-item-border solid var(--border-color)
|
||||
border-radius 5rem
|
||||
margin-bottom 10rem
|
||||
overflow hidden
|
||||
font-size 15rem
|
||||
padding-left $image-width
|
||||
transition all 0.3s ease
|
||||
|
||||
&.disable {
|
||||
pointer-events none
|
||||
cursor not-allowed
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom 0
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow 0 0 5rem var(--shadow-hover-color)
|
||||
}
|
||||
|
||||
.left-image-box {
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
width $image-width
|
||||
height 100%
|
||||
box-sizing border-box
|
||||
margin-right 5rem
|
||||
|
||||
img {
|
||||
object-fit cover
|
||||
width 100%
|
||||
height 100%
|
||||
overflow hidden
|
||||
cursor pointer
|
||||
border-top-left-radius 5rem
|
||||
border-bottom-left-radius 5rem
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.right-operation-box {
|
||||
position relative
|
||||
width 100%
|
||||
height 100%
|
||||
box-sizing border-box
|
||||
padding $info-item-padding 20rem $info-item-padding $info-item-padding
|
||||
|
||||
.top, .bottom {
|
||||
width 100%
|
||||
height 50%
|
||||
box-sizing border-box
|
||||
padding 0 5rem
|
||||
}
|
||||
|
||||
.top {
|
||||
display flex
|
||||
justify-content space-between
|
||||
|
||||
.image-name,
|
||||
.image-info {
|
||||
display flex
|
||||
align-items center
|
||||
box-sizing border-box
|
||||
height 100%
|
||||
}
|
||||
|
||||
.image-name {
|
||||
font-size 13rem
|
||||
overflow hidden
|
||||
text-overflow ellipsis
|
||||
white-space nowrap
|
||||
}
|
||||
|
||||
.image-info {
|
||||
font-size 12rem
|
||||
|
||||
.item {
|
||||
padding 1rem 4rem
|
||||
background var(--third-background-color)
|
||||
border-radius 2rem
|
||||
margin-left 10rem
|
||||
|
||||
&.compressed {
|
||||
color $compressed-file-background-color
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.bottom {
|
||||
display flex
|
||||
align-items center
|
||||
|
||||
&.rename-operation {
|
||||
|
||||
.el-checkbox {
|
||||
margin-right 20rem
|
||||
}
|
||||
|
||||
.rename-input {
|
||||
input {
|
||||
height 23rem
|
||||
line-height 23rem
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.upload-status-box {
|
||||
box-sizing border-box
|
||||
color #fff
|
||||
position absolute
|
||||
right -17rem
|
||||
top -7rem
|
||||
width 46rem
|
||||
height 26rem
|
||||
text-align center
|
||||
transform rotate(45deg)
|
||||
box-shadow 0 1rem 1rem var(--border-color)
|
||||
|
||||
&.wait-upload {
|
||||
background var(--await-upload-color)
|
||||
}
|
||||
|
||||
&.uploading {
|
||||
background var(--uploading-color)
|
||||
}
|
||||
|
||||
&.uploaded {
|
||||
background var(--uploaded-color)
|
||||
}
|
||||
|
||||
i {
|
||||
font-size 12rem
|
||||
margin-top 12rem
|
||||
transform rotate(-45deg)
|
||||
}
|
||||
}
|
||||
|
||||
.remove-to-upload-image {
|
||||
position absolute
|
||||
bottom 5rem
|
||||
right 5rem
|
||||
cursor pointer
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,300 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="to-upload-image-list-card"
|
||||
v-if="toUploadImage.list.length || userConfigInfo.selectedRepos"
|
||||
>
|
||||
<div class="header">
|
||||
<div>
|
||||
<selected-info-bar />
|
||||
</div>
|
||||
<div>
|
||||
<span v-if="toUploadImage.list.length">
|
||||
已上传:{{ toUploadImage.uploadedNumber }} / {{ toUploadImage.list.length }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="body" v-if="toUploadImage.list.length">
|
||||
<ul class="image-uploading-info-box">
|
||||
<li
|
||||
class="image-uploading-info-item"
|
||||
:class="{ disable: loadingAllImage }"
|
||||
v-for="(imgItem, index) in toUploadImage.list"
|
||||
:key="index"
|
||||
>
|
||||
<div class="left-image-box">
|
||||
<img data-fancybox="gallery" :src="imgItem.imgData.base64Url" />
|
||||
</div>
|
||||
|
||||
<div class="right-operation-box">
|
||||
<div class="top">
|
||||
<div class="image-name">
|
||||
{{ imgItem.filename.now }}
|
||||
</div>
|
||||
<div class="image-info">
|
||||
<span class="file-size item" v-if="userSettings.isCompress">
|
||||
<del>
|
||||
{{ getFileSize(imgItem.fileInfo.originSize) }}
|
||||
</del>
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="file-size item"
|
||||
:class="{ compressed: userSettings.isCompress }"
|
||||
>
|
||||
{{ getFileSize(imgItem.fileInfo.size) }}
|
||||
</span>
|
||||
|
||||
<span class="last-modified item">
|
||||
{{ formatLastModified(imgItem.fileInfo.lastModified) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bottom rename-operation"
|
||||
v-if="
|
||||
!imgItem.uploadStatus.uploading && imgItem.uploadStatus.progress !== 100
|
||||
"
|
||||
>
|
||||
<!-- 哈希化 -->
|
||||
<el-checkbox
|
||||
label="哈希化"
|
||||
v-model="imgItem.filename.isHashRename"
|
||||
@change="hashRename($event, imgItem)"
|
||||
></el-checkbox>
|
||||
|
||||
<!-- 重命名 -->
|
||||
<el-checkbox
|
||||
label="重命名"
|
||||
v-model="imgItem.filename.isRename"
|
||||
@change="rename($event, imgItem)"
|
||||
></el-checkbox>
|
||||
<el-input
|
||||
class="rename-input"
|
||||
size="small"
|
||||
v-if="imgItem.filename.isRename"
|
||||
v-model="imgItem.filename.newName"
|
||||
@input="rename($event, imgItem)"
|
||||
clearable
|
||||
></el-input>
|
||||
|
||||
<!-- 命名前缀 -->
|
||||
<el-checkbox
|
||||
label="命名前缀"
|
||||
v-if="
|
||||
!imgItem.filename.isRename &&
|
||||
userConfigInfo.defaultPrefix &&
|
||||
userConfigInfo.prefixName
|
||||
"
|
||||
v-model="imgItem.filename.isPrefix"
|
||||
@change="prefixName($event, imgItem)"
|
||||
></el-checkbox>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bottom rename-operation"
|
||||
v-if="
|
||||
!imgItem.uploadStatus.uploading && imgItem.uploadStatus.progress === 100
|
||||
"
|
||||
>
|
||||
<copy-externalLink :img-obj="imgItem.uploadedImg" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="upload-status-box"
|
||||
:class="{
|
||||
'wait-upload':
|
||||
!imgItem.uploadStatus.uploading && imgItem.uploadStatus.progress !== 100,
|
||||
uploading:
|
||||
imgItem.uploadStatus.uploading && imgItem.uploadStatus.progress !== 100,
|
||||
uploaded:
|
||||
!imgItem.uploadStatus.uploading && imgItem.uploadStatus.progress === 100
|
||||
}"
|
||||
>
|
||||
<el-icon
|
||||
v-if="
|
||||
!imgItem.uploadStatus.uploading && imgItem.uploadStatus.progress !== 100
|
||||
"
|
||||
><Upload
|
||||
/></el-icon>
|
||||
|
||||
<el-icon
|
||||
class="is-loading"
|
||||
v-if="
|
||||
imgItem.uploadStatus.uploading && imgItem.uploadStatus.progress !== 100
|
||||
"
|
||||
><Loading
|
||||
/></el-icon>
|
||||
|
||||
<el-icon
|
||||
v-if="
|
||||
!imgItem.uploadStatus.uploading && imgItem.uploadStatus.progress === 100
|
||||
"
|
||||
><Check
|
||||
/></el-icon>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="remove-to-upload-image"
|
||||
v-if="
|
||||
imgItem.uploadStatus.progress !== 100 && !imgItem.uploadStatus.uploading
|
||||
"
|
||||
@click="removeToUploadImage(imgItem)"
|
||||
>
|
||||
<el-tooltip effect="dark" content="移除" placement="top">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, reactive, toRefs, onMounted } from 'vue'
|
||||
import { useStore } from '@/store'
|
||||
import { getFileSize } from '@/utils/file-handle-helper'
|
||||
import { UserConfigInfoModel } from '@/common/model/user-config-info.model'
|
||||
import { ToUploadImageModel, UploadStatusEnum } from '@/common/model/upload.model'
|
||||
import TimeHelper from '@/utils/time-helper'
|
||||
import copyExternalLink from '@/components/copy-external-link/copy-external-link.vue'
|
||||
import selectedInfoBar from '@/components/selected-info-bar/selected-info-bar.vue'
|
||||
import { uploadImage_single } from '@/utils/upload-helper'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'to-upload-image-card',
|
||||
|
||||
components: {
|
||||
copyExternalLink,
|
||||
selectedInfoBar
|
||||
},
|
||||
|
||||
props: {
|
||||
loadingAllImage: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
setup() {
|
||||
const store = useStore()
|
||||
|
||||
const reactiveData = reactive({
|
||||
isShowDialog: false,
|
||||
curImgInfo: {
|
||||
size: ''
|
||||
},
|
||||
|
||||
userConfigInfo: computed(() => store.getters.getUserConfigInfo).value,
|
||||
userSettings: computed(() => store.getters.getUserSettings).value,
|
||||
toUploadImage: computed(() => store.getters.getToUploadImage).value,
|
||||
|
||||
hashRename(e: boolean, img: any) {
|
||||
if (e) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
img.filename.now = `${img.filename.name}.${img.filename.hash}.${img.filename.suffix}`
|
||||
} else {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
img.filename.now = `${img.filename.name}.${img.filename.suffix}`
|
||||
}
|
||||
},
|
||||
|
||||
prefixName(e: boolean, img: any) {
|
||||
if (e) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
img.filename.name = `${img.filename.prefixName}${img.filename.initName}`
|
||||
} else {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
img.filename.name = `${img.filename.initName}`
|
||||
}
|
||||
if (img.filename.isHashRename) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
img.filename.now = `${img.filename.name}.${img.filename.hash}.${img.filename.suffix}`
|
||||
} else {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
img.filename.now = `${img.filename.name}.${img.filename.suffix}`
|
||||
}
|
||||
},
|
||||
|
||||
rename(e: boolean, img: any) {
|
||||
if (e) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
img.filename.name = img.filename.newName.trim().replace(/\s+/g, '-')
|
||||
} else {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
reactiveData.prefixName(img.filename.isPrefix, img) // 恢复列表prefix选项
|
||||
}
|
||||
|
||||
if (img.filename.isHashRename) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
img.filename.now = `${img.filename.name}.${img.filename.hash}.${img.filename.suffix}`
|
||||
} else {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
img.filename.now = `${img.filename.name}.${img.filename.suffix}`
|
||||
}
|
||||
},
|
||||
|
||||
getFileSize(size: number) {
|
||||
return `${getFileSize(size)} KB`
|
||||
},
|
||||
|
||||
formatLastModified(t: number) {
|
||||
return TimeHelper.formatTimestamp(t)
|
||||
},
|
||||
|
||||
async uploadImage_all(userConfigInfo: UserConfigInfoModel) {
|
||||
const uploadIndex = this.toUploadImage.uploadedNumber
|
||||
|
||||
if (uploadIndex >= this.toUploadImage.list.length) {
|
||||
return UploadStatusEnum.uploaded
|
||||
}
|
||||
|
||||
if (
|
||||
await uploadImage_single(userConfigInfo, this.toUploadImage.list[uploadIndex])
|
||||
) {
|
||||
if (uploadIndex < this.toUploadImage.list.length) {
|
||||
await this.uploadImage_all(userConfigInfo)
|
||||
return UploadStatusEnum.allUploaded
|
||||
}
|
||||
return UploadStatusEnum.uploaded
|
||||
}
|
||||
return UploadStatusEnum.uploadFail
|
||||
}
|
||||
})
|
||||
|
||||
const removeToUploadImage = (imgItem: ToUploadImageModel) => {
|
||||
store.dispatch('TO_UPLOAD_IMAGE_LIST_REMOVE', imgItem.uuid)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const {
|
||||
defaultHash: isHash,
|
||||
defaultPrefix: isPrefix,
|
||||
prefixName
|
||||
} = reactiveData.userSettings
|
||||
reactiveData.toUploadImage.list.forEach((v: ToUploadImageModel) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
v.filename.isPrefix = isPrefix
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
v.filename.prefixName = prefixName
|
||||
reactiveData.prefixName(isPrefix, v)
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
v.filename.isHashRename = isHash
|
||||
reactiveData.hashRename(isHash, v)
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
...toRefs(reactiveData),
|
||||
removeToUploadImage
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import "to-upload-image-card.styl"
|
||||
</style>
|
||||
@@ -1,40 +0,0 @@
|
||||
<template>
|
||||
<div class="tutorials-step-1">
|
||||
<h3>
|
||||
创建一个用来存储图片的
|
||||
<span class="go-create-repo" @click="goCreateRepo"> GitHub 仓库</span>
|
||||
</h3>
|
||||
<img
|
||||
src="https://cdn.staticaly.com/gh/XPoet/image-hosting@master/PicX/image.j1486dtk68n.png"
|
||||
alt="Create GitHub Repository"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
function goCreateRepo() {
|
||||
window.open('https://github.com/new')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
|
||||
.tutorials-step-1 {
|
||||
|
||||
width 800rem
|
||||
|
||||
.go-create-repo {
|
||||
cursor pointer
|
||||
color #1c81e9
|
||||
|
||||
&:hover {
|
||||
color #085fb8
|
||||
border-bottom 1rem solid #085fb8
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width 100%
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,54 +0,0 @@
|
||||
<template>
|
||||
<div class="tutorials-step-2">
|
||||
<h3>
|
||||
创建一个有 repo 权限的
|
||||
<span class="go-create-token" @click="goCreateToken"> GitHub Token</span>
|
||||
</h3>
|
||||
<img
|
||||
src="https://cdn.staticaly.com/gh/XPoet/image-hosting@master/PicX/image.lpt1xl9fu.png"
|
||||
alt="Create GitHub Token"
|
||||
/>
|
||||
<p>然后点击 Generate token 按钮,即可生成一个token,如下图:</p>
|
||||
<img
|
||||
src="https://cdn.staticaly.com/gh/XPoet/image-hosting@master/PicX/image.pzmcp6b80fk.png"
|
||||
alt="token-demo"
|
||||
/>
|
||||
<p style="color: red">
|
||||
<em>新生成的 Token 只会显示一次,请妥善保存!如有遗失,重新生成即可。</em>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
function goCreateToken() {
|
||||
window.open('https://github.com/settings/tokens/new')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
|
||||
.tutorials-step-2 {
|
||||
|
||||
width 800rem
|
||||
|
||||
.go-create-token {
|
||||
cursor pointer
|
||||
color #1c81e9
|
||||
|
||||
&:hover {
|
||||
color #085fb8
|
||||
border-bottom 1rem solid #085fb8
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
img {
|
||||
width 100%
|
||||
}
|
||||
|
||||
p {
|
||||
font-weight bold
|
||||
padding-top 20rem
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,62 +0,0 @@
|
||||
<template>
|
||||
<div class="tutorials-step-3">
|
||||
<h3>进行图床配置(绑定 GitHub Token、存储图片的仓库和目录)</h3>
|
||||
<p>1、填写 Token,自动获取该用户下的仓库</p>
|
||||
<img
|
||||
src="https://cdn.staticaly.com/gh/XPoet/image-hosting@master/PicX/image.4g8q5m7c8sq0.png"
|
||||
/>
|
||||
<br />
|
||||
<p>2、在仓库的下拉列表中,选择一个作为图床的仓库</p>
|
||||
<img
|
||||
src="https://cdn.staticaly.com/gh/XPoet/image-hosting@master/PicX/image.746g75olruk0.png"
|
||||
/>
|
||||
<br />
|
||||
<p>3、选择一种目录方式(目录即仓库里存放图片的文件夹)</p>
|
||||
<img
|
||||
src="https://cdn.staticaly.com/gh/XPoet/image-hosting@master/PicX/image.5ydmhgxjhgo0.png"
|
||||
/>
|
||||
|
||||
<div class="dir-desc-box">
|
||||
<ul>
|
||||
<li>新建目录:需手动输入一个新目录。</li>
|
||||
<li>根目录:图片将直接存储在仓库根目录下。</li>
|
||||
<li>自动目录:自动生成日期格式 YYYYMMDD 的目录。例如:20200909</li>
|
||||
<li>选择仓库目录:自动获取仓库下所有目录,选择一个即可。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="stylus">
|
||||
|
||||
.tutorials-step-3 {
|
||||
|
||||
width 800rem
|
||||
|
||||
img {
|
||||
width 100%
|
||||
}
|
||||
|
||||
p {
|
||||
font-weight bold
|
||||
padding-top 20rem
|
||||
}
|
||||
|
||||
|
||||
.dir-desc-box {
|
||||
display flex
|
||||
justify-content center
|
||||
|
||||
ul {
|
||||
width 100%
|
||||
padding 0
|
||||
|
||||
li {
|
||||
padding-top 10rem
|
||||
text-align left
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -1,57 +0,0 @@
|
||||
@import "../../style/base.styl"
|
||||
|
||||
.upload-area {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 300rem;
|
||||
border: 4rem dashed var(--third-text-color)
|
||||
box-sizing border-box
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 999;
|
||||
|
||||
&.focus {
|
||||
border-color: var(--upload-area-focus-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--upload-area-focus-color);
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1000;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
position: absolute;
|
||||
left: -9999rem;
|
||||
top: -9999rem;
|
||||
}
|
||||
|
||||
.tips {
|
||||
text-align: center;
|
||||
color: #aaa;
|
||||
|
||||
.icon {
|
||||
font-size: 100rem;
|
||||
}
|
||||
|
||||
.text {
|
||||
cursor: default;
|
||||
font-size: 20rem;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="upload-area active-upload"
|
||||
:class="{ focus: uploadAreaActive }"
|
||||
@dragover.prevent
|
||||
@drop.stop.prevent="onDrop"
|
||||
@paste="onPaste"
|
||||
v-loading="imageLoading"
|
||||
element-loading-text="图片上传中..."
|
||||
element-loading-background="rgba(0, 0, 0, 0.5)"
|
||||
>
|
||||
<label for="uploader" class="active-upload" v-if="uploadAreaActive"></label>
|
||||
<input id="uploader" type="file" @change="onSelect" multiple="multiple" />
|
||||
<div class="tips active-upload" v-if="!toUploadImage.curImgBase64Url">
|
||||
<el-icon class="icon active-upload"><UploadFilled /></el-icon>
|
||||
<div class="text active-upload">拖拽、粘贴、或点击此处上传</div>
|
||||
</div>
|
||||
<img
|
||||
class="active-upload"
|
||||
v-if="toUploadImage.curImgBase64Url"
|
||||
:src="toUploadImage.curImgBase64Url"
|
||||
alt="Pictures to be uploaded"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, reactive, toRefs } from 'vue'
|
||||
import { useStore } from '@/store'
|
||||
import { filenameHandle } from '@/utils/file-handle-helper'
|
||||
import selectedFileHandle, { handleResult } from '@/utils/selected-file-handle'
|
||||
import createToUploadImageObject from '@/utils/create-to-upload-image'
|
||||
import paste from '@/utils/paste'
|
||||
import Upload from '@/views/upload/upload.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'upload-area',
|
||||
props: {
|
||||
imageLoading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
setup() {
|
||||
const store = useStore()
|
||||
|
||||
const reactiveData = reactive({
|
||||
userConfigInfo: computed(() => store.getters.getUserConfigInfo).value,
|
||||
userSettings: computed(() => store.getters.getUserSettings).value,
|
||||
uploadAreaActive: computed((): boolean => store.getters.getUploadAreaActive),
|
||||
uploadSettings: computed(() => store.getters.getUploadSettings).value,
|
||||
toUploadImage: computed(() => store.getters.getToUploadImage).value,
|
||||
|
||||
// 选择图片
|
||||
onSelect(e: any) {
|
||||
store.commit('CHANGE_UPLOAD_AREA_ACTIVE', true)
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const file of e.target.files) {
|
||||
selectedFileHandle(file, this.uploadSettings.imageMaxSize)?.then((result) => {
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
const { base64, originalFile, compressFile } = result
|
||||
this.getImage(base64, originalFile, compressFile)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 拖拽图片
|
||||
onDrop(e: any) {
|
||||
store.commit('CHANGE_UPLOAD_AREA_ACTIVE', true)
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const file of e.dataTransfer.files) {
|
||||
selectedFileHandle(file, this.uploadSettings.imageMaxSize)?.then((result) => {
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
const { base64, originalFile, compressFile } = result
|
||||
this.getImage(base64, originalFile, compressFile)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 复制图片
|
||||
async onPaste(e: any) {
|
||||
const { base64, originalFile, compressFile }: handleResult = await paste(
|
||||
e,
|
||||
this.uploadSettings.imageMaxSize
|
||||
)
|
||||
this.getImage(base64, originalFile, compressFile)
|
||||
},
|
||||
|
||||
// 获取图片对象
|
||||
getImage(base64Data: string, originFile: File, compressFile?: File) {
|
||||
if (
|
||||
this.toUploadImage.list.length === this.toUploadImage.uploadedNumber &&
|
||||
this.toUploadImage.list.length > 0 &&
|
||||
this.toUploadImage.uploadedNumber > 0
|
||||
) {
|
||||
store.dispatch('TO_UPLOAD_IMAGE_CLEAN_LIST')
|
||||
store.dispatch('TO_UPLOAD_IMAGE_CLEAN_UPLOADED_NUMBER')
|
||||
}
|
||||
|
||||
const { defaultHash, isCompress, defaultPrefix, prefixName } = this.userSettings
|
||||
const file = isCompress ? compressFile : originFile
|
||||
const curImg = createToUploadImageObject()
|
||||
|
||||
curImg.imgData.base64Url = base64Data
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
curImg.imgData.base64Content = base64Data.split(',')[1]
|
||||
|
||||
const { name, hash, suffix } = filenameHandle(file?.name)
|
||||
curImg.uuid = hash
|
||||
curImg.fileInfo.compressedSize = compressFile?.size
|
||||
curImg.fileInfo.originSize = originFile.size
|
||||
curImg.fileInfo.size = file?.size
|
||||
curImg.fileInfo.lastModified = file?.lastModified
|
||||
|
||||
curImg.filename.initName = name
|
||||
curImg.filename.name = defaultPrefix ? `${prefixName}${name}` : name
|
||||
curImg.filename.prefixName = prefixName
|
||||
curImg.filename.hash = hash
|
||||
curImg.filename.suffix = suffix
|
||||
curImg.filename.now = defaultHash
|
||||
? `${curImg.filename.name}.${hash}.${suffix}`
|
||||
: `${curImg.filename.name}.${suffix}`
|
||||
curImg.filename.isHashRename = defaultHash
|
||||
curImg.filename.isPrefix = defaultPrefix
|
||||
|
||||
store.dispatch('TO_UPLOAD_IMAGE_LIST_ADD', JSON.parse(JSON.stringify(curImg)))
|
||||
store.dispatch('TO_UPLOAD_IMAGE_SET_CURRENT', {
|
||||
uuid: hash,
|
||||
base64Url: base64Data
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
...toRefs(reactiveData)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import "upload-area.styl"
|
||||
</style>
|
||||
@@ -1,21 +0,0 @@
|
||||
import { createApp } from 'vue'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import router from '@/router/index'
|
||||
import { key, store } from '@/store'
|
||||
import App from './App.vue'
|
||||
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||
|
||||
if (import.meta.env.MODE === 'production') {
|
||||
// @ts-ignore
|
||||
import('@/utils/register-sw.ts')
|
||||
}
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// import element-plus icons
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
// @ts-ignore
|
||||
app.use(router).use(store, key).mount('#app')
|
||||
@@ -1,32 +0,0 @@
|
||||
import type { Plugin } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||
import { ViteEnv } from '@/common/model/vite-config.model'
|
||||
|
||||
import configPWAPlugin from './pwa'
|
||||
|
||||
export default function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean) {
|
||||
const vitePlugins: (Plugin | Plugin[])[] = [vue()]
|
||||
|
||||
// On-demand import style for Element Plus
|
||||
vitePlugins.push(
|
||||
AutoImport({
|
||||
resolvers: [ElementPlusResolver()]
|
||||
}),
|
||||
Components({
|
||||
resolvers: [ElementPlusResolver()]
|
||||
})
|
||||
)
|
||||
|
||||
// production env
|
||||
if (isBuild) {
|
||||
// add plugin vite-plugin-pwa
|
||||
if (viteEnv.VITE_USE_PWA) {
|
||||
vitePlugins.push(configPWAPlugin(viteEnv))
|
||||
}
|
||||
}
|
||||
|
||||
return vitePlugins
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
* Zero config PWA for Vite
|
||||
* Plugin: vite-plugin-pwa
|
||||
* https://github.com/antfu/vite-plugin-pwa
|
||||
*/
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
import { ViteEnv } from '@/common/model/vite-config.model'
|
||||
|
||||
export default function configPWAPlugin(env: ViteEnv) {
|
||||
return VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
manifest: {
|
||||
name: env.VITE_GLOB_APP_TITLE,
|
||||
short_name: env.VITE_GLOB_APP_SHORT_NAME,
|
||||
icons: [
|
||||
{
|
||||
src: './logo@192x192.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
|
||||
import config from '@/views/config/config.vue'
|
||||
import upload from '@/views/upload/upload.vue'
|
||||
import management from '@/views/management/management.vue'
|
||||
import tutorials from '@/views/tutorials/tutorials.vue'
|
||||
import settings from '@/views/settings/settings.vue'
|
||||
import { store } from '@/store'
|
||||
|
||||
const titleSuffix = ` | PicX 图床神器`
|
||||
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'index',
|
||||
redirect: {
|
||||
name: 'upload'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/config',
|
||||
name: 'config',
|
||||
component: config,
|
||||
meta: {
|
||||
title: `图床配置${titleSuffix}`
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/upload',
|
||||
name: 'upload',
|
||||
component: upload,
|
||||
meta: {
|
||||
title: `图片上传${titleSuffix}`
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/management',
|
||||
name: 'Management',
|
||||
component: management,
|
||||
meta: {
|
||||
title: `图床管理${titleSuffix}`
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/tutorials',
|
||||
name: 'tutorials',
|
||||
component: tutorials,
|
||||
meta: {
|
||||
title: `使用教程${titleSuffix}`
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
component: () => import('@/views/about/about.vue'),
|
||||
meta: {
|
||||
title: `帮助反馈${titleSuffix}`
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'settings',
|
||||
component: settings,
|
||||
meta: {
|
||||
title: `我的设置${titleSuffix}`
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (to.meta.title) (<any>window).document.title = to.meta.title
|
||||
if (from.path === '/management') {
|
||||
store.dispatch('USER_CONFIG_INFO_RESET')
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
||||
6
picx/src/shims-vue.d.ts
vendored
6
picx/src/shims-vue.d.ts
vendored
@@ -1,6 +0,0 @@
|
||||
declare module '*.vue' {
|
||||
import { DefineComponent } from 'vue'
|
||||
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import { InjectionKey } from 'vue'
|
||||
import { createStore, Store, useStore as baseUseStore } from 'vuex'
|
||||
import RootStateTypes, { AllStateTypes } from './types'
|
||||
import dirImageListModule from './modules/dir-image-list'
|
||||
import toUploadImageModule from './modules/to-upload-image'
|
||||
import uploadedImageListModule from './modules/uploaded-image-list'
|
||||
import userConfigInfoModule from './modules/user-config-info'
|
||||
import imageViewerModule from './modules/image-viewer'
|
||||
import imageCardModule from './modules/image-card'
|
||||
import uploadAreaActiveModule from './modules/upload-area-active'
|
||||
import uploadSettingsModule from './modules/upload-settings'
|
||||
import userSettingsModule from './modules/user-settings'
|
||||
|
||||
// Create a new store instance.
|
||||
export const store = createStore<RootStateTypes>({
|
||||
modules: {
|
||||
dirImageListModule,
|
||||
toUploadImageModule,
|
||||
uploadedImageListModule,
|
||||
userConfigInfoModule,
|
||||
imageViewerModule,
|
||||
imageCardModule,
|
||||
uploadAreaActiveModule,
|
||||
uploadSettingsModule,
|
||||
userSettingsModule
|
||||
},
|
||||
|
||||
state: {
|
||||
rootName: 'root'
|
||||
},
|
||||
|
||||
mutations: {},
|
||||
|
||||
actions: {
|
||||
// 退出登录(删除 localStorage 和 sessionStorage 数据,清空 state 的值)
|
||||
LOGOUT({ dispatch, commit }) {
|
||||
dispatch('DIR_IMAGE_LOGOUT')
|
||||
dispatch('TO_UPLOAD_IMAGE_LOGOUT')
|
||||
dispatch('UPLOADED_LIST_LOGOUT')
|
||||
dispatch('USER_CONFIG_INFO_LOGOUT')
|
||||
commit('IMAGE_VIEWER_LOGOUT')
|
||||
commit('UPLOAD_AREA_ACTIVE_LOGOUT')
|
||||
commit('UPLOAD_SETTINGS_LOGOUT')
|
||||
dispatch('USER_SETTINGS_LOGOUT')
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
}
|
||||
},
|
||||
|
||||
getters: {}
|
||||
})
|
||||
|
||||
export const key: InjectionKey<Store<RootStateTypes>> = Symbol('vuex-store')
|
||||
|
||||
export function useStore<T = AllStateTypes>() {
|
||||
return baseUseStore<T>(key)
|
||||
}
|
||||
@@ -1,260 +0,0 @@
|
||||
import { Module } from 'vuex'
|
||||
import { PICX_MANAGEMENT } from '@/common/model/storage.model'
|
||||
import DirImageListStateTypes, { DirObject } from './types'
|
||||
import RootStateTypes from '../../types'
|
||||
import {
|
||||
createDirObject,
|
||||
getUpLevelDirList,
|
||||
getUpOneLevelDir
|
||||
} from '@/store/modules/dir-image-list/utils'
|
||||
import { UploadedImageModel } from '@/common/model/upload.model'
|
||||
import { getDirContent } from '@/views/management/management.util'
|
||||
|
||||
const initDirObject = () => {
|
||||
const dirObj = localStorage.getItem(PICX_MANAGEMENT)
|
||||
return dirObj ? JSON.parse(dirObj) : createDirObject('/', '/')
|
||||
}
|
||||
|
||||
const dirImageListModule: Module<DirImageListStateTypes, RootStateTypes> = {
|
||||
state: {
|
||||
name: 'dirImageListModule',
|
||||
dirObject: initDirObject()
|
||||
},
|
||||
|
||||
mutations: {},
|
||||
|
||||
actions: {
|
||||
// 图床管理 - 增加目录
|
||||
DIR_IMAGE_LIST_ADD_DIR({ state, dispatch }, dirPath: string) {
|
||||
if (dirPath === '/') {
|
||||
return
|
||||
}
|
||||
|
||||
const findAssign = (dirObj: DirObject, dir: string, dirPath: string) => {
|
||||
if (dirObj) {
|
||||
if (!dirObj.childrenDirs.some((v: DirObject) => v.dir === dir)) {
|
||||
dirObj.childrenDirs.push(createDirObject(dir, dirPath))
|
||||
}
|
||||
const temp = dirObj.childrenDirs.find((x: DirObject) => x.dir === dir)
|
||||
return temp || createDirObject(dir, dirPath)
|
||||
}
|
||||
return createDirObject(dir, dirPath)
|
||||
}
|
||||
|
||||
const dirList: string[] = dirPath.split('/')
|
||||
let dirPathC = ''
|
||||
let tempDirObj: DirObject = state.dirObject
|
||||
|
||||
// eslint-disable-next-line no-plusplus
|
||||
for (let i = 0, len = dirList.length; i < len; i++) {
|
||||
const dirName = dirList[i]
|
||||
dirPathC += `${i > 0 ? '/' : ''}${dirName}`
|
||||
tempDirObj = findAssign(tempDirObj, dirName, dirPathC)
|
||||
|
||||
if (i === 0) {
|
||||
dispatch('USER_CONFIG_INFO_ADD_DIR', dirName)
|
||||
}
|
||||
}
|
||||
|
||||
dispatch('DIR_IMAGE_LIST_PERSIST')
|
||||
},
|
||||
|
||||
// 图床管理 - 删除目录
|
||||
DIR_IMAGE_LIST_REMOVE_DIR({ state, dispatch }, dirPath: string) {
|
||||
if (dirPath === '/') {
|
||||
return
|
||||
}
|
||||
|
||||
const rmDir = (dirObj: DirObject, dir: string, isRm: boolean) => {
|
||||
if (dir === '/') {
|
||||
return state.dirObject
|
||||
}
|
||||
|
||||
const temp = dirObj.childrenDirs.find((v) => v.dir === dir)
|
||||
if (!temp) {
|
||||
return dirObj
|
||||
}
|
||||
|
||||
if (isRm) {
|
||||
const rmIndex = dirObj.childrenDirs.findIndex((v: any) => v.dir === dir)
|
||||
if (rmIndex !== -1) {
|
||||
dirObj.childrenDirs.splice(rmIndex, 1)
|
||||
}
|
||||
}
|
||||
|
||||
return temp
|
||||
}
|
||||
|
||||
const dirList = dirPath.split('/')
|
||||
|
||||
let tempDirObj = state.dirObject
|
||||
dirList.forEach((d, i) => {
|
||||
tempDirObj = rmDir(tempDirObj, d, i === dirList.length - 1)
|
||||
|
||||
// 删除在用户配置信息模块里的目录项
|
||||
if (i === 0) {
|
||||
dispatch('USER_CONFIG_INFO_REMOVE_DIR', d)
|
||||
}
|
||||
})
|
||||
|
||||
dispatch('DIR_IMAGE_LIST_PERSIST')
|
||||
},
|
||||
|
||||
// 图床管理 - 增加图片
|
||||
DIR_IMAGE_LIST_ADD_IMAGE({ state, dispatch }, item: UploadedImageModel) {
|
||||
const addImg = (
|
||||
dirObj: DirObject,
|
||||
dir: string,
|
||||
Img: UploadedImageModel,
|
||||
isAdd: boolean = false
|
||||
) => {
|
||||
if (!dirObj) {
|
||||
return state.dirObject
|
||||
}
|
||||
|
||||
const temp = dirObj.childrenDirs?.find((x: DirObject) => x.dir === dir)
|
||||
if (!temp) {
|
||||
return state.dirObject
|
||||
}
|
||||
|
||||
if (isAdd && !temp.imageList.some((v) => v.name === Img.name)) {
|
||||
temp.imageList.push(Img)
|
||||
}
|
||||
|
||||
return temp
|
||||
}
|
||||
|
||||
let tempDirObj: DirObject = state.dirObject
|
||||
|
||||
if (item.dir === '/') {
|
||||
if (!tempDirObj.imageList.some((v) => v.name === item.name)) {
|
||||
tempDirObj.imageList.push(item)
|
||||
}
|
||||
} else {
|
||||
const dirList: string[] = item.dir.split('/')
|
||||
dirList.forEach((dir, i) => {
|
||||
tempDirObj = addImg(tempDirObj, dir, item, i === dirList.length - 1)
|
||||
})
|
||||
}
|
||||
dispatch('DIR_IMAGE_LIST_PERSIST')
|
||||
},
|
||||
|
||||
// 图床管理 - 删除图片(即删除指定目录里的指定图片)
|
||||
DIR_IMAGE_LIST_REMOVE({ state, dispatch }, item: any) {
|
||||
// 删除
|
||||
const rm = (list: UploadedImageModel[], uuid: string) => {
|
||||
if (list.length) {
|
||||
const rmIndex = list.findIndex((v: any) => v.uuid === uuid)
|
||||
if (rmIndex !== -1) {
|
||||
list.splice(rmIndex, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除图片
|
||||
const rmImg = (
|
||||
dirObj: DirObject,
|
||||
dir: string,
|
||||
img: UploadedImageModel,
|
||||
isRm: boolean
|
||||
) => {
|
||||
if (!dirObj) {
|
||||
return state.dirObject
|
||||
}
|
||||
|
||||
const temp = dirObj.childrenDirs.find((x: DirObject) => x.dir === dir)
|
||||
if (!temp) {
|
||||
return state.dirObject
|
||||
}
|
||||
|
||||
if (temp.dir === dir && isRm) {
|
||||
rm(temp.imageList, img.uuid)
|
||||
}
|
||||
|
||||
return temp
|
||||
}
|
||||
|
||||
const { dir, uuid } = item
|
||||
|
||||
if (dir === '/') {
|
||||
rm(state.dirObject.imageList, uuid)
|
||||
dispatch('DIR_IMAGE_LIST_PERSIST')
|
||||
return
|
||||
}
|
||||
|
||||
const dirList: string[] = dir.split('/')
|
||||
let tempDirObj: DirObject = state.dirObject
|
||||
|
||||
dirList.forEach((d, i) => {
|
||||
tempDirObj = rmImg(tempDirObj, d, item, i === dirList.length - 1)
|
||||
if (!tempDirObj.imageList.length && !tempDirObj.childrenDirs.length) {
|
||||
const dirPathList = getUpLevelDirList(tempDirObj.dirPath)
|
||||
|
||||
// 循环遍历判断上一级目录的内容是否为空,为空则删除,依次往上查找,直到根目录
|
||||
dirPathList.forEach((dp) => {
|
||||
const dpc = getDirContent(dp, state.dirObject)
|
||||
if (dpc && !dpc.imageList.length && !dpc.childrenDirs.length) {
|
||||
const { dirPath } = getUpOneLevelDir(dp)
|
||||
dispatch('SET_USER_CONFIG_INFO', { selectedDir: dirPath })
|
||||
dispatch('DIR_IMAGE_LIST_REMOVE_DIR', dp)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 图床管理 - 初始化指定目录(即删除指定目录的子目录列表和图片列表) -- OK
|
||||
DIR_IMAGE_LIST_INIT_DIR({ state, dispatch }, dirPath: string) {
|
||||
let tempDirObj = state.dirObject
|
||||
|
||||
if (dirPath === '/') {
|
||||
tempDirObj.imageList = []
|
||||
tempDirObj.childrenDirs = []
|
||||
dispatch('DIR_IMAGE_LIST_PERSIST')
|
||||
return
|
||||
}
|
||||
|
||||
const initDirObject = (dirObj: DirObject, dir: string, isInit: boolean) => {
|
||||
if (!dirObj) {
|
||||
return state.dirObject
|
||||
}
|
||||
|
||||
const temp = dirObj.childrenDirs?.find((x: DirObject) => x.dir === dir)
|
||||
if (!temp) {
|
||||
return state.dirObject
|
||||
}
|
||||
|
||||
if (isInit) {
|
||||
temp.imageList = []
|
||||
temp.childrenDirs = []
|
||||
}
|
||||
|
||||
return temp
|
||||
}
|
||||
|
||||
const dirList = dirPath.split('/')
|
||||
|
||||
dirList.forEach((d, i) => {
|
||||
tempDirObj = initDirObject(tempDirObj, d, i === dirList.length - 1)
|
||||
})
|
||||
|
||||
dispatch('DIR_IMAGE_LIST_PERSIST')
|
||||
},
|
||||
|
||||
// 图床管理 - 持久化存储 -- OK
|
||||
DIR_IMAGE_LIST_PERSIST({ state }) {
|
||||
localStorage.setItem(PICX_MANAGEMENT, JSON.stringify(state.dirObject))
|
||||
},
|
||||
|
||||
// 图床管理 - 退出登录
|
||||
DIR_IMAGE_LOGOUT({ state }) {
|
||||
state.dirObject = createDirObject('/', '/')
|
||||
}
|
||||
},
|
||||
|
||||
getters: {
|
||||
getDirObject: (state: any) => state.dirObject
|
||||
}
|
||||
}
|
||||
|
||||
export default dirImageListModule
|
||||
@@ -1,14 +0,0 @@
|
||||
import { UploadedImageModel } from '@/common/model/upload.model'
|
||||
|
||||
export interface DirObject {
|
||||
type: 'dir'
|
||||
dir: string
|
||||
dirPath: string
|
||||
childrenDirs: DirObject[]
|
||||
imageList: UploadedImageModel[]
|
||||
}
|
||||
|
||||
export default interface DirImageListStateTypes {
|
||||
name: string
|
||||
dirObject: DirObject
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import { DirObject } from '@/store/modules/dir-image-list/types'
|
||||
|
||||
/**
|
||||
* 构造一个新的目录对象
|
||||
* @param dir
|
||||
* @param dirPath
|
||||
*/
|
||||
export const createDirObject = (dir: string, dirPath: string): DirObject => {
|
||||
return {
|
||||
type: 'dir',
|
||||
dir,
|
||||
dirPath,
|
||||
childrenDirs: [],
|
||||
imageList: []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上一级目录
|
||||
* @param dirPath
|
||||
*/
|
||||
export const getUpOneLevelDir = (dirPath: string) => {
|
||||
if (dirPath === '/') {
|
||||
return {
|
||||
currentDir: '/',
|
||||
dirPath: '/'
|
||||
}
|
||||
}
|
||||
|
||||
const dirList = dirPath.split('/')
|
||||
|
||||
if (dirList.length === 1) {
|
||||
return {
|
||||
currentDir: '/',
|
||||
dirPath: '/'
|
||||
}
|
||||
}
|
||||
|
||||
if (dirList.length > 1) {
|
||||
dirList.length -= 1
|
||||
return {
|
||||
currentDir: dirList[dirList.length - 1],
|
||||
dirPath: dirList.join('/')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
currentDir: '/',
|
||||
dirPath: '/'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上级目录列表
|
||||
* @param dirPath
|
||||
*/
|
||||
export const getUpLevelDirList = (dirPath: string) => {
|
||||
if (dirPath === '/') {
|
||||
return []
|
||||
}
|
||||
|
||||
const dirList = dirPath.split('/')
|
||||
|
||||
const tempL: string[] = []
|
||||
let tempP = ''
|
||||
|
||||
dirList.forEach((d, i) => {
|
||||
tempP += `${i > 0 ? '/' : ''}${d}`
|
||||
tempL.unshift(tempP)
|
||||
})
|
||||
|
||||
return tempL
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Module } from 'vuex'
|
||||
import { ImageCardStateTypes } from './types'
|
||||
import RootStateTypes from '../../types'
|
||||
import { UploadedImageModel } from '@/common/model/upload.model'
|
||||
|
||||
const imageCardModule: Module<ImageCardStateTypes, RootStateTypes> = {
|
||||
state: {
|
||||
imgCardArr: []
|
||||
},
|
||||
mutations: {
|
||||
IMAGE_CARD(state: ImageCardStateTypes, { imageObj }) {
|
||||
const { uuid, checked } = imageObj
|
||||
if (checked) {
|
||||
state.imgCardArr.forEach((item) => {
|
||||
if (item.uuid === uuid) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
item.checked = true
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
REPLACE_IMAGE_CARD(state: ImageCardStateTypes, { checkedImgArr }) {
|
||||
if (checkedImgArr.length > 0) {
|
||||
state.imgCardArr = checkedImgArr
|
||||
} else {
|
||||
state.imgCardArr = []
|
||||
}
|
||||
}
|
||||
},
|
||||
actions: {},
|
||||
getters: {
|
||||
getImageCardArr: (state: ImageCardStateTypes) => state.imgCardArr,
|
||||
getImageCardCheckedArr: (state: ImageCardStateTypes) => {
|
||||
return state.imgCardArr.filter((item: UploadedImageModel) => {
|
||||
return item.checked
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default imageCardModule
|
||||
@@ -1,5 +0,0 @@
|
||||
import { UploadedImageModel } from '@/common/model/upload.model'
|
||||
|
||||
export interface ImageCardStateTypes {
|
||||
imgCardArr: UploadedImageModel[]
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Module } from 'vuex'
|
||||
import ImageViewerStateTypes from './types'
|
||||
import RootStateTypes from '../../types'
|
||||
|
||||
const imageViewerModule: Module<ImageViewerStateTypes, RootStateTypes> = {
|
||||
state: {
|
||||
imageViewer: {
|
||||
imgInfo: null,
|
||||
isShow: false
|
||||
}
|
||||
},
|
||||
mutations: {
|
||||
IMAGE_VIEWER(state: ImageViewerStateTypes, { imgInfo, isShow }) {
|
||||
state.imageViewer.imgInfo = imgInfo
|
||||
state.imageViewer.isShow = isShow
|
||||
},
|
||||
|
||||
IMAGE_VIEWER_LOGOUT(state: ImageViewerStateTypes) {
|
||||
state.imageViewer.isShow = false
|
||||
state.imageViewer.imgInfo = null
|
||||
}
|
||||
},
|
||||
actions: {},
|
||||
getters: {
|
||||
getImageViewer: (state: ImageViewerStateTypes) => state.imageViewer
|
||||
}
|
||||
}
|
||||
|
||||
export default imageViewerModule
|
||||
@@ -1,12 +0,0 @@
|
||||
export interface ImgInfo {
|
||||
name: string
|
||||
size: number
|
||||
lastModified: number
|
||||
url: string
|
||||
}
|
||||
export default interface ImageViewerStateTypes {
|
||||
imageViewer: {
|
||||
imgInfo: ImgInfo | null
|
||||
isShow: boolean
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
import { ToUploadImageModel } from '@/common/model/upload.model'
|
||||
import { Module } from 'vuex'
|
||||
import ToUploadImageStateTypes from '@/store/modules/to-upload-image/types'
|
||||
import RootStateTypes from '@/store/types'
|
||||
|
||||
const toUploadImageModule: Module<ToUploadImageStateTypes, RootStateTypes> = {
|
||||
state: {
|
||||
curImgBase64Url: '',
|
||||
curImgUuid: '',
|
||||
list: [],
|
||||
uploadedNumber: 0
|
||||
},
|
||||
|
||||
actions: {
|
||||
// 要上传的图片列表 - 增加图片项
|
||||
TO_UPLOAD_IMAGE_LIST_ADD({ state }, item: ToUploadImageModel) {
|
||||
state.list.unshift(item)
|
||||
},
|
||||
|
||||
// 要上传的图片列表 - 设置当前图片的 Base64Url
|
||||
TO_UPLOAD_IMAGE_SET_CURRENT({ state }, { uuid, base64Url }) {
|
||||
state.curImgUuid = uuid
|
||||
state.curImgBase64Url = base64Url
|
||||
},
|
||||
|
||||
// 要上传的图片列表 - 上传完成的图片数量 +1
|
||||
TO_UPLOAD_IMAGE_UPLOADED({ state }) {
|
||||
state.uploadedNumber += 1
|
||||
},
|
||||
|
||||
// 要上传的图片列表 - 删除图片项
|
||||
TO_UPLOAD_IMAGE_LIST_REMOVE({ state }, uuid: string) {
|
||||
if (state.list.length > 0) {
|
||||
const rmIndex = state.list.findIndex((v: ToUploadImageModel) => v.uuid === uuid)
|
||||
if (rmIndex !== -1) {
|
||||
state.list.splice(rmIndex, 1)
|
||||
}
|
||||
if (state.list.length === 0) {
|
||||
state.curImgBase64Url = ''
|
||||
state.uploadedNumber = 0
|
||||
} else if (state.curImgUuid === uuid) {
|
||||
const cur = state.list[0]
|
||||
state.curImgBase64Url = cur.imgData.base64Url
|
||||
state.curImgUuid = cur.uuid
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 要上传的图片列表 - 上传失败时,在列表中移除已上传的图片
|
||||
TO_UPLOAD_IMAGE_LIST_FAIL({ state }) {
|
||||
if (state.list.length > 0) {
|
||||
const temp: ToUploadImageModel[] = state.list.filter(
|
||||
(v: ToUploadImageModel) => v.uploadStatus.progress !== 100
|
||||
)
|
||||
if (temp.length > 0) {
|
||||
state.list = temp
|
||||
state.uploadedNumber = 0
|
||||
state.curImgBase64Url = temp[0].imgData.base64Url
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 要上传的图片列表 - 清空 Url
|
||||
TO_UPLOAD_IMAGE_CLEAN_URL({ state }) {
|
||||
state.curImgBase64Url = ''
|
||||
},
|
||||
|
||||
// 要上传的图片列表 - 清空 List
|
||||
TO_UPLOAD_IMAGE_CLEAN_LIST({ state }) {
|
||||
state.list = []
|
||||
},
|
||||
|
||||
// 要上传的图片列表 - 清空上传完成数量
|
||||
TO_UPLOAD_IMAGE_CLEAN_UPLOADED_NUMBER({ state }) {
|
||||
state.uploadedNumber = 0
|
||||
},
|
||||
|
||||
// 要上传的图片列表 - 退出登录
|
||||
TO_UPLOAD_IMAGE_LOGOUT({ state }) {
|
||||
state.curImgBase64Url = ''
|
||||
state.list = []
|
||||
state.uploadedNumber = 0
|
||||
}
|
||||
},
|
||||
|
||||
getters: {
|
||||
getToUploadImageList: (state: ToUploadImageStateTypes) => state.list,
|
||||
getToUploadImage: (state: ToUploadImageStateTypes) => state
|
||||
}
|
||||
}
|
||||
|
||||
export default toUploadImageModule
|
||||
@@ -1,6 +0,0 @@
|
||||
export default interface ToUploadImageStateTypes {
|
||||
curImgBase64Url: string
|
||||
curImgUuid: string
|
||||
list: any[]
|
||||
uploadedNumber: number
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Module } from 'vuex'
|
||||
import UploadAreaActiveStateTypes from './types'
|
||||
import RootStateTypes from '../../types'
|
||||
|
||||
const uploadAreaActiveModule: Module<UploadAreaActiveStateTypes, RootStateTypes> = {
|
||||
state: {
|
||||
uploadAreaActive: false
|
||||
},
|
||||
mutations: {
|
||||
// 修改上传区域激活状态
|
||||
CHANGE_UPLOAD_AREA_ACTIVE(state: UploadAreaActiveStateTypes, isActive: boolean) {
|
||||
state.uploadAreaActive = isActive
|
||||
},
|
||||
|
||||
UPLOAD_AREA_ACTIVE_LOGOUT(state: UploadAreaActiveStateTypes) {
|
||||
state.uploadAreaActive = false
|
||||
}
|
||||
},
|
||||
actions: {},
|
||||
getters: {
|
||||
getUploadAreaActive: (state: UploadAreaActiveStateTypes) => state.uploadAreaActive
|
||||
}
|
||||
}
|
||||
|
||||
export default uploadAreaActiveModule
|
||||
@@ -1,3 +0,0 @@
|
||||
export default interface UploadAreaActiveStateTypes {
|
||||
uploadAreaActive: boolean
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Module } from 'vuex'
|
||||
import UploadAreaActiveStateTypes from './types'
|
||||
import RootStateTypes from '../../types'
|
||||
|
||||
const uploadSettingsModule: Module<UploadAreaActiveStateTypes, RootStateTypes> = {
|
||||
state: {
|
||||
uploadSettings: {
|
||||
isSetMaxSize: true,
|
||||
imageMaxSize: 30 * 1024
|
||||
}
|
||||
},
|
||||
mutations: {
|
||||
UPLOAD_SETTINGS_LOGOUT(state: UploadAreaActiveStateTypes) {
|
||||
state.uploadSettings.isSetMaxSize = true
|
||||
state.uploadSettings.imageMaxSize = 50 * 1024
|
||||
}
|
||||
},
|
||||
actions: {},
|
||||
getters: {
|
||||
getUploadSettings: (state) => state.uploadSettings
|
||||
}
|
||||
}
|
||||
|
||||
export default uploadSettingsModule
|
||||
@@ -1,6 +0,0 @@
|
||||
export default interface UploadSettingsStateTypes {
|
||||
uploadSettings: {
|
||||
isSetMaxSize: boolean
|
||||
imageMaxSize: number
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { Module } from 'vuex'
|
||||
import UploadedImageListStateTypes from '@/store/modules/uploaded-image-list/types'
|
||||
import RootStateTypes from '@/store/types'
|
||||
import { PICX_UPLOADED } from '@/common/model/storage.model'
|
||||
import { UploadedImageModel } from '@/common/model/upload.model'
|
||||
|
||||
const initUploadedImageList = (): UploadedImageModel[] => {
|
||||
const imageList: string | null = sessionStorage.getItem(PICX_UPLOADED)
|
||||
return imageList ? JSON.parse(imageList) : []
|
||||
}
|
||||
|
||||
const uploadedImageListModule: Module<UploadedImageListStateTypes, RootStateTypes> = {
|
||||
state: {
|
||||
uploadedImageList: initUploadedImageList()
|
||||
},
|
||||
|
||||
mutations: {},
|
||||
|
||||
actions: {
|
||||
// 上传完成的图片列表 - 增加
|
||||
UPLOADED_LIST_ADD({ state, dispatch }, item: UploadedImageModel) {
|
||||
state.uploadedImageList.unshift(item)
|
||||
dispatch('UPLOADED_LIST_PERSIST')
|
||||
},
|
||||
|
||||
// 上传完成的图片列表 - 删除
|
||||
UPLOADED_LIST_REMOVE({ state, dispatch }, uuid: string) {
|
||||
if (state.uploadedImageList.length > 0) {
|
||||
const rmIndex = state.uploadedImageList.findIndex((v) => v.uuid === uuid)
|
||||
if (rmIndex !== -1) {
|
||||
state.uploadedImageList.splice(rmIndex, 1)
|
||||
dispatch('UPLOADED_LIST_PERSIST')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 上传完成的图片列表 - 持久化
|
||||
UPLOADED_LIST_PERSIST({ state }) {
|
||||
sessionStorage.setItem(PICX_UPLOADED, JSON.stringify(state.uploadedImageList))
|
||||
},
|
||||
|
||||
// 上传完成的图片列表 - 退出登录
|
||||
UPLOADED_LIST_LOGOUT({ state }) {
|
||||
state.uploadedImageList = []
|
||||
}
|
||||
},
|
||||
|
||||
getters: {
|
||||
getUploadedImageList: (state: any) => state.uploadedImageList
|
||||
}
|
||||
}
|
||||
|
||||
export default uploadedImageListModule
|
||||
@@ -1,5 +0,0 @@
|
||||
import { UploadedImageModel } from '@/common/model/upload.model'
|
||||
|
||||
export default interface UploadedImageListStateTypes {
|
||||
uploadedImageList: UploadedImageModel[]
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
import { Module } from 'vuex'
|
||||
import {
|
||||
BranchModeEnum,
|
||||
UserConfigInfoModel
|
||||
} from '@/common/model/user-config-info.model'
|
||||
import { PICX_CONFIG } from '@/common/model/storage.model'
|
||||
import { deepAssignObject, cleanObject } from '@/utils/object-helper'
|
||||
import UserConfigInfoStateTypes from '@/store/modules/user-config-info/types'
|
||||
import RootStateTypes from '@/store/types'
|
||||
import { DirModeEnum } from '@/common/model/dir.model'
|
||||
import TimeHelper from '@/utils/time-helper'
|
||||
|
||||
const initUserConfigInfo = (): UserConfigInfoModel => {
|
||||
const initConfig: UserConfigInfoModel = {
|
||||
token: '',
|
||||
owner: '',
|
||||
email: '',
|
||||
name: '',
|
||||
avatarUrl: '',
|
||||
selectedRepos: '',
|
||||
reposList: [],
|
||||
branchMode: BranchModeEnum.reposBranch,
|
||||
branchList: [],
|
||||
selectedBranch: '',
|
||||
selectedDir: '',
|
||||
dirMode: DirModeEnum.reposDir,
|
||||
dirList: [],
|
||||
loggingStatus: false,
|
||||
selectedDirList: []
|
||||
}
|
||||
|
||||
const LSConfig: string | null = localStorage.getItem(PICX_CONFIG)
|
||||
|
||||
if (LSConfig) {
|
||||
// Assign: oldConfig -> initConfig
|
||||
deepAssignObject(initConfig, JSON.parse(LSConfig))
|
||||
|
||||
if (initConfig.selectedBranch && !initConfig.branchList.length) {
|
||||
initConfig.branchList = [
|
||||
{
|
||||
value: initConfig.selectedBranch,
|
||||
label: initConfig.selectedBranch
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
if (initConfig.dirMode === DirModeEnum.autoDir) {
|
||||
initConfig.selectedDir = TimeHelper.getYyyyMmDd()
|
||||
}
|
||||
|
||||
return initConfig
|
||||
}
|
||||
|
||||
return initConfig
|
||||
}
|
||||
|
||||
const userConfigInfoUpdate = (state: UserConfigInfoStateTypes): void => {
|
||||
const { selectedDir, selectedBranch, dirMode } = state.userConfigInfo
|
||||
if (dirMode === 'newDir') {
|
||||
const strList = selectedDir.split('')
|
||||
let count = 0
|
||||
let newStr = ''
|
||||
// eslint-disable-next-line no-plusplus
|
||||
for (let i = 0; i < strList.length; i++) {
|
||||
if (strList[i] === ' ' || strList[i] === '.' || strList[i] === '、') {
|
||||
strList[i] = '-'
|
||||
}
|
||||
if (strList[i] === '/') {
|
||||
count += 1
|
||||
}
|
||||
if (count >= 3) {
|
||||
break
|
||||
}
|
||||
newStr += strList[i]
|
||||
}
|
||||
state.userConfigInfo.selectedDir = newStr
|
||||
}
|
||||
state.userConfigInfo.selectedBranch = selectedBranch.replace(/\s+/g, '-')
|
||||
}
|
||||
|
||||
const userConfigInfoModule: Module<UserConfigInfoStateTypes, RootStateTypes> = {
|
||||
state: {
|
||||
userConfigInfo: initUserConfigInfo()
|
||||
},
|
||||
|
||||
actions: {
|
||||
// 持久化状态获取
|
||||
USER_CONFIG_INFO_RESET({ state }) {
|
||||
state.userConfigInfo = initUserConfigInfo()
|
||||
},
|
||||
// 设置用户配置信息
|
||||
SET_USER_CONFIG_INFO(
|
||||
{ state, dispatch },
|
||||
configInfo: UserConfigInfoStateTypes,
|
||||
needPersist: boolean = true
|
||||
) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const key in configInfo) {
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (state.userConfigInfo.hasOwnProperty(key)) {
|
||||
// @ts-ignore
|
||||
state.userConfigInfo[key] = configInfo[key]
|
||||
} else if (key === 'needPersist') {
|
||||
// eslint-disable-next-line
|
||||
needPersist = false
|
||||
}
|
||||
}
|
||||
if (!needPersist) return
|
||||
dispatch('USER_CONFIG_INFO_PERSIST')
|
||||
},
|
||||
|
||||
// 用户配置信息 - 增加目录
|
||||
USER_CONFIG_INFO_ADD_DIR({ state, dispatch }, dir: string) {
|
||||
if (!state.userConfigInfo.dirList.some((v: any) => v.value === dir)) {
|
||||
state.userConfigInfo.dirList.push({ label: dir, value: dir })
|
||||
dispatch('USER_CONFIG_INFO_PERSIST')
|
||||
}
|
||||
},
|
||||
|
||||
// 用户配置信息 - 删除目录列表的某个目录
|
||||
USER_CONFIG_INFO_REMOVE_DIR({ state, dispatch }, dir: string) {
|
||||
const { dirList } = state.userConfigInfo
|
||||
if (dirList.some((v: any) => v.value === dir)) {
|
||||
const rmIndex = dirList.findIndex((v: any) => v.value === dir)
|
||||
dirList.splice(rmIndex, 1)
|
||||
dispatch('USER_CONFIG_INFO_PERSIST')
|
||||
}
|
||||
},
|
||||
|
||||
// 持久化用户配置信息
|
||||
USER_CONFIG_INFO_PERSIST({ state }) {
|
||||
userConfigInfoUpdate(state)
|
||||
localStorage.setItem(PICX_CONFIG, JSON.stringify(state.userConfigInfo))
|
||||
},
|
||||
|
||||
// 修改 userConfigInfo 但无需持久化 (目前提供图床管理页面使用)
|
||||
USER_CONFIG_INFO_NOT_PERSIST({ state }) {
|
||||
userConfigInfoUpdate(state)
|
||||
},
|
||||
|
||||
// 退出登录
|
||||
USER_CONFIG_INFO_LOGOUT({ state }) {
|
||||
cleanObject(state.userConfigInfo)
|
||||
}
|
||||
},
|
||||
|
||||
getters: {
|
||||
getUserLoggingStatus: (state: UserConfigInfoStateTypes): boolean =>
|
||||
state.userConfigInfo.loggingStatus,
|
||||
getUserConfigInfo: (state: UserConfigInfoStateTypes): UserConfigInfoModel =>
|
||||
state.userConfigInfo
|
||||
}
|
||||
}
|
||||
|
||||
export default userConfigInfoModule
|
||||
@@ -1,5 +0,0 @@
|
||||
import { UserConfigInfoModel } from '@/common/model/user-config-info.model'
|
||||
|
||||
export default interface UserConfigInfoStateTypes {
|
||||
userConfigInfo: UserConfigInfoModel
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import { Module } from 'vuex'
|
||||
import { PICX_SETTINGS } from '@/common/model/storage.model'
|
||||
import { deepAssignObject } from '@/utils/object-helper'
|
||||
import UserConfigInfoStateTypes from '@/store/modules/user-config-info/types'
|
||||
import RootStateTypes from '@/store/types'
|
||||
import { CompressEncoderMap } from '@/utils/compress'
|
||||
import { UserSettingsModel } from '@/common/model/user-settings.model'
|
||||
import UserSettingsStateTypes from '@/store/modules/user-settings/types'
|
||||
import { getLocalItem } from '@/utils/common-utils'
|
||||
import ExternalLinkType from '@/common/model/external-link.model'
|
||||
|
||||
const initSettings: UserSettingsModel = {
|
||||
defaultHash: true,
|
||||
defaultMarkdown: false,
|
||||
defaultPrefix: false,
|
||||
prefixName: '',
|
||||
isCompress: true,
|
||||
compressEncoder: CompressEncoderMap.webP,
|
||||
themeMode: 'light',
|
||||
autoLightThemeTime: ['08:00', '19:00'],
|
||||
elementPlusSize: 'default',
|
||||
externalLinkType: ExternalLinkType.staticaly
|
||||
}
|
||||
|
||||
const initUserSettings = (): UserSettingsModel => {
|
||||
const LSSettings = getLocalItem(PICX_SETTINGS)
|
||||
if (LSSettings) {
|
||||
deepAssignObject(initSettings, LSSettings)
|
||||
}
|
||||
return initSettings
|
||||
}
|
||||
|
||||
const userSettingsModule: Module<UserSettingsStateTypes, RootStateTypes> = {
|
||||
state: {
|
||||
userSettings: initUserSettings()
|
||||
},
|
||||
|
||||
actions: {
|
||||
// 设置
|
||||
SET_USER_SETTINGS({ state }, configInfo: UserConfigInfoStateTypes) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const key in configInfo) {
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (state.userSettings.hasOwnProperty(key)) {
|
||||
// @ts-ignore
|
||||
state.userSettings[key] = configInfo[key]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 持久化
|
||||
USER_SETTINGS_PERSIST({ state }) {
|
||||
localStorage.setItem(PICX_SETTINGS, JSON.stringify(state.userSettings))
|
||||
},
|
||||
|
||||
// 退出登录
|
||||
USER_SETTINGS_LOGOUT({ state }) {
|
||||
state.userSettings = initSettings
|
||||
}
|
||||
},
|
||||
|
||||
getters: {
|
||||
getUserSettings: (state): UserSettingsModel => state.userSettings
|
||||
}
|
||||
}
|
||||
|
||||
export default userSettingsModule
|
||||
@@ -1,5 +0,0 @@
|
||||
import { UserSettingsModel } from '@/common/model/user-settings.model'
|
||||
|
||||
export default interface UserSettingsStateTypes {
|
||||
userSettings: UserSettingsModel
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import DirImageListStateTypes from './modules/dir-image-list/types'
|
||||
import ToUploadImageStateTypes from './modules/to-upload-image/types'
|
||||
import UploadedImageListStateTypes from './modules/uploaded-image-list/types'
|
||||
import UserConfigInfoStateTypes from './modules/user-config-info/types'
|
||||
import ImageViewerStateTypes from './modules/image-viewer/types'
|
||||
import UploadAreaActiveStateTypes from './modules/upload-area-active/types'
|
||||
import UploadSettingsStateTypes from './modules/upload-settings/types'
|
||||
|
||||
export default interface RootStateTypes {
|
||||
rootName: string
|
||||
}
|
||||
|
||||
export interface AllStateTypes extends RootStateTypes {
|
||||
dirImageListModule: DirImageListStateTypes
|
||||
toUploadImageModule: ToUploadImageStateTypes
|
||||
uploadedImageListModule: UploadedImageListStateTypes
|
||||
userConfigInfoModule: UserConfigInfoStateTypes
|
||||
imageViewerModule: ImageViewerStateTypes
|
||||
uploadAreaActiveModule: UploadAreaActiveStateTypes
|
||||
uploadSettingsModule: UploadSettingsStateTypes
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
@import './theme.styl'
|
||||
@import './variables.styl'
|
||||
|
||||
$component-interval = 16rem
|
||||
$box-border-radius = 6rem
|
||||
$content-max-width = 888rem
|
||||
$scrollbar-size = 8rem
|
||||
|
||||
:root {
|
||||
font-size 1px
|
||||
|
||||
+picx-tablet() {
|
||||
font-size 0.9px
|
||||
}
|
||||
|
||||
+picx-mobile() {
|
||||
font-size 0.8px
|
||||
}
|
||||
}
|
||||
|
||||
html, body {
|
||||
position relative
|
||||
padding 0
|
||||
margin 0
|
||||
width 100%
|
||||
height 100%
|
||||
color var(--default-text-color)
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration none
|
||||
font-size 1.5rem
|
||||
color var(--default-text-color)
|
||||
}
|
||||
|
||||
a:link {
|
||||
color var(--default-text-color)
|
||||
text-decoration none
|
||||
}
|
||||
|
||||
ul, ol, li {
|
||||
list-style none
|
||||
}
|
||||
|
||||
* {
|
||||
&::-webkit-scrollbar {
|
||||
height $scrollbar-size
|
||||
width $scrollbar-size
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background var(--scrollbar-color)
|
||||
border-radius $box-border-radius
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background transparent
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.flex-center {
|
||||
display flex
|
||||
justify-content center
|
||||
align-items center
|
||||
}
|
||||
|
||||
|
||||
.flex-start {
|
||||
display flex
|
||||
justify-content flex-start
|
||||
align-items center
|
||||
}
|
||||
|
||||
|
||||
.page-container {
|
||||
width 100%
|
||||
height 100%
|
||||
box-sizing border-box
|
||||
padding 30rem
|
||||
background var(--background-color)
|
||||
border-top-left-radius $box-border-radius
|
||||
border-top-right-radius $box-border-radius
|
||||
overflow-y auto
|
||||
}
|
||||
|
||||
.clear {
|
||||
&::after {
|
||||
content ''
|
||||
display block
|
||||
clear both
|
||||
visibility hidden
|
||||
overflow hidden
|
||||
height 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
@import './variables.styl'
|
||||
|
||||
:root {
|
||||
root-color('light')
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme light) {
|
||||
:root {
|
||||
root-color('light')
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme dark) {
|
||||
:root {
|
||||
root-color('dark')
|
||||
}
|
||||
}
|
||||
|
||||
.light {
|
||||
root-color('light')
|
||||
}
|
||||
|
||||
.dark {
|
||||
root-color('dark')
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
// ========================================================================================
|
||||
// media query
|
||||
// ========================================================================================
|
||||
$media-max-width-tablet = 800px; // media query max width (tablet)
|
||||
$media-max-width-mobile = 500px; // media query max width (mobile)
|
||||
|
||||
picx-tablet()
|
||||
@media (max-width: $media-max-width-tablet)
|
||||
{ block }
|
||||
|
||||
picx-mobile()
|
||||
@media (max-width: $media-max-width-mobile)
|
||||
{ block }
|
||||
|
||||
|
||||
// ========================================================================================
|
||||
// z-index
|
||||
// ========================================================================================
|
||||
$z-index-1 = 1001;
|
||||
$z-index-2 = 1002;
|
||||
$z-index-3 = 1003;
|
||||
$z-index-4 = 1004;
|
||||
$z-index-5 = 1005;
|
||||
$z-index-6 = 1006;
|
||||
$z-index-7 = 1007;
|
||||
$z-index-8 = 1008;
|
||||
$z-index-9 = 1009;
|
||||
|
||||
|
||||
// ========================================================================================
|
||||
// light mode color
|
||||
// ========================================================================================
|
||||
$primary-color = #0066CC;
|
||||
$background-color = #fff;
|
||||
$second-background-color = darken($background-color, 5%);
|
||||
$third-background-color = darken($background-color, 10%);
|
||||
$default-text-color = #50505c;
|
||||
$first-text-color = darken($default-text-color, 10%);
|
||||
$second-text-color = darken($default-text-color, 5%);
|
||||
$third-text-color = lighten($default-text-color, 30%);
|
||||
$fourth-text-color = lighten($default-text-color, 90%);
|
||||
$border-color = darken($background-color, 30%);
|
||||
$selection-color = lighten($primary-color, 10%);
|
||||
$shadow-color = rgba(0, 0, 0, 0.2);
|
||||
$shadow-hover-color = rgba(0, 0, 0, 0.28);
|
||||
$scrollbar-color = darken($background-color, 20%);
|
||||
$scroll-bar-bg-color = darken($background-color, 30%);
|
||||
$upload-area-focus-color = #0066CC;
|
||||
$await-upload-color = #E6A23C;
|
||||
$uploading-color = #409EFF;
|
||||
$uploaded-color = #67C23A;
|
||||
$markdown-icon-color = #808080;
|
||||
$markdown-icon-active-color = darken($markdown-icon-color, 30%);
|
||||
|
||||
|
||||
|
||||
// ========================================================================================
|
||||
// dark mode color
|
||||
// ========================================================================================
|
||||
$dark-primary-color = #0066CC;
|
||||
$dark-background-color = #2a2a2f;
|
||||
$dark-second-background-color = darken($dark-background-color, 10%);
|
||||
$dark-third-background-color = darken($dark-background-color, 15%);
|
||||
$dark-default-text-color = #bebec6;
|
||||
$dark-first-text-color = lighten($dark-default-text-color, 30%);
|
||||
$dark-second-text-color = lighten($dark-default-text-color, 20%);
|
||||
$dark-third-text-color = darken($dark-default-text-color, 20%);
|
||||
$dark-fourth-text-color = darken($dark-default-text-color, 80%);
|
||||
$dark-border-color = lighten($dark-background-color, 20%);
|
||||
$dark-selection-color = $selection-color;
|
||||
$dark-shadow-color = rgba(128, 128, 128, 0.2);
|
||||
$dark-shadow-hover-color = rgba(128, 128, 128, 0.28);
|
||||
$dark-scrollbar-color = darken($dark-background-color, 20%);
|
||||
$dark-scroll-bar-bg-color = lighten($dark-background-color, 30%);
|
||||
$dark-upload-area-focus-color = #1070d0;
|
||||
$dark-await-upload-color = #c08327;
|
||||
$dark-uploading-color = #287dd5;
|
||||
$dark-uploaded-color = #55b626;
|
||||
$dark-markdown-icon-color = #aaa;
|
||||
$dark-markdown-icon-active-color = lighten($dark-markdown-icon-color, 30%);
|
||||
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// light/dark mode color
|
||||
// ========================================================================
|
||||
root-color(mode) {
|
||||
--background-color: mode == 'light' ? $background-color : $dark-background-color;
|
||||
--second-background-color: mode == 'light' ? $second-background-color : $dark-second-background-color;
|
||||
--third-background-color: mode == 'light' ? $third-background-color : $dark-third-background-color;
|
||||
--primary-color: mode == 'light' ? $primary-color : $dark-primary-color;
|
||||
--first-text-color: mode == 'light' ? $first-text-color : $dark-first-text-color;
|
||||
--second-text-color: mode == 'light' ? $second-text-color : $dark-second-text-color;
|
||||
--third-text-color: mode == 'light' ? $third-text-color : $dark-third-text-color;
|
||||
--fourth-text-color: mode == 'light' ? $fourth-text-color : $dark-fourth-text-color;
|
||||
--default-text-color: mode == 'light' ? $default-text-color : $dark-default-text-color;
|
||||
--border-color: mode == 'light' ? $border-color : $dark-border-color;
|
||||
--selection-color: mode == 'light' ? $selection-color : $dark-selection-color;
|
||||
--shadow-color: mode == 'light' ? $shadow-color : $dark-shadow-color;
|
||||
--shadow-hover-color: mode == 'light' ? $shadow-hover-color : $dark-shadow-hover-color;
|
||||
--scrollbar-color: mode == 'light' ? $scrollbar-color : $dark-scrollbar-color;
|
||||
--scroll-bar-bg-color: mode == 'light' ? $scroll-bar-bg-color : $dark-scroll-bar-bg-color;
|
||||
--upload-area-focus-color : mode == 'light' ? $upload-area-focus-color : $dark-upload-area-focus-color;
|
||||
--await-upload-color : mode == 'light' ? $await-upload-color : $dark-await-upload-color;
|
||||
--uploading-color : mode == 'light' ? $uploading-color : $dark-uploading-color;
|
||||
--uploaded-color : mode == 'light' ? $uploaded-color : $dark-uploaded-color;
|
||||
--markdown-icon-color : mode == 'light' ? $markdown-icon-color : $dark-markdown-icon-color;
|
||||
--markdown-icon-active-color : mode == 'light' ? $markdown-icon-active-color : $dark-markdown-icon-active-color;
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import Axios from 'axios'
|
||||
import { PICX_CONFIG } from '@/common/model/storage.model'
|
||||
|
||||
const baseURL = 'https://api.github.com'
|
||||
|
||||
const axios = Axios.create({
|
||||
baseURL,
|
||||
timeout: 300000 // request timeout 请求超时 5m
|
||||
})
|
||||
|
||||
axios.defaults.headers['Content-Type'] = 'application/json'
|
||||
|
||||
// 发起请求之前的拦截器(前置拦截)
|
||||
axios.interceptors.request.use(
|
||||
(config) => {
|
||||
const userConfig = localStorage.getItem(PICX_CONFIG)
|
||||
|
||||
if (userConfig) {
|
||||
const { token } = JSON.parse(userConfig)
|
||||
if (token) {
|
||||
config.headers.Authorization = `token ${token}`
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
axios.interceptors.response.use(
|
||||
(response) => {
|
||||
return response
|
||||
},
|
||||
(error) => {
|
||||
if (error.response && error.response.data) {
|
||||
const code = error.response.status
|
||||
const msg = error.response.data.message
|
||||
ElMessage.error(`Code: ${code}, Message: ${msg}`)
|
||||
console.error(`[PicX Error]`, error.response)
|
||||
} else {
|
||||
ElMessage.error(`${error}`)
|
||||
}
|
||||
|
||||
return error.response
|
||||
}
|
||||
)
|
||||
|
||||
export default axios
|
||||
@@ -1,27 +0,0 @@
|
||||
/**
|
||||
* Get JavaScript basic data types
|
||||
* @param data
|
||||
* @returns {string} array | string | number ...
|
||||
*/
|
||||
export const getType = (data: string) => {
|
||||
const type = Object.prototype.toString.call(data).split(' ')[1]
|
||||
return type.substring(0, type.length - 1).toLowerCase()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a string(uuid) that is not repeated
|
||||
* @returns uuid {string}
|
||||
*/
|
||||
export const getUuid = () => {
|
||||
return Number(Math.random().toString().substr(2, 5) + Date.now()).toString(36)
|
||||
}
|
||||
|
||||
/**
|
||||
* get localStorage value
|
||||
* @param key
|
||||
* @returns {*}
|
||||
*/
|
||||
export const getLocalItem = (key: string) => {
|
||||
const temp = window.localStorage.getItem(key)
|
||||
return temp ? JSON.parse(temp) : null
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import Compress from '@yireen/squoosh-browser'
|
||||
import {
|
||||
defaultPreprocessorState,
|
||||
defaultProcessorState,
|
||||
encoderMap,
|
||||
EncoderState
|
||||
} from '@yireen/squoosh-browser/dist/client/lazy-app/feature-meta'
|
||||
|
||||
export enum CompressEncoderMap {
|
||||
mozJPEG = 'mozJPEG',
|
||||
avif = 'avif',
|
||||
webP = 'webP'
|
||||
}
|
||||
|
||||
export const compress = async (file: File, encoder: CompressEncoderMap) => {
|
||||
const compress = new Compress(file, {
|
||||
encoderState: {
|
||||
type: encoder,
|
||||
options: encoderMap[encoder].meta.defaultOptions
|
||||
} as EncoderState,
|
||||
processorState: defaultProcessorState,
|
||||
preprocessorState: defaultPreprocessorState
|
||||
})
|
||||
|
||||
return compress.process()
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { ToUploadImageModel } from '../common/model/upload.model'
|
||||
|
||||
export default (): ToUploadImageModel => {
|
||||
return {
|
||||
uuid: '',
|
||||
|
||||
uploadStatus: {
|
||||
progress: 0,
|
||||
uploading: false
|
||||
},
|
||||
|
||||
imgData: {
|
||||
base64Content: '',
|
||||
base64Url: ''
|
||||
},
|
||||
|
||||
fileInfo: {
|
||||
size: 0,
|
||||
lastModified: 0
|
||||
},
|
||||
|
||||
filename: {
|
||||
name: '',
|
||||
hash: '',
|
||||
suffix: '',
|
||||
prefixName: '',
|
||||
now: '',
|
||||
initName: '',
|
||||
newName: 'xxx',
|
||||
isHashRename: true,
|
||||
isRename: false,
|
||||
isPrefix: false
|
||||
},
|
||||
|
||||
externalLink: {
|
||||
github: '',
|
||||
jsdelivr: '',
|
||||
staticaly: '',
|
||||
cloudflare: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import { UploadedImageModel } from '../common/model/upload.model'
|
||||
import { UserConfigInfoModel } from '../common/model/user-config-info.model'
|
||||
import axios from '@/utils/axios'
|
||||
import { deleteStatusEnum } from '../common/model/delete.model'
|
||||
import { store } from '@/store'
|
||||
|
||||
let deleteIndex = 0
|
||||
|
||||
export async function deleteSingleImage(
|
||||
imageObj: UploadedImageModel,
|
||||
userConfigInfo: UserConfigInfoModel
|
||||
): Promise<boolean> {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
imageObj.deleting = true
|
||||
const { owner, selectedRepos } = userConfigInfo
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.delete(`/repos/${owner}/${selectedRepos}/contents/${imageObj.path}`, {
|
||||
data: {
|
||||
owner,
|
||||
repo: selectedRepos,
|
||||
path: imageObj.path,
|
||||
message: 'delete picture via PicX(https://github.com/XPoet/picx)',
|
||||
sha: imageObj.sha
|
||||
}
|
||||
})
|
||||
.then((res) => {
|
||||
if (res && res.status === 200) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
imageObj.deleting = false
|
||||
store.dispatch('UPLOADED_LIST_REMOVE', imageObj.uuid)
|
||||
store.dispatch('DIR_IMAGE_LIST_REMOVE', imageObj)
|
||||
resolve(true)
|
||||
} else {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
imageObj.deleting = false
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function delelteBatchImage(
|
||||
imgCardArr: Array<UploadedImageModel>,
|
||||
userConfigInfo: UserConfigInfoModel
|
||||
) {
|
||||
if (deleteIndex >= imgCardArr.length) {
|
||||
return deleteStatusEnum.deleted
|
||||
}
|
||||
if (await deleteSingleImage(imgCardArr[deleteIndex], userConfigInfo)) {
|
||||
if (deleteIndex < imgCardArr.length) {
|
||||
deleteIndex += 1
|
||||
if (await delelteBatchImage(imgCardArr, userConfigInfo)) {
|
||||
deleteIndex = 0
|
||||
return deleteStatusEnum.allDeleted
|
||||
}
|
||||
}
|
||||
return deleteStatusEnum.deleted
|
||||
}
|
||||
return deleteStatusEnum.deleteFail
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import { Recordable, ViteEnv } from '@/common/model/vite-config.model'
|
||||
|
||||
// Read all environment variable configuration files to process.env
|
||||
export default function wrapperEnv(envConf: Recordable): ViteEnv {
|
||||
const ret: any = {}
|
||||
|
||||
for (const envName of Object.keys(envConf)) {
|
||||
let realName = envConf[envName].replace(/\\n/g, '\n')
|
||||
if (realName === 'true') {
|
||||
realName = true
|
||||
} else if (realName === 'false') {
|
||||
realName = false
|
||||
}
|
||||
|
||||
if (envName === 'VITE_PORT') {
|
||||
realName = Number(realName)
|
||||
}
|
||||
ret[envName] = realName
|
||||
process.env[envName] = realName
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
import ExternalLinkType from '@/common/model/external-link.model'
|
||||
import { UserConfigInfoModel } from '@/common/model/user-config-info.model'
|
||||
import { getFilename } from '@/utils/file-handle-helper'
|
||||
import { UploadedImageModel } from '@/common/model/upload.model'
|
||||
|
||||
/**
|
||||
* 创建承载图片外链文本的 DOM 元素
|
||||
*/
|
||||
export const createExternalLinkDom = () => {
|
||||
let externalLinkDom: any = document.querySelector('.temp-external-link-txt')
|
||||
if (!externalLinkDom) {
|
||||
externalLinkDom = document.createElement('textarea')
|
||||
externalLinkDom.setAttribute('class', 'temp-external-link-txt')
|
||||
externalLinkDom.style.position = 'absolute'
|
||||
externalLinkDom.style.top = '-99999rem'
|
||||
externalLinkDom.style.left = '-99999rem'
|
||||
document.body.appendChild(externalLinkDom)
|
||||
}
|
||||
return externalLinkDom
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成图片外链
|
||||
* @param type
|
||||
* @param content
|
||||
* @param config
|
||||
*/
|
||||
export const generateExternalLink = (
|
||||
type: ExternalLinkType,
|
||||
content: any,
|
||||
config: UserConfigInfoModel
|
||||
): string => {
|
||||
const staticalyLink: string = `https://cdn.staticaly.com/gh/${config.owner}/${config.selectedRepos}@${config.selectedBranch}/${content.path}`
|
||||
const cloudflareLink: string = `https://git.poker/${config.owner}/${config.selectedRepos}/blob/${config.selectedBranch}/${content.path}?raw=true`
|
||||
const jsdelivrLink: string = `https://cdn.jsdelivr.net/gh/${config.owner}/${config.selectedRepos}@${config.selectedBranch}/${content.path}`
|
||||
const githubLink: string = decodeURI(content.download_url)
|
||||
|
||||
// eslint-disable-next-line default-case
|
||||
switch (type) {
|
||||
case ExternalLinkType.staticaly:
|
||||
return staticalyLink
|
||||
|
||||
case ExternalLinkType.cloudflare:
|
||||
return cloudflareLink
|
||||
|
||||
case ExternalLinkType.jsdelivr:
|
||||
return jsdelivrLink
|
||||
|
||||
case ExternalLinkType.github:
|
||||
return githubLink
|
||||
|
||||
default:
|
||||
return githubLink
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片外链转换为 Markdown 格式
|
||||
* @param name 图片名
|
||||
* @param url 图片外链
|
||||
*/
|
||||
export const formatMarkdown = (name: string, url: string): string => {
|
||||
return ``
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制图片外链
|
||||
* @param img 图片对象
|
||||
* @param type CDN 类型
|
||||
*/
|
||||
export const copyExternalLink = (img: UploadedImageModel, type: ExternalLinkType) => {
|
||||
let externalLink = ''
|
||||
let successInfo = ''
|
||||
const { name, is_transform_md: isMD } = img
|
||||
|
||||
switch (type) {
|
||||
case ExternalLinkType.jsdelivr:
|
||||
if (isMD) {
|
||||
externalLink = formatMarkdown(name, img.jsdelivr_cdn_url)
|
||||
successInfo = 'Markdown 格式的 jsDelivr CDN'
|
||||
} else {
|
||||
externalLink = img.jsdelivr_cdn_url
|
||||
successInfo = 'jsDelivr CDN'
|
||||
}
|
||||
break
|
||||
|
||||
case ExternalLinkType.staticaly:
|
||||
if (isMD) {
|
||||
externalLink = formatMarkdown(name, img.staticaly_cdn_url)
|
||||
successInfo = 'Markdown 格式的 Staticaly CDN'
|
||||
} else {
|
||||
externalLink = img.staticaly_cdn_url
|
||||
successInfo = 'Staticaly CDN'
|
||||
}
|
||||
break
|
||||
|
||||
case ExternalLinkType.cloudflare:
|
||||
if (isMD) {
|
||||
externalLink = formatMarkdown(name, img.cloudflare_cdn_url)
|
||||
successInfo = 'Markdown 格式的 Cloudflare CDN'
|
||||
} else {
|
||||
externalLink = img.cloudflare_cdn_url
|
||||
successInfo = 'Cloudflare CDN'
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
if (isMD) {
|
||||
externalLink = formatMarkdown(name, img.github_url)
|
||||
successInfo = 'Markdown 格式的 GitHub'
|
||||
} else {
|
||||
externalLink = img.github_url
|
||||
successInfo = 'GitHub'
|
||||
}
|
||||
}
|
||||
|
||||
const externalLinkDom: any = createExternalLinkDom()
|
||||
|
||||
externalLinkDom.value = externalLink
|
||||
externalLinkDom.select()
|
||||
document.execCommand('copy')
|
||||
ElMessage.success(`${successInfo} 外链复制成功!`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量复制图片外链
|
||||
* @param imgCardList 图片列表
|
||||
* @param type 当前选择的外链类型
|
||||
*/
|
||||
export const batchCopyExternalLink = (
|
||||
imgCardList: Array<UploadedImageModel>,
|
||||
type: ExternalLinkType
|
||||
) => {
|
||||
let externalLink = ''
|
||||
const externalLinkDom: any = createExternalLinkDom()
|
||||
externalLinkDom.value = ''
|
||||
if (imgCardList?.length > 0) {
|
||||
imgCardList.forEach((item: UploadedImageModel, index) => {
|
||||
const isMD = item.is_transform_md
|
||||
switch (type) {
|
||||
case ExternalLinkType.jsdelivr:
|
||||
externalLink = isMD
|
||||
? formatMarkdown(item.name, item.jsdelivr_cdn_url)
|
||||
: item.jsdelivr_cdn_url
|
||||
break
|
||||
|
||||
case ExternalLinkType.staticaly:
|
||||
externalLink = isMD
|
||||
? formatMarkdown(item.name, item.staticaly_cdn_url)
|
||||
: item.staticaly_cdn_url
|
||||
break
|
||||
|
||||
case ExternalLinkType.cloudflare:
|
||||
externalLink = isMD
|
||||
? formatMarkdown(item.name, item.cloudflare_cdn_url)
|
||||
: item.cloudflare_cdn_url
|
||||
break
|
||||
|
||||
default:
|
||||
externalLink = isMD
|
||||
? formatMarkdown(item.name, item.github_url)
|
||||
: item.github_url
|
||||
}
|
||||
|
||||
if (index < imgCardList.length - 1) {
|
||||
// eslint-disable-next-line prefer-template
|
||||
externalLinkDom.value += externalLink + '\n'
|
||||
} else {
|
||||
externalLinkDom.value += externalLink
|
||||
}
|
||||
})
|
||||
externalLinkDom.select()
|
||||
document.execCommand('copy')
|
||||
ElMessage.success(`批量复制图片链接成功`)
|
||||
} else {
|
||||
console.warn('请先选择图片')
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { getUuid } from './common-utils'
|
||||
|
||||
/**
|
||||
* get filename
|
||||
* @param filename
|
||||
*/
|
||||
export const getFilename = (filename: string) => {
|
||||
const splitIndex = filename.indexOf('.')
|
||||
return filename.substr(0, splitIndex).trim().replace(/\s+/g, '-')
|
||||
}
|
||||
|
||||
/**
|
||||
* get filename suffix
|
||||
* @param filename
|
||||
*/
|
||||
export const getFileSuffix = (filename: string) => {
|
||||
const splitIndex = filename.lastIndexOf('.')
|
||||
return filename.substr(splitIndex + 1, filename.length)
|
||||
}
|
||||
|
||||
export const isImage = (suffix: string): boolean => {
|
||||
return /(png|jpg|gif|jpeg|webp|avif|svg\+xml|image\/x-icon)$/.test(suffix)
|
||||
}
|
||||
|
||||
/**
|
||||
* get file size (KB)
|
||||
* @param size
|
||||
*/
|
||||
export const getFileSize = (size: number) => {
|
||||
return Number((size / 1024).toFixed(2))
|
||||
}
|
||||
|
||||
/**
|
||||
* filename handle
|
||||
* @param filename
|
||||
*/
|
||||
export const filenameHandle = (filename: string | undefined) => {
|
||||
if (filename) {
|
||||
return {
|
||||
name: getFilename(filename),
|
||||
hash: getUuid(),
|
||||
suffix: getFileSuffix(filename)
|
||||
}
|
||||
}
|
||||
return {
|
||||
name: '',
|
||||
hash: '',
|
||||
suffix: ''
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { computed } from 'vue'
|
||||
import { UploadedImageModel } from '@/common/model/upload.model'
|
||||
import { getUuid } from '@/utils/common-utils'
|
||||
import { generateExternalLink } from '@/utils/external-link-handler'
|
||||
import ExternalLinkType from '@/common/model/external-link.model'
|
||||
import { store } from '@/store'
|
||||
|
||||
const userConfigInfo = computed(() => store.getters.getUserConfigInfo).value
|
||||
|
||||
export default function structureImageObject(
|
||||
item: any,
|
||||
selectedDir: string
|
||||
): UploadedImageModel {
|
||||
return {
|
||||
type: 'image',
|
||||
uuid: getUuid(),
|
||||
dir: selectedDir,
|
||||
name: item.name,
|
||||
path: item.path,
|
||||
sha: item.sha,
|
||||
deleting: false,
|
||||
is_transform_md: false,
|
||||
size: item.size,
|
||||
checked: false,
|
||||
github_url: generateExternalLink(ExternalLinkType.github, item, userConfigInfo),
|
||||
jsdelivr_cdn_url: generateExternalLink(
|
||||
ExternalLinkType.jsdelivr,
|
||||
item,
|
||||
userConfigInfo
|
||||
),
|
||||
staticaly_cdn_url: generateExternalLink(
|
||||
ExternalLinkType.staticaly,
|
||||
item,
|
||||
userConfigInfo
|
||||
),
|
||||
cloudflare_cdn_url: generateExternalLink(
|
||||
ExternalLinkType.cloudflare,
|
||||
item,
|
||||
userConfigInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import { getType } from './common-utils'
|
||||
|
||||
/**
|
||||
* 根据 object 每个 key 上值的数据类型,赋对应的初始值
|
||||
* @param object
|
||||
*/
|
||||
export const cleanObject = (object: any) => {
|
||||
// eslint-disable-next-line guard-for-in,no-restricted-syntax
|
||||
for (const key in object) {
|
||||
// eslint-disable-next-line default-case
|
||||
switch (getType(object[key])) {
|
||||
case 'object':
|
||||
cleanObject(object[key])
|
||||
break
|
||||
|
||||
case 'string':
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
object[key] = ''
|
||||
break
|
||||
|
||||
case 'array':
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
object[key] = []
|
||||
break
|
||||
|
||||
case 'number':
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
object[key] = 0
|
||||
break
|
||||
|
||||
case 'boolean':
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
object[key] = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 obj2 对象的值深度赋值给 obj1 对象
|
||||
* @param obj1{Object}
|
||||
* @param obj2{Object}
|
||||
*/
|
||||
export const deepAssignObject = (obj1: object, obj2: object): any => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const key in obj2) {
|
||||
// @ts-ignore
|
||||
if (getType(obj2[key]) !== 'object') {
|
||||
if (obj1) {
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
obj1[key] = obj2[key]
|
||||
}
|
||||
} else {
|
||||
// @ts-ignore
|
||||
deepAssignObject(obj1[key], obj2[key])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import selectedFileHandle from './selected-file-handle'
|
||||
|
||||
const onPaste = (e: any, maxsize: number): Promise<any> | null => {
|
||||
if (!(e.clipboardData && e.clipboardData.items)) {
|
||||
return null
|
||||
}
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
return new Promise((resolve) => {
|
||||
// eslint-disable-next-line no-plusplus
|
||||
for (let i = 0, len = e.clipboardData.items.length; i < len; i++) {
|
||||
const item = e.clipboardData.items[i]
|
||||
if (item.kind === 'file') {
|
||||
const pasteFile = item.getAsFile()
|
||||
|
||||
selectedFileHandle(pasteFile, maxsize)?.then((result) => {
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
const { base64, originalFile, compressFile } = result
|
||||
resolve({ base64, originalFile, compressFile })
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default onPaste
|
||||
@@ -1,3 +0,0 @@
|
||||
import { registerSW } from 'virtual:pwa-register'
|
||||
|
||||
registerSW()
|
||||
@@ -1,56 +0,0 @@
|
||||
import { computed } from 'vue'
|
||||
import { store } from '@/store'
|
||||
import createToUploadImageObject from '@/utils/create-to-upload-image'
|
||||
import { filenameHandle } from './file-handle-helper'
|
||||
|
||||
/**
|
||||
* 根据图片链接获取图片 base64 编码
|
||||
* @param url 图片路径
|
||||
* @param ext 图片格式
|
||||
*/
|
||||
export function getBase64ByImageUrl(url: string, ext: string): Promise<string | null> {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
const img = new Image()
|
||||
img.crossOrigin = 'Anonymous'
|
||||
img.src = url
|
||||
return new Promise((resolve) => {
|
||||
img.onload = () => {
|
||||
const { width } = img
|
||||
const { height } = img
|
||||
canvas.width = width // 指定画板的高度,自定义
|
||||
canvas.height = height // 指定画板的宽度,自定义
|
||||
ctx?.drawImage(img, 0, 0, width, height) // 参数可自定义
|
||||
const dataURL: string = canvas.toDataURL(`image/${ext}`)
|
||||
resolve(dataURL)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取图片对象
|
||||
export function getImage(base64Data: string, file: any) {
|
||||
const userSettings = computed(() => store.getters.getUserSettings).value
|
||||
const curImg = createToUploadImageObject()
|
||||
|
||||
curImg.imgData.base64Url = base64Data
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
curImg.imgData.base64Content = base64Data.split(',')[1]
|
||||
|
||||
const { name, hash, suffix } = filenameHandle(file.name)
|
||||
curImg.uuid = hash
|
||||
|
||||
curImg.fileInfo.size = file.size
|
||||
curImg.fileInfo.originSize = file.size
|
||||
curImg.fileInfo.lastModified = file.lastModified
|
||||
|
||||
curImg.filename.name = name
|
||||
curImg.filename.hash = hash
|
||||
curImg.filename.suffix = suffix
|
||||
curImg.filename.now = userSettings.defaultHash
|
||||
? `${name}.${hash}.${suffix}`
|
||||
: `${name}.${suffix}`
|
||||
curImg.filename.initName = name
|
||||
curImg.filename.isHashRename = userSettings.defaultHash
|
||||
|
||||
return curImg
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import { store } from '@/store'
|
||||
import { compress } from './compress'
|
||||
import { getFileSize, isImage } from './file-handle-helper'
|
||||
|
||||
export type handleResult = { base64: string; originalFile: File; compressFile?: File }
|
||||
|
||||
const selectedFileHandle = async (
|
||||
file: File,
|
||||
maxsize: number
|
||||
): Promise<handleResult | null> => {
|
||||
if (!file) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!isImage(file.type)) {
|
||||
ElMessage.error('该文件格式不支持!')
|
||||
return null
|
||||
}
|
||||
let compressFile: NonNullable<File>
|
||||
const { isCompress, compressEncoder } = store.getters.getUserSettings
|
||||
const isGif = file.type === 'image/gif'
|
||||
if (!isGif && isCompress) {
|
||||
const loadingInstance = ElLoading.service({
|
||||
target: '.upload-area',
|
||||
text: '正在压缩图片'
|
||||
})
|
||||
compressFile = await compress(file, compressEncoder)
|
||||
loadingInstance.close()
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const reader = new FileReader()
|
||||
reader.readAsDataURL(!isGif && isCompress ? compressFile : file)
|
||||
reader.onload = (e: ProgressEvent<FileReader>) => {
|
||||
const base64: any = e.target?.result
|
||||
const curImgSize = getFileSize(base64.length)
|
||||
|
||||
if (curImgSize >= maxsize) {
|
||||
// 给出提示,引导用户自行去压缩图片
|
||||
ElMessageBox.confirm(
|
||||
`当前图片 ${(curImgSize / 1024).toFixed(
|
||||
2
|
||||
)} M,CDN 只能加速小于 50 MB 的图片,建议使用第三方工具 TinyPNG 压缩`,
|
||||
'图片过大,禁止上传',
|
||||
{
|
||||
confirmButtonText: '前往 TinyPNG',
|
||||
cancelButtonText: '放弃上传'
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
window.open('https://tinypng.com/')
|
||||
})
|
||||
.catch(() => {
|
||||
console.log('放弃上传')
|
||||
})
|
||||
} else {
|
||||
resolve({
|
||||
base64,
|
||||
originalFile: file,
|
||||
compressFile: !isGif && isCompress ? compressFile : file
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default selectedFileHandle
|
||||
@@ -1,51 +0,0 @@
|
||||
import { watch, nextTick } from 'vue'
|
||||
import { useStore } from '@/store'
|
||||
import { UserSettingsModel } from '@/common/model/user-settings.model'
|
||||
|
||||
const setThemeMode = () => {
|
||||
const store = useStore()
|
||||
|
||||
const setBodyClassName = async (theme: 'dark' | 'light') => {
|
||||
await nextTick(() => {
|
||||
const body = document.getElementsByTagName('html')[0]
|
||||
if (theme === 'dark') {
|
||||
body.classList.remove('light')
|
||||
body.classList.add('dark')
|
||||
} else {
|
||||
body.classList.remove('dark')
|
||||
body.classList.add('light')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const autoThemeModeTimeHandle = (autoLightThemeTime: string[]) => {
|
||||
const getTimestamp = (i: number) => {
|
||||
const D = new Date()
|
||||
const yyyy = D.getFullYear()
|
||||
const mm = D.getMonth() + 1
|
||||
const dd = D.getDate()
|
||||
return new Date(`${yyyy}/${mm}/${dd} ${autoLightThemeTime[i]}:00`).getTime()
|
||||
}
|
||||
const now = Date.now()
|
||||
return getTimestamp(0) <= now && now <= getTimestamp(1)
|
||||
}
|
||||
|
||||
const setThemeByConfigFn = (settings: UserSettingsModel) => {
|
||||
const { themeMode, autoLightThemeTime } = settings
|
||||
if (themeMode === 'auto') {
|
||||
setBodyClassName(autoThemeModeTimeHandle(autoLightThemeTime) ? 'light' : 'dark')
|
||||
} else {
|
||||
setBodyClassName(themeMode)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
(): UserSettingsModel => store.getters.getUserSettings,
|
||||
(newValue) => {
|
||||
setThemeByConfigFn(newValue)
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
}
|
||||
|
||||
export default setThemeMode
|
||||
@@ -1,26 +0,0 @@
|
||||
export default class TimeHelper {
|
||||
private static zerofill(n: number) {
|
||||
return n < 10 ? `0${n}` : n
|
||||
}
|
||||
|
||||
static getYyyyMmDd(now: number = Date.now()) {
|
||||
const date: Date = new Date(now)
|
||||
const yyyy = date.getFullYear()
|
||||
const MM = date.getMonth() + 1
|
||||
const DD = date.getDate()
|
||||
return `${yyyy}${this.zerofill(MM)}${this.zerofill(DD)}`
|
||||
}
|
||||
|
||||
static formatTimestamp(now: number = Date.now()) {
|
||||
const date: Date = new Date(now)
|
||||
const YYYY = date.getFullYear()
|
||||
const MM = date.getMonth() + 1
|
||||
const DD = date.getDate()
|
||||
const hh = date.getHours()
|
||||
const mm = date.getMinutes()
|
||||
const ss = date.getSeconds()
|
||||
return `${YYYY}-${this.zerofill(MM)}-${this.zerofill(DD)} ${this.zerofill(
|
||||
hh
|
||||
)}:${this.zerofill(mm)}:${this.zerofill(ss)}`
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
import { UserConfigInfoModel } from '@/common/model/user-config-info.model'
|
||||
import { ToUploadImageModel, UploadedImageModel } from '@/common/model/upload.model'
|
||||
import axios from '@/utils/axios'
|
||||
import { store } from '@/store'
|
||||
import { generateExternalLink } from '@/utils/external-link-handler'
|
||||
import ExternalLinkType from '@/common/model/external-link.model'
|
||||
|
||||
export const uploadUrlHandle = (
|
||||
config: UserConfigInfoModel,
|
||||
filename: string
|
||||
): string => {
|
||||
let path = ''
|
||||
if (config.selectedDir !== '/') {
|
||||
path = `${config.selectedDir}/`
|
||||
}
|
||||
return `/repos/${config.owner}/${config.selectedRepos}/contents/${path}${filename}`
|
||||
}
|
||||
|
||||
export function uploadImage_single(
|
||||
userConfigInfo: UserConfigInfoModel,
|
||||
img: ToUploadImageModel
|
||||
): Promise<Boolean> {
|
||||
const { selectedBranch, email, owner } = userConfigInfo
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
img.uploadStatus.uploading = true
|
||||
|
||||
const data: any = {
|
||||
message: 'Upload picture via PicX(https://github.com/XPoet/picx)',
|
||||
branch: selectedBranch,
|
||||
content: img.imgData.base64Content
|
||||
}
|
||||
|
||||
if (email) {
|
||||
data.committer = {
|
||||
name: owner,
|
||||
email
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.put(uploadUrlHandle(userConfigInfo, img.filename.now), data)
|
||||
.then((res) => {
|
||||
if (res && res.status === 201) {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
uploadedHandle(res, img, userConfigInfo)
|
||||
store.dispatch('TO_UPLOAD_IMAGE_UPLOADED', img.uuid)
|
||||
resolve(true)
|
||||
} else {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
img.uploadStatus.uploading = false
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function uploadedHandle(
|
||||
res: any,
|
||||
img: ToUploadImageModel,
|
||||
userConfigInfo: UserConfigInfoModel
|
||||
) {
|
||||
const userSettings = store.getters.getUserSettings
|
||||
|
||||
// 上传状态处理
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
img.uploadStatus.progress = 100
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
img.uploadStatus.uploading = false
|
||||
|
||||
// 生成 GitHub 外链
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
img.externalLink.github = generateExternalLink(
|
||||
ExternalLinkType.github,
|
||||
res.data.content,
|
||||
userConfigInfo
|
||||
)
|
||||
|
||||
// 生成 jsDelivr CDN 外链
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
img.externalLink.jsdelivr = generateExternalLink(
|
||||
ExternalLinkType.jsdelivr,
|
||||
res.data.content,
|
||||
userConfigInfo
|
||||
)
|
||||
|
||||
// 生成 Staticaly CDN 外链
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
img.externalLink.staticaly = generateExternalLink(
|
||||
ExternalLinkType.staticaly,
|
||||
res.data.content,
|
||||
userConfigInfo
|
||||
)
|
||||
|
||||
// 生成 Cloudflare CDN 外链
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
img.externalLink.cloudflare = generateExternalLink(
|
||||
ExternalLinkType.cloudflare,
|
||||
res.data.content,
|
||||
userConfigInfo
|
||||
)
|
||||
|
||||
const item: UploadedImageModel = {
|
||||
checked: false,
|
||||
type: 'image',
|
||||
uuid: img.uuid,
|
||||
dir: userConfigInfo.selectedDir,
|
||||
name: res.data.content.name,
|
||||
path: res.data.content.path,
|
||||
sha: res.data.content.sha,
|
||||
github_url: img.externalLink.github,
|
||||
jsdelivr_cdn_url: img.externalLink.jsdelivr,
|
||||
staticaly_cdn_url: img.externalLink.staticaly,
|
||||
cloudflare_cdn_url: img.externalLink.cloudflare,
|
||||
is_transform_md: userSettings.defaultMarkdown,
|
||||
deleting: false,
|
||||
size: img.fileInfo.size
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
img.uploadedImg = item
|
||||
|
||||
// uploadedList 增加图片
|
||||
store.dispatch('UPLOADED_LIST_ADD', item)
|
||||
|
||||
// dirImageList 增加目录
|
||||
store.dispatch('DIR_IMAGE_LIST_ADD_DIR', userConfigInfo.selectedDir)
|
||||
|
||||
// dirImageList 增加图片
|
||||
store.dispatch('DIR_IMAGE_LIST_ADD_IMAGE', item)
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
.feedback-page-container {
|
||||
|
||||
.help-info-item {
|
||||
font-size: 16rem;
|
||||
padding: 6rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.description {
|
||||
font-weight: bold;
|
||||
line-height: 28rem;
|
||||
}
|
||||
|
||||
.red-text {
|
||||
color: #de1a1a;
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
<template>
|
||||
<div class="page-container feedback-page-container">
|
||||
<div class="help-info-item description">
|
||||
PicX 是一款基于 GitHub API & jsDelivr 开发的具有 CDN 加速功能的图床工具。
|
||||
<br />
|
||||
无需下载!无需安装!打开网站即用!免费!极速!稳定!
|
||||
</div>
|
||||
|
||||
<div class="help-info-item">建议将本站添加至浏览器收藏夹,方便下次使用 😊</div>
|
||||
|
||||
<div class="help-info-item">
|
||||
作者:
|
||||
<el-link type="primary" href="https://xpoet.cn/" target="_blank">@XPoet</el-link>
|
||||
</div>
|
||||
|
||||
<div class="help-info-item">
|
||||
仓库:
|
||||
<el-link type="primary" href="https://github.com/XPoet/picx" target="_blank">
|
||||
https://github.com/XPoet/picx
|
||||
</el-link>
|
||||
</div>
|
||||
|
||||
<div class="help-info-item">
|
||||
教程:
|
||||
<el-link
|
||||
type="primary"
|
||||
href="https://github.com/XPoet/picx/blob/master/README.md"
|
||||
target="_blank"
|
||||
>
|
||||
https://github.com/XPoet/picx/blob/master/README.md
|
||||
</el-link>
|
||||
</div>
|
||||
|
||||
<div class="help-info-item">
|
||||
在使用过程中遇到问题,请仔细阅读文档,或者给作者提
|
||||
<el-link
|
||||
type="primary"
|
||||
style="font-size: 16rem"
|
||||
href="https://github.com/XPoet/picx/issues"
|
||||
target="_blank"
|
||||
>
|
||||
Issue
|
||||
</el-link>
|
||||
。
|
||||
</div>
|
||||
|
||||
<div class="help-info-item red-text">
|
||||
图片上传缓慢或加载不出来等情况,可借助
|
||||
<el-link
|
||||
style="font-size: 16rem"
|
||||
type="primary"
|
||||
href="https://github.com/Alvin9999/new-pac/wiki"
|
||||
target="_blank"
|
||||
>VPN 工具
|
||||
</el-link>
|
||||
。
|
||||
</div>
|
||||
|
||||
<div class="help-info-item">
|
||||
<strong>
|
||||
郑重声明:请勿通过本站上传违反你当地法律的图片,所造成的一切后果与本站无关。
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'about'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import "about.styl"
|
||||
</style>
|
||||
@@ -1,13 +0,0 @@
|
||||
.config-page-container {
|
||||
.operation {
|
||||
text-align right
|
||||
|
||||
.el-button {
|
||||
margin-left 20rem
|
||||
|
||||
&:first-child {
|
||||
margin-left 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,498 +0,0 @@
|
||||
<template>
|
||||
<div class="page-container config-page-container">
|
||||
<!-- Token -->
|
||||
<el-form label-width="70rem" :label-position="labelPosition" class="config-form">
|
||||
<el-form-item label="Token">
|
||||
<el-input
|
||||
v-model="userConfigInfo.token"
|
||||
clearable
|
||||
:autofocus="!userConfigInfo.token"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="请输入 GitHub Token"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item class="operation">
|
||||
<el-button
|
||||
plain
|
||||
type="primary"
|
||||
native-type="submit"
|
||||
@click.prevent="getUserInfo()"
|
||||
>
|
||||
确认 Token
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<el-form
|
||||
label-width="70rem"
|
||||
:label-position="labelPosition"
|
||||
v-if="userConfigInfo.token"
|
||||
v-loading="loading"
|
||||
element-loading-text="加载中..."
|
||||
>
|
||||
<el-form-item v-if="userConfigInfo.owner" label="用户名">
|
||||
<el-input v-model="userConfigInfo.owner" readonly></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="userConfigInfo.email" label="邮箱">
|
||||
<el-input v-model="userConfigInfo.email" readonly></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="userConfigInfo.reposList.length" label="选择仓库">
|
||||
<el-select
|
||||
v-model="userConfigInfo.selectedRepos"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
placeholder="请选择图床仓库..."
|
||||
@change="selectRepos"
|
||||
>
|
||||
<el-option
|
||||
v-for="(repos, index) in userConfigInfo.reposList"
|
||||
:key="index"
|
||||
:label="repos.label"
|
||||
:value="repos.value"
|
||||
>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 分支 -->
|
||||
<el-form
|
||||
label-width="70rem"
|
||||
:label-position="labelPosition"
|
||||
v-if="userConfigInfo.selectedRepos && userConfigInfo.branchList.length"
|
||||
v-loading="branchLoading"
|
||||
element-loading-text="加载中..."
|
||||
>
|
||||
<!-- 因未验证 API 是否能创建空分支,暂时不开启分支选择方式 && 0 -->
|
||||
<el-form-item v-if="userConfigInfo.selectedRepos && 0" label="分支方式">
|
||||
<el-radio-group v-model="userConfigInfo.branchMode" @change="branchModeChange">
|
||||
<el-tooltip
|
||||
v-if="userConfigInfo.branchList.length"
|
||||
:content="'选择 ' + userConfigInfo.selectedRepos + ' 仓库下的一个分支'"
|
||||
placement="top"
|
||||
>
|
||||
<el-radio label="reposBranch">
|
||||
选择 {{ userConfigInfo.selectedRepos }} 仓库下的分支
|
||||
</el-radio>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="手动创建一个新分支" placement="top">
|
||||
<el-radio label="newBranch">新建分支</el-radio>
|
||||
</el-tooltip>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
v-if="
|
||||
userConfigInfo.branchList.length > 1 &&
|
||||
userConfigInfo.branchMode === 'reposBranch'
|
||||
"
|
||||
label="选择分支"
|
||||
>
|
||||
<el-select
|
||||
v-model="userConfigInfo.selectedBranch"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
placeholder="请选择分支..."
|
||||
@change="selectBranch"
|
||||
>
|
||||
<el-option
|
||||
v-for="(repos, reposIndex) in userConfigInfo.branchList"
|
||||
:key="reposIndex"
|
||||
:label="repos.label"
|
||||
:value="repos.value"
|
||||
>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="userConfigInfo.branchMode === 'newBranch'" label="新建分支">
|
||||
<el-input
|
||||
v-model="userConfigInfo.selectedBranch"
|
||||
@input="persistUserConfigInfo()"
|
||||
clearable
|
||||
placeholder="请输入新建的分支..."
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 目录 -->
|
||||
<el-form
|
||||
label-width="70rem"
|
||||
:label-position="labelPosition"
|
||||
v-if="userConfigInfo.selectedBranch"
|
||||
v-loading="dirLoading"
|
||||
element-loading-text="加载中..."
|
||||
>
|
||||
<el-form-item v-if="userConfigInfo.selectedBranch" label="目录方式">
|
||||
<el-radio-group v-model="userConfigInfo.dirMode" @change="dirModeChange">
|
||||
<el-tooltip content="手动输入一个新目录" placement="top" :offset="-1">
|
||||
<el-radio label="newDir">新建目录</el-radio>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip
|
||||
:content="'图片存储在 ' + userConfigInfo.selectedBranch + ' 分支的根目录下'"
|
||||
placement="top"
|
||||
:offset="-1"
|
||||
>
|
||||
<el-radio label="rootDir">根目录</el-radio>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip
|
||||
:content="'根据日期自动创建格式 YYYYMMDD 的目录'"
|
||||
placement="top"
|
||||
:offset="-1"
|
||||
>
|
||||
<el-radio label="autoDir">自动目录</el-radio>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip
|
||||
v-if="
|
||||
userConfigInfo.dirList.length && userConfigInfo.branchMode !== 'newBranch'
|
||||
"
|
||||
:content="'选择 ' + userConfigInfo.selectedBranch + ' 分支下的一个目录'"
|
||||
placement="top"
|
||||
:offset="-1"
|
||||
>
|
||||
<el-radio label="reposDir">
|
||||
选择 {{ userConfigInfo.selectedRepos }} 仓库目录
|
||||
</el-radio>
|
||||
</el-tooltip>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="userConfigInfo.dirMode === 'autoDir'" label="自动目录">
|
||||
<el-input v-model="userConfigInfo.selectedDir" readonly></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="userConfigInfo.dirMode === 'rootDir'" label="根目录">
|
||||
<el-input v-model="userConfigInfo.selectedDir" readonly></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="userConfigInfo.dirMode === 'newDir'" label="新建目录">
|
||||
<el-input
|
||||
v-model="userConfigInfo.selectedDir"
|
||||
@input="persistUserConfigInfo()"
|
||||
clearable
|
||||
placeholder="请输入新建的目录..."
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
v-if="
|
||||
userConfigInfo.dirList.length &&
|
||||
userConfigInfo.dirMode === 'reposDir' &&
|
||||
userConfigInfo.branchMode !== 'newBranch'
|
||||
"
|
||||
label="选择目录"
|
||||
>
|
||||
<el-cascader
|
||||
style="width: 100%"
|
||||
:props="cascaderProps"
|
||||
:key="elCascaderKey"
|
||||
v-model="userConfigInfo.selectedDirList"
|
||||
filterable
|
||||
placeholder="请选择一个目录..."
|
||||
clearable
|
||||
@change="cascaderChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 操作(重置、完成配置) -->
|
||||
<el-form label-width="70rem" :label-position="labelPosition">
|
||||
<el-form-item class="operation">
|
||||
<el-button plain type="warning" @click="reset()" v-if="userConfigInfo.owner">
|
||||
重置
|
||||
</el-button>
|
||||
<el-button
|
||||
plain
|
||||
type="success"
|
||||
@click="goUpload"
|
||||
v-if="userConfigInfo.selectedRepos"
|
||||
>
|
||||
完成配置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useStore } from '@/store'
|
||||
import { DirModeEnum } from '@/common/model/dir.model'
|
||||
import { BranchModeEnum } from '@/common/model/user-config-info.model'
|
||||
import axios from '@/utils/axios'
|
||||
import TimeHelper from '@/utils/time-helper'
|
||||
import { getDirListByPath } from '@/common/api'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useStore()
|
||||
|
||||
const userConfigInfo = computed(() => store.getters.getUserConfigInfo).value
|
||||
const loggingStatus = computed(() => store.getters.getUserConfigInfo).value
|
||||
const userSettings = computed(() => store.getters.getUserSettings).value
|
||||
|
||||
const loading = ref(false)
|
||||
const dirLoading = ref(false)
|
||||
const branchLoading = ref(false)
|
||||
|
||||
const labelPosition = computed(() => {
|
||||
return userSettings.elementPlusSize === 'default' ? 'top' : 'right'
|
||||
})
|
||||
|
||||
const elCascaderKey = ref<string>('elCascaderKey')
|
||||
|
||||
function persistUserConfigInfo() {
|
||||
store.dispatch('USER_CONFIG_INFO_PERSIST')
|
||||
}
|
||||
|
||||
function saveUserInfo(res: any) {
|
||||
userConfigInfo.loggingStatus = true
|
||||
userConfigInfo.owner = res.data.login
|
||||
userConfigInfo.name = res.data.name
|
||||
userConfigInfo.email = res.data.email
|
||||
userConfigInfo.avatarUrl = res.data.avatar_url
|
||||
persistUserConfigInfo()
|
||||
}
|
||||
|
||||
function getReposList(reposUrl: string) {
|
||||
axios
|
||||
.get(reposUrl, {
|
||||
params: {
|
||||
type: 'public',
|
||||
sort: 'created',
|
||||
per_page: 100
|
||||
}
|
||||
})
|
||||
.then((res: any) => {
|
||||
console.log('[getReposList] ', res)
|
||||
if (res.status === 200 && res.data.length > 0) {
|
||||
userConfigInfo.reposList = []
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const repos of res.data) {
|
||||
if (!repos.fork && !repos.private) {
|
||||
userConfigInfo.reposList.push({
|
||||
value: repos.name,
|
||||
label: repos.name,
|
||||
desc: repos.description
|
||||
})
|
||||
}
|
||||
}
|
||||
loading.value = false
|
||||
persistUserConfigInfo()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function getDirList() {
|
||||
dirLoading.value = true
|
||||
userConfigInfo.dirList = await getDirListByPath()
|
||||
persistUserConfigInfo()
|
||||
dirLoading.value = false
|
||||
}
|
||||
|
||||
function dirModeChange(dirMode: DirModeEnum) {
|
||||
switch (dirMode) {
|
||||
case DirModeEnum.rootDir:
|
||||
// 根目录
|
||||
userConfigInfo.selectedDir = '/'
|
||||
break
|
||||
|
||||
case DirModeEnum.autoDir:
|
||||
// 自动目录,根据当天日期自动生成
|
||||
userConfigInfo.selectedDir = TimeHelper.getYyyyMmDd()
|
||||
break
|
||||
|
||||
case DirModeEnum.newDir:
|
||||
// 手动输入的新建目录
|
||||
userConfigInfo.selectedDir = 'xxx'
|
||||
break
|
||||
|
||||
case DirModeEnum.reposDir:
|
||||
// 仓库目录
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const { dirList } = userConfigInfo
|
||||
if (dirList.length) {
|
||||
userConfigInfo.selectedDir = dirList[0].value
|
||||
} else {
|
||||
userConfigInfo.selectedDir = ''
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
userConfigInfo.selectedDir = '/'
|
||||
break
|
||||
}
|
||||
persistUserConfigInfo()
|
||||
}
|
||||
|
||||
function getBranchList(repos: string) {
|
||||
branchLoading.value = true
|
||||
axios.get(`/repos/${userConfigInfo.owner}/${repos}/branches`).then((res: any) => {
|
||||
console.log('[getBranchList] ', res)
|
||||
if (res && res.status === 200) {
|
||||
branchLoading.value = false
|
||||
if (res.data.length > 0) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const item of res.data) {
|
||||
userConfigInfo.branchList.push({
|
||||
value: item.name,
|
||||
label: item.name
|
||||
})
|
||||
}
|
||||
userConfigInfo.branchList.reverse()
|
||||
userConfigInfo.selectedBranch = userConfigInfo.branchList[0].value
|
||||
userConfigInfo.branchMode = BranchModeEnum.reposBranch
|
||||
getDirList()
|
||||
} else {
|
||||
userConfigInfo.selectedBranch = 'master'
|
||||
userConfigInfo.branchMode = BranchModeEnum.newBranch
|
||||
}
|
||||
dirModeChange(userConfigInfo.dirMode)
|
||||
persistUserConfigInfo()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getUserInfo() {
|
||||
if (userConfigInfo.token) {
|
||||
loading.value = true
|
||||
axios
|
||||
.get('/user', {
|
||||
headers: { Authorization: `token ${userConfigInfo.token}` }
|
||||
})
|
||||
.then((res: any) => {
|
||||
console.log('[getUserInfo] ', res)
|
||||
if (res && res.status === 200) {
|
||||
saveUserInfo(res)
|
||||
getReposList(res.data.repos_url)
|
||||
} else {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
} else {
|
||||
ElMessage.warning('Token 不能为空!')
|
||||
}
|
||||
}
|
||||
|
||||
function selectRepos(repos: string) {
|
||||
userConfigInfo.branchList = []
|
||||
userConfigInfo.dirList = []
|
||||
getBranchList(repos)
|
||||
persistUserConfigInfo()
|
||||
}
|
||||
|
||||
async function selectBranch(branch: string) {
|
||||
userConfigInfo.selectedBranch = branch
|
||||
await getDirList()
|
||||
elCascaderKey.value = userConfigInfo.selectedBranch
|
||||
userConfigInfo.selectedDir = userConfigInfo.dirList[0].value
|
||||
userConfigInfo.selectedDirList = [userConfigInfo.selectedDir]
|
||||
persistUserConfigInfo()
|
||||
}
|
||||
|
||||
function branchModeChange(mode: BranchModeEnum) {
|
||||
const selBranch = userConfigInfo.selectedBranch
|
||||
const bv = userConfigInfo.branchList[0].value
|
||||
|
||||
switch (mode) {
|
||||
case BranchModeEnum.newBranch:
|
||||
userConfigInfo.selectedBranch = 'xxx'
|
||||
userConfigInfo.dirMode = DirModeEnum.newDir
|
||||
userConfigInfo.selectedDir = 'xxx'
|
||||
break
|
||||
|
||||
case BranchModeEnum.reposBranch:
|
||||
if (selBranch !== bv) {
|
||||
userConfigInfo.selectedBranch = bv
|
||||
getDirList()
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
userConfigInfo.selectedBranch = ''
|
||||
break
|
||||
}
|
||||
persistUserConfigInfo()
|
||||
}
|
||||
|
||||
function reset() {
|
||||
loading.value = false
|
||||
dirLoading.value = false
|
||||
store.dispatch('LOGOUT')
|
||||
}
|
||||
|
||||
function goUpload() {
|
||||
const { selectedDir, dirMode } = userConfigInfo
|
||||
let warningMessage: string = '目录不能为空!'
|
||||
|
||||
if (selectedDir === '') {
|
||||
switch (dirMode) {
|
||||
case DirModeEnum.newDir:
|
||||
warningMessage = '请在输入框输入一个新目录!'
|
||||
break
|
||||
case DirModeEnum.reposDir:
|
||||
warningMessage = `请选择 ${userConfigInfo.selectedRepos} 仓库下的一个目录!`
|
||||
break
|
||||
default:
|
||||
warningMessage = '请在输入框输入一个新目录!'
|
||||
break
|
||||
}
|
||||
ElMessage.warning(warningMessage)
|
||||
} else {
|
||||
router.push('/upload')
|
||||
}
|
||||
}
|
||||
|
||||
const cascaderProps = {
|
||||
lazy: true,
|
||||
checkStrictly: true,
|
||||
async lazyLoad(node: any, resolve: any) {
|
||||
const { level, pathLabels } = node
|
||||
let dirs: any
|
||||
if (level === 0) {
|
||||
dirs = userConfigInfo.dirList
|
||||
} else {
|
||||
dirs = await getDirListByPath(pathLabels.join('/'))
|
||||
}
|
||||
if (dirs) {
|
||||
resolve(
|
||||
dirs.map((x: any) => ({
|
||||
value: x.value,
|
||||
label: x.label,
|
||||
leaf: level >= 2
|
||||
}))
|
||||
)
|
||||
} else {
|
||||
resolve([])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cascaderChange(e: string[]) {
|
||||
userConfigInfo.selectedDirList = e
|
||||
userConfigInfo.selectedDir = e.join('/')
|
||||
persistUserConfigInfo()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => loggingStatus,
|
||||
(_n) => {
|
||||
if (!_n) {
|
||||
loading.value = false
|
||||
dirLoading.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import "config.styl"
|
||||
</style>
|
||||
@@ -1,70 +0,0 @@
|
||||
@import "../../style/base.styl"
|
||||
|
||||
$infoBarHeight = 50rem
|
||||
|
||||
.management-page-container {
|
||||
padding-bottom 0 !important
|
||||
|
||||
.content-container {
|
||||
position relative
|
||||
width 100%
|
||||
height 100%
|
||||
padding-top $infoBarHeight
|
||||
box-sizing border-box
|
||||
|
||||
.top {
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
width 100%
|
||||
height $infoBarHeight
|
||||
box-sizing border-box
|
||||
display flex
|
||||
align-items center
|
||||
justify-content space-between
|
||||
font-size 14rem
|
||||
padding-bottom 20rem
|
||||
|
||||
.right {
|
||||
|
||||
.btn-icon {
|
||||
cursor pointer
|
||||
font-size 22rem
|
||||
margin-left 10rem
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.bottom {
|
||||
position relative
|
||||
width 100%
|
||||
height 100%
|
||||
box-sizing border-box
|
||||
border 1rem solid var(--border-color)
|
||||
|
||||
.image-list {
|
||||
width 100%
|
||||
//height 100%
|
||||
//max-height calc(100% - 60rem)
|
||||
margin 0
|
||||
padding 2rem
|
||||
list-style none
|
||||
overflow-y auto
|
||||
box-sizing border-box
|
||||
|
||||
li.image-item {
|
||||
float left
|
||||
box-sizing border-box
|
||||
padding 10rem
|
||||
|
||||
&:last-child {
|
||||
margin-right 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import { Store } from 'vuex'
|
||||
import { DirObject } from '@/store/modules/dir-image-list/types'
|
||||
|
||||
function getContent(targetContent: any, dirList: string[], n: number): any {
|
||||
if (targetContent) {
|
||||
if (dirList.length === n) {
|
||||
return targetContent
|
||||
}
|
||||
return getContent(
|
||||
targetContent.childrenDirs?.find((v: any) => v.dir === dirList[n]),
|
||||
dirList,
|
||||
// eslint-disable-next-line no-param-reassign,no-plusplus
|
||||
++n
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前目录下所有内容(子目录和图片)
|
||||
* @param dirPath
|
||||
* @param dirObj
|
||||
*/
|
||||
export const getDirContent = (dirPath: string, dirObj: DirObject) => {
|
||||
if (dirPath === '/') {
|
||||
return dirObj
|
||||
}
|
||||
const dirList: string[] = dirPath.split('/')
|
||||
return getContent(dirObj, dirList, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤当前目录的内容(子目录或图片)
|
||||
* @param dirPath
|
||||
* @param content
|
||||
* @param type
|
||||
*/
|
||||
export const filterDirContent = (dirPath: string, content: any, type: string): any => {
|
||||
if (type === 'dir') {
|
||||
return content.childrenDirs?.filter((x: any) => x.type === 'dir')
|
||||
}
|
||||
|
||||
if (type === 'image') {
|
||||
return content.imageList.filter((x: any) => x.type === 'image')
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
export const dirModeHandle = (dir: string, store: Store<any>) => {
|
||||
if (dir === '/') {
|
||||
store.dispatch('SET_USER_CONFIG_INFO', {
|
||||
dirMode: 'rootDir',
|
||||
needPersist: false
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
<template>
|
||||
<div class="page-container management-page-container">
|
||||
<div class="content-container">
|
||||
<div class="top">
|
||||
<div class="left">
|
||||
<selected-info-bar />
|
||||
</div>
|
||||
<div class="right flex-start">
|
||||
<el-tooltip
|
||||
placement="top"
|
||||
:content="listing ? '切换方块展示' : '切换列表展示'"
|
||||
>
|
||||
<el-icon class="btn-icon" @click.stop="toggleListing">
|
||||
<Tickets v-if="listing" />
|
||||
<Menu v-if="!listing" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
<el-tooltip placement="top" content="重新加载图片">
|
||||
<el-icon class="btn-icon" @click.stop="reloadCurrentDirContent">
|
||||
<Refresh />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom" v-loading="loadingImageList" element-loading-text="加载中...">
|
||||
<image-selector
|
||||
v-if="currentPathImageList.length"
|
||||
:currentDirImageList="currentPathImageList"
|
||||
@update:initImageList="currentPathImageList"
|
||||
:key="renderKey"
|
||||
></image-selector>
|
||||
<ul
|
||||
class="image-list"
|
||||
:style="{
|
||||
height: isShowBatchTools ? 'calc(100% - 50rem)' : '100%'
|
||||
}"
|
||||
>
|
||||
<li class="image-item" v-if="userConfigInfo.selectedDir !== '/'">
|
||||
<folder-card mode="back" />
|
||||
</li>
|
||||
<li class="image-item" v-for="(dir, index) in currentPathDirList" :key="index">
|
||||
<folder-card :folder-obj="dir" />
|
||||
</li>
|
||||
<div class="clear"></div>
|
||||
<li
|
||||
class="image-item"
|
||||
v-for="(image, index) in currentPathImageList"
|
||||
:key="index"
|
||||
:style="{
|
||||
width: listing ? '50%' : '230rem',
|
||||
height: listing ? '80rem' : '240rem'
|
||||
}"
|
||||
>
|
||||
<image-card
|
||||
:image-obj="image"
|
||||
:listing="listing"
|
||||
v-model="activeIndex"
|
||||
:index="index"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, watch, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useStore } from '@/store'
|
||||
import { getContentByReposPath } from '@/common/api'
|
||||
import {
|
||||
dirModeHandle,
|
||||
filterDirContent,
|
||||
getDirContent
|
||||
} from '@/views/management/management.util'
|
||||
|
||||
import imageCard from '@/components/image-card/image-card.vue'
|
||||
import selectedInfoBar from '@/components/selected-info-bar/selected-info-bar.vue'
|
||||
import folderCard from '@/components/folder-card/folder-card.vue'
|
||||
import imageSelector from '@/components/image-selector/image-selector.vue'
|
||||
import { UploadedImageModel } from '@/common/model/upload.model'
|
||||
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
|
||||
const userConfigInfo = computed(() => store.getters.getUserConfigInfo).value
|
||||
const loggingStatus = computed(() => store.getters.getUserLoggingStatus).value
|
||||
const dirObject = computed(() => store.getters.getDirObject).value
|
||||
|
||||
const renderKey = ref(new Date().getTime()) // key for update image-selector component
|
||||
const loadingImageList = ref(false)
|
||||
const listing = ref(false)
|
||||
const activeIndex = ref<number>()
|
||||
|
||||
const currentPathDirList = ref([])
|
||||
const currentPathImageList = ref([])
|
||||
|
||||
async function dirContentHandle(dir: string) {
|
||||
loadingImageList.value = true
|
||||
|
||||
const dirContent = getDirContent(dir, dirObject)
|
||||
if (dirContent) {
|
||||
const dirs = filterDirContent(dir, dirContent, 'dir')
|
||||
const images = filterDirContent(dir, dirContent, 'image')
|
||||
if (!dirs.length && !images.length) {
|
||||
await getContentByReposPath(dir)
|
||||
} else {
|
||||
currentPathDirList.value = dirs
|
||||
currentPathImageList.value = images
|
||||
store.commit('REPLACE_IMAGE_CARD', { checkedImgArr: currentPathImageList.value })
|
||||
}
|
||||
} else {
|
||||
await getContentByReposPath(dir)
|
||||
}
|
||||
loadingImageList.value = false
|
||||
}
|
||||
|
||||
async function initDirImageList() {
|
||||
const { selectedDir, dirMode } = userConfigInfo
|
||||
|
||||
if (
|
||||
(dirMode === 'newDir' || dirMode === 'autoDir') &&
|
||||
!getDirContent(selectedDir, dirObject)
|
||||
) {
|
||||
userConfigInfo.selectedDir = '/'
|
||||
userConfigInfo.dirMode = 'rootDir'
|
||||
}
|
||||
|
||||
if (!dirObject.imageList.length && !dirObject.childrenDirs.length) {
|
||||
await getContentByReposPath(userConfigInfo.selectedDir)
|
||||
return
|
||||
}
|
||||
|
||||
await dirContentHandle(userConfigInfo.selectedDir)
|
||||
}
|
||||
|
||||
function toggleListing() {
|
||||
listing.value = !listing.value
|
||||
}
|
||||
|
||||
// 重新加载当前目录内容(网络请求)
|
||||
async function reloadCurrentDirContent() {
|
||||
const { selectedDir } = userConfigInfo
|
||||
await store.dispatch('DIR_IMAGE_LIST_INIT_DIR', selectedDir)
|
||||
loadingImageList.value = true
|
||||
await getContentByReposPath(selectedDir)
|
||||
loadingImageList.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initDirImageList()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => loggingStatus,
|
||||
(nv) => {
|
||||
if (nv === false) {
|
||||
router.push('/config')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => userConfigInfo.selectedDir,
|
||||
async (nDir) => {
|
||||
dirModeHandle(nDir, store)
|
||||
await dirContentHandle(nDir)
|
||||
renderKey.value += 1
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => dirObject,
|
||||
(nv: any) => {
|
||||
const { selectedDir } = userConfigInfo
|
||||
const dirContent = getDirContent(selectedDir, nv)
|
||||
if (dirContent) {
|
||||
currentPathDirList.value = filterDirContent(selectedDir, dirContent, 'dir')
|
||||
currentPathImageList.value = filterDirContent(selectedDir, dirContent, 'image')
|
||||
store.commit('REPLACE_IMAGE_CARD', { checkedImgArr: currentPathImageList.value })
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const isShowBatchTools = ref(false)
|
||||
watch(
|
||||
() => currentPathImageList.value,
|
||||
(nv: UploadedImageModel[]) => {
|
||||
isShowBatchTools.value = nv.filter((x) => x.checked).length > 0
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import 'management.styl';
|
||||
</style>
|
||||
@@ -1,44 +0,0 @@
|
||||
.setting-title {
|
||||
font-size 16rem
|
||||
font-weight bold
|
||||
margin 40rem 0 20rem 0
|
||||
|
||||
&:first-child {
|
||||
margin-top 0
|
||||
}
|
||||
}
|
||||
|
||||
.setting-list {
|
||||
padding 0
|
||||
margin 0
|
||||
|
||||
.setting-item {
|
||||
margin-bottom 10rem
|
||||
|
||||
&.last-child {
|
||||
margin-bottom 0
|
||||
}
|
||||
|
||||
.prefix-input {
|
||||
width calc(100% - 50rem)
|
||||
margin-left 50rem
|
||||
margin-top 15rem
|
||||
}
|
||||
|
||||
.img-encoder-title {
|
||||
margin-bottom 12rem
|
||||
}
|
||||
|
||||
:deep() .el-radio-group {
|
||||
|
||||
display inline-block
|
||||
|
||||
.el-radio {
|
||||
display block
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
<template>
|
||||
<div class="page-container settings-page-container">
|
||||
<div class="setting-title">个性设置:</div>
|
||||
<ul class="setting-list">
|
||||
<li class="setting-item">
|
||||
<el-switch
|
||||
v-model="userSettings.defaultHash"
|
||||
@change="persistUserSettings"
|
||||
active-text="上传时给图片名加上哈希码(确保图片名唯一,强烈建议开启)"
|
||||
></el-switch>
|
||||
</li>
|
||||
<li class="setting-item">
|
||||
<el-switch
|
||||
v-model="userSettings.defaultPrefix"
|
||||
@change="persistUserSettings"
|
||||
active-text="上传时给图片名加上配置的前缀(示例:abc-image.jpg,abc- 为前缀)"
|
||||
></el-switch>
|
||||
<el-input
|
||||
class="prefix-input"
|
||||
v-if="userSettings.defaultPrefix"
|
||||
v-model="userSettings.prefixName"
|
||||
placeholder="请输入命名前缀"
|
||||
@input="persistUserSettings"
|
||||
clearable
|
||||
autofocus
|
||||
></el-input>
|
||||
</li>
|
||||
<li class="setting-item">
|
||||
<el-switch
|
||||
v-model="userSettings.defaultMarkdown"
|
||||
@change="persistUserSettings"
|
||||
active-text="上传成功后复制的图片外链启用 Markdown 格式()"
|
||||
></el-switch>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="setting-title">CDN 提供商:</div>
|
||||
<ul class="setting-list">
|
||||
<li class="setting-item">
|
||||
<el-select
|
||||
v-model="userSettings.externalLinkType"
|
||||
placeholder="选择 CDN 提供商"
|
||||
@change="saveUserSettings"
|
||||
>
|
||||
<el-option label="Staticaly" value="staticaly"></el-option>
|
||||
<el-option label="Cloudflare" value="cloudflare"></el-option>
|
||||
<el-option label="jsDelivr" value="jsdelivr"></el-option>
|
||||
</el-select>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="setting-title">压缩设置:</div>
|
||||
<ul class="setting-list">
|
||||
<li class="setting-item">
|
||||
<el-switch
|
||||
v-model="userSettings.isCompress"
|
||||
@change="persistUserSettings"
|
||||
active-text="是否压缩图片"
|
||||
></el-switch>
|
||||
</li>
|
||||
<li class="setting-item">
|
||||
<div class="img-encoder-title">选择图像编码器(压缩算法):</div>
|
||||
<el-radio-group
|
||||
:disabled="!userSettings.isCompress"
|
||||
v-model="userSettings.compressEncoder"
|
||||
@change="persistUserSettings"
|
||||
>
|
||||
<el-radio :label="compressEncoder.webP">
|
||||
{{ compressEncoder.webP }} (压缩后图片格式为 webp,大多数现代浏览器支持)
|
||||
</el-radio>
|
||||
<el-radio :label="compressEncoder.mozJPEG">
|
||||
{{ compressEncoder.mozJPEG }} (压缩后图片格式为 jpg,兼容性最好)
|
||||
</el-radio>
|
||||
<el-radio :label="compressEncoder.avif">
|
||||
{{ compressEncoder.avif }}
|
||||
(压缩后图片格式为 avif,压缩比最高,目前仅谷歌浏览器支持)
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="setting-title">主题设置:</div>
|
||||
<ul class="setting-list">
|
||||
<li class="setting-item">
|
||||
<el-select
|
||||
v-model="userSettings.themeMode"
|
||||
placeholder="主题模式"
|
||||
@change="saveUserSettings"
|
||||
>
|
||||
<el-option label="自动设置" value="auto"></el-option>
|
||||
<el-option label="暗夜主题" value="dark"></el-option>
|
||||
<el-option label="白昼主题" value="light"></el-option>
|
||||
</el-select>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="setting-title" v-if="userSettings.themeMode === 'auto'">
|
||||
设置白昼模式时间区间:
|
||||
</div>
|
||||
<ul class="setting-list" v-if="userSettings.themeMode === 'auto'">
|
||||
<li class="setting-item">
|
||||
<el-form ref="form">
|
||||
<el-form-item>
|
||||
<el-time-select
|
||||
v-model="userSettings.autoLightThemeTime[0]"
|
||||
start="00:00"
|
||||
step="00:30"
|
||||
end="23:59"
|
||||
@change="saveUserSettings"
|
||||
></el-time-select>
|
||||
<span class="time-middle-space"> ~ </span>
|
||||
<el-time-select
|
||||
v-model="userSettings.autoLightThemeTime[1]"
|
||||
:start="userSettings.autoLightThemeTime[0]"
|
||||
step="00:30"
|
||||
end="23:59"
|
||||
@change="saveUserSettings"
|
||||
></el-time-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { store } from '@/store'
|
||||
import { CompressEncoderMap } from '@/utils/compress'
|
||||
|
||||
const userSettings = computed(() => store.getters.getUserSettings).value
|
||||
|
||||
const persistUserSettings = () => {
|
||||
store.dispatch('USER_SETTINGS_PERSIST')
|
||||
}
|
||||
|
||||
const compressEncoder = CompressEncoderMap
|
||||
|
||||
const saveUserSettings = () => {
|
||||
store.dispatch('SET_USER_SETTINGS', {
|
||||
...userSettings
|
||||
})
|
||||
persistUserSettings()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import "settings.styl"
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user