Compare commits
51 Commits
50a360aca2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 96350bb8e2 | |||
| 28e35ea429 | |||
| a708bbfa72 | |||
| db53baba23 | |||
| d95bdef3f5 | |||
| 1a00811cfc | |||
| 836ccbe4aa | |||
| a2f453a638 | |||
| 7f23d64eb2 | |||
| 475d23f49e | |||
| 85697f35c6 | |||
| de0655adbc | |||
| 8f8e1ed1aa | |||
| 39fcaf35ca | |||
| da723409ca | |||
| 438cb8a1d9 | |||
| 5749dbbcee | |||
| eb9b8f5464 | |||
| b086957452 | |||
| 9e23810ccb | |||
| 964ba2ef8e | |||
| 62e80bfe1e | |||
| cd5a0f44c7 | |||
| ce9d0b64f2 | |||
| 06f39aa01b | |||
| c990a4eb36 | |||
| 7e3d3f4ac0 | |||
| 375ea9a686 | |||
| 9d703212ef | |||
| 8a07bb728e | |||
| b29750b56b | |||
| 909f1075b4 | |||
| b845b5994a | |||
| 64f226c714 | |||
| 5eafb11f0b | |||
| c6af350e4b | |||
| 1477097e17 | |||
| a104d74ae6 | |||
| dedfc9a959 | |||
| 2a7873549d | |||
| 4517a06eff | |||
| a2250d25cf | |||
| 5e73571705 | |||
| 92f32202f1 | |||
| 7517019dde | |||
| 24c1dc6d59 | |||
| 75750180cc | |||
| 735a8e9a51 | |||
| 31b73937fe | |||
| b78f35101b | |||
| 56de8390e5 |
3
.gitignore
vendored
@@ -5,4 +5,5 @@ dist
|
||||
.idea
|
||||
__pycache__
|
||||
*.pyc
|
||||
files
|
||||
files
|
||||
build_info.py
|
||||
674
LICENSE
Normal file
@@ -0,0 +1,674 @@
|
||||
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>.
|
||||
22
build.bat
@@ -1,36 +1,36 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
echo === 尝试激活虚拟环境 ===
|
||||
echo === Activating virtual environment ===
|
||||
if exist ".venv\Scripts\activate.bat" (
|
||||
call .venv\Scripts\activate.bat
|
||||
) else (
|
||||
echo 未发现虚拟环境,尝试创建中...
|
||||
echo Virtual environment not found. Creating...
|
||||
python -m venv .venv
|
||||
if errorlevel 1 (
|
||||
echo [错误] 创建虚拟环境失败,请确认是否已安装 Python。
|
||||
echo [ERROR] Failed to create virtual environment. Please ensure Python is installed.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo 虚拟环境创建成功,开始激活...
|
||||
echo Virtual environment created. Activating...
|
||||
call .venv\Scripts\activate.bat
|
||||
)
|
||||
|
||||
echo === 安装依赖项 ===
|
||||
pip install -r requirements.txt
|
||||
echo === Installing dependencies ===
|
||||
pip install -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple -r requirements.txt
|
||||
if errorlevel 1 (
|
||||
echo [错误] pip 安装依赖失败!
|
||||
echo [ERROR] pip failed to install dependencies.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo === 使用 pyinstaller 构建 ===
|
||||
echo === Building with pyinstaller ===
|
||||
python .\utils\hook.py
|
||||
pyinstaller .\main.spec
|
||||
if errorlevel 1 (
|
||||
echo [错误] 构建失败!
|
||||
echo [ERROR] Build failed.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo === 构建完成 ===
|
||||
pause
|
||||
echo === Build completed ===
|
||||
|
||||
352
images/3rd/matplotlib.svg
Normal file
@@ -0,0 +1,352 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="167.76pt" height="167.76pt" viewBox="0 0 167.76 167.76" xmlns="http://www.w3.org/2000/svg" version="1.1">
|
||||
<metadata>
|
||||
<rdf:RDF xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<cc:Work>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<dc:date>2022-09-27T22:26:51.030457</dc:date>
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:creator>
|
||||
<cc:Agent>
|
||||
<dc:title>Matplotlib v3.6.0, https://matplotlib.org/</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:creator>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs>
|
||||
<style type="text/css">*{stroke-linejoin: round; stroke-linecap: butt}</style>
|
||||
</defs>
|
||||
<g id="figure_1">
|
||||
<g id="patch_1">
|
||||
<path d="M 0 167.76
|
||||
L 167.76 167.76
|
||||
L 167.76 0
|
||||
L 0 0
|
||||
L 0 167.76
|
||||
z
|
||||
" style="fill: none; opacity: 0"/>
|
||||
</g>
|
||||
<g id="axes_1">
|
||||
<g id="patch_2">
|
||||
<path d="M 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
L 167.808464 83.88
|
||||
C 167.808464 94.901385 165.637491 105.815601 161.41979 115.998033
|
||||
C 157.202089 126.180465 151.019682 135.43309 143.226386 143.226386
|
||||
C 135.43309 151.019682 126.180465 157.202089 115.998033 161.41979
|
||||
C 105.815601 165.637491 94.901385 167.808464 83.88 167.808464
|
||||
C 72.858615 167.808464 61.944399 165.637491 51.761967 161.41979
|
||||
C 41.579535 157.202089 32.32691 151.019682 24.533614 143.226386
|
||||
C 16.740318 135.43309 10.557911 126.180465 6.34021 115.998033
|
||||
C 2.122509 105.815601 -0.048464 94.901385 -0.048464 83.88
|
||||
C -0.048464 72.858615 2.122509 61.944399 6.34021 51.761967
|
||||
C 10.557911 41.579535 16.740318 32.32691 24.533614 24.533614
|
||||
C 32.32691 16.740318 41.579535 10.557911 51.761967 6.34021
|
||||
C 61.944399 2.122509 72.858615 -0.048464 83.88 -0.048464
|
||||
C 94.901385 -0.048464 105.815601 2.122509 115.998033 6.34021
|
||||
C 126.180465 10.557911 135.43309 16.740318 143.226386 24.533614
|
||||
C 151.019682 32.32691 157.202089 41.579535 161.41979 51.761967
|
||||
C 165.637491 61.944399 167.808464 72.858615 167.808464 83.88
|
||||
z
|
||||
" style="fill: #ffffff; fill-opacity: 0.9"/>
|
||||
</g>
|
||||
<g id="matplotlib.axis_1">
|
||||
<g id="xtick_1">
|
||||
<g id="line2d_1">
|
||||
<path d="M 83.88 83.88
|
||||
L 162.7272 83.88
|
||||
" clip-path="url(#pccb0f66489)" style="fill: none; stroke: #e6e6e6; stroke-linecap: square"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="xtick_2">
|
||||
<g id="line2d_2">
|
||||
<path d="M 83.88 83.88
|
||||
L 139.63339 28.12661
|
||||
" clip-path="url(#pccb0f66489)" style="fill: none; stroke: #e6e6e6; stroke-linecap: square"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="xtick_3">
|
||||
<g id="line2d_3">
|
||||
<path d="M 83.88 83.88
|
||||
L 83.88 5.0328
|
||||
" clip-path="url(#pccb0f66489)" style="fill: none; stroke: #e6e6e6; stroke-linecap: square"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="xtick_4">
|
||||
<g id="line2d_4">
|
||||
<path d="M 83.88 83.88
|
||||
L 28.12661 28.12661
|
||||
" clip-path="url(#pccb0f66489)" style="fill: none; stroke: #e6e6e6; stroke-linecap: square"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="xtick_5">
|
||||
<g id="line2d_5">
|
||||
<path d="M 83.88 83.88
|
||||
L 5.0328 83.88
|
||||
" clip-path="url(#pccb0f66489)" style="fill: none; stroke: #e6e6e6; stroke-linecap: square"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="xtick_6">
|
||||
<g id="line2d_6">
|
||||
<path d="M 83.88 83.88
|
||||
L 28.12661 139.63339
|
||||
" clip-path="url(#pccb0f66489)" style="fill: none; stroke: #e6e6e6; stroke-linecap: square"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="xtick_7">
|
||||
<g id="line2d_7">
|
||||
<path d="M 83.88 83.88
|
||||
L 83.88 162.7272
|
||||
" clip-path="url(#pccb0f66489)" style="fill: none; stroke: #e6e6e6; stroke-linecap: square"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="xtick_8">
|
||||
<g id="line2d_8">
|
||||
<path d="M 83.88 83.88
|
||||
L 139.63339 139.63339
|
||||
" clip-path="url(#pccb0f66489)" style="fill: none; stroke: #e6e6e6; stroke-linecap: square"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="matplotlib.axis_2">
|
||||
<g id="ytick_1">
|
||||
<g id="line2d_9">
|
||||
<path d="M 92.6408 83.88
|
||||
C 92.6408 82.729542 92.414185 81.590271 91.973924 80.527387
|
||||
C 91.533663 79.464503 90.888318 78.498675 90.074821 77.685179
|
||||
C 89.261325 76.871682 88.295497 76.226337 87.232613 75.786076
|
||||
C 86.169729 75.345815 85.030458 75.1192 83.88 75.1192
|
||||
C 82.729542 75.1192 81.590271 75.345815 80.527387 75.786076
|
||||
C 79.464503 76.226337 78.498675 76.871682 77.685179 77.685179
|
||||
C 76.871682 78.498675 76.226337 79.464503 75.786076 80.527387
|
||||
C 75.345815 81.590271 75.1192 82.729542 75.1192 83.88
|
||||
C 75.1192 85.030458 75.345815 86.169729 75.786076 87.232613
|
||||
C 76.226337 88.295497 76.871682 89.261325 77.685179 90.074821
|
||||
C 78.498675 90.888318 79.464503 91.533663 80.527387 91.973924
|
||||
C 81.590271 92.414185 82.729542 92.6408 83.88 92.6408
|
||||
C 85.030458 92.6408 86.169729 92.414185 87.232613 91.973924
|
||||
C 88.295497 91.533663 89.261325 90.888318 90.074821 90.074821
|
||||
C 90.888318 89.261325 91.533663 88.295497 91.973924 87.232613
|
||||
C 92.414185 86.169729 92.6408 85.030458 92.6408 83.88
|
||||
" clip-path="url(#pccb0f66489)" style="fill: none; stroke: #e6e6e6; stroke-linecap: square"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="ytick_2">
|
||||
<g id="line2d_10">
|
||||
<path d="M 110.1624 83.88
|
||||
C 110.1624 80.428627 109.482555 77.010814 108.161771 73.822161
|
||||
C 106.840988 70.633508 104.904953 67.736026 102.464463 65.295537
|
||||
C 100.023974 62.855047 97.126492 60.919012 93.937839 59.598229
|
||||
C 90.749186 58.277445 87.331373 57.5976 83.88 57.5976
|
||||
C 80.428627 57.5976 77.010814 58.277445 73.822161 59.598229
|
||||
C 70.633508 60.919012 67.736026 62.855047 65.295537 65.295537
|
||||
C 62.855047 67.736026 60.919012 70.633508 59.598229 73.822161
|
||||
C 58.277445 77.010814 57.5976 80.428627 57.5976 83.88
|
||||
C 57.5976 87.331373 58.277445 90.749186 59.598229 93.937839
|
||||
C 60.919012 97.126492 62.855047 100.023974 65.295537 102.464463
|
||||
C 67.736026 104.904953 70.633508 106.840988 73.822161 108.161771
|
||||
C 77.010814 109.482555 80.428627 110.1624 83.88 110.1624
|
||||
C 87.331373 110.1624 90.749186 109.482555 93.937839 108.161771
|
||||
C 97.126492 106.840988 100.023974 104.904953 102.464463 102.464463
|
||||
C 104.904953 100.023974 106.840988 97.126492 108.161771 93.937839
|
||||
C 109.482555 90.749186 110.1624 87.331373 110.1624 83.88
|
||||
" clip-path="url(#pccb0f66489)" style="fill: none; stroke: #e6e6e6; stroke-linecap: square"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="ytick_3">
|
||||
<g id="line2d_11">
|
||||
<path d="M 127.684 83.88
|
||||
C 127.684 78.127711 126.550925 72.431357 124.349619 67.116935
|
||||
C 122.148314 61.802513 118.921588 56.973377 114.854105 52.905895
|
||||
C 110.786623 48.838412 105.957487 45.611686 100.643065 43.410381
|
||||
C 95.328643 41.209075 89.632289 40.076 83.88 40.076
|
||||
C 78.127711 40.076 72.431357 41.209075 67.116935 43.410381
|
||||
C 61.802513 45.611686 56.973377 48.838412 52.905895 52.905895
|
||||
C 48.838412 56.973377 45.611686 61.802513 43.410381 67.116935
|
||||
C 41.209075 72.431357 40.076 78.127711 40.076 83.88
|
||||
C 40.076 89.632289 41.209075 95.328643 43.410381 100.643065
|
||||
C 45.611686 105.957487 48.838412 110.786623 52.905895 114.854105
|
||||
C 56.973377 118.921588 61.802513 122.148314 67.116935 124.349619
|
||||
C 72.431357 126.550925 78.127711 127.684 83.88 127.684
|
||||
C 89.632289 127.684 95.328643 126.550925 100.643065 124.349619
|
||||
C 105.957487 122.148314 110.786623 118.921588 114.854105 114.854105
|
||||
C 118.921588 110.786623 122.148314 105.957487 124.349619 100.643065
|
||||
C 126.550925 95.328643 127.684 89.632289 127.684 83.88
|
||||
" clip-path="url(#pccb0f66489)" style="fill: none; stroke: #e6e6e6; stroke-linecap: square"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="ytick_4">
|
||||
<g id="line2d_12">
|
||||
<path d="M 145.2056 83.88
|
||||
C 145.2056 75.826796 143.619294 67.851899 140.537467 60.411709
|
||||
C 137.455639 52.971519 132.938223 46.210728 127.243748 40.516252
|
||||
C 121.549272 34.821777 114.788481 30.304361 107.348291 27.222533
|
||||
C 99.908101 24.140706 91.933204 22.5544 83.88 22.5544
|
||||
C 75.826796 22.5544 67.851899 24.140706 60.411709 27.222533
|
||||
C 52.971519 30.304361 46.210728 34.821777 40.516252 40.516252
|
||||
C 34.821777 46.210728 30.304361 52.971519 27.222533 60.411709
|
||||
C 24.140706 67.851899 22.5544 75.826796 22.5544 83.88
|
||||
C 22.5544 91.933204 24.140706 99.908101 27.222533 107.348291
|
||||
C 30.304361 114.788481 34.821777 121.549272 40.516252 127.243748
|
||||
C 46.210728 132.938223 52.971519 137.455639 60.411709 140.537467
|
||||
C 67.851899 143.619294 75.826796 145.2056 83.88 145.2056
|
||||
C 91.933204 145.2056 99.908101 143.619294 107.348291 140.537467
|
||||
C 114.788481 137.455639 121.549272 132.938223 127.243748 127.243748
|
||||
C 132.938223 121.549272 137.455639 114.788481 140.537467 107.348291
|
||||
C 143.619294 99.908101 145.2056 91.933204 145.2056 83.88
|
||||
" clip-path="url(#pccb0f66489)" style="fill: none; stroke: #e6e6e6; stroke-linecap: square"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="patch_3">
|
||||
<path d="M 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
L 100.544032 78.465528
|
||||
C 100.827679 79.338503 101.042289 80.232419 101.18588 81.139018
|
||||
C 101.329471 82.045617 101.4016 82.9621 101.4016 83.88
|
||||
z
|
||||
" clip-path="url(#pccb0f66489)" style="fill: #004cff; fill-opacity: 0.6; stroke: #4c4c4c; stroke-width: 1.5; stroke-linejoin: miter"/>
|
||||
</g>
|
||||
<g id="patch_4">
|
||||
<path d="M 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
L 102.349947 34.667001
|
||||
C 104.928058 35.634582 107.426154 36.803257 109.821161 38.162231
|
||||
C 112.216167 39.521205 114.500687 41.06628 116.653617 42.783184
|
||||
z
|
||||
" clip-path="url(#pccb0f66489)" style="fill: #ceff29; fill-opacity: 0.6; stroke: #4c4c4c; stroke-width: 1.5; stroke-linejoin: miter"/>
|
||||
</g>
|
||||
<g id="patch_5">
|
||||
<path d="M 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
L 38.963335 30.078544
|
||||
C 43.193773 26.546722 47.826049 23.526156 52.764087 21.079495
|
||||
C 57.702125 18.632834 62.911548 16.77711 68.284309 15.550812
|
||||
z
|
||||
" clip-path="url(#pccb0f66489)" style="fill: #ff6800; fill-opacity: 0.6; stroke: #4c4c4c; stroke-width: 1.5; stroke-linejoin: miter"/>
|
||||
</g>
|
||||
<g id="patch_6">
|
||||
<path d="M 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
L 23.539928 94.830109
|
||||
C 22.390882 88.498347 22.245596 82.025002 23.109411 75.648064
|
||||
C 23.973226 69.271127 25.835425 63.069714 28.627544 57.271819
|
||||
z
|
||||
" clip-path="url(#pccb0f66489)" style="fill: #ffc400; fill-opacity: 0.6; stroke: #4c4c4c; stroke-width: 1.5; stroke-linejoin: miter"/>
|
||||
</g>
|
||||
<g id="patch_7">
|
||||
<path d="M 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
L 55.074417 103.836559
|
||||
C 54.551888 103.082334 54.059183 102.307877 53.597441 101.51498
|
||||
C 53.135699 100.722082 52.705276 99.911356 52.307168 99.084675
|
||||
z
|
||||
" clip-path="url(#pccb0f66489)" style="fill: #29ffce; fill-opacity: 0.6; stroke: #4c4c4c; stroke-width: 1.5; stroke-linejoin: miter"/>
|
||||
</g>
|
||||
<g id="patch_8">
|
||||
<path d="M 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
L 91.217443 127.065094
|
||||
C 88.388717 127.545714 85.519599 127.747241 82.651462 127.666769
|
||||
C 79.783325 127.586296 76.93002 127.224214 74.132693 126.585742
|
||||
z
|
||||
" clip-path="url(#pccb0f66489)" style="fill: #7dff7a; fill-opacity: 0.6; stroke: #4c4c4c; stroke-width: 1.5; stroke-linejoin: miter"/>
|
||||
</g>
|
||||
<g id="patch_9">
|
||||
<path d="M 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
L 139.162587 126.960611
|
||||
C 137.470326 129.132181 135.651582 131.202154 133.71581 133.159767
|
||||
C 131.780039 135.117381 129.730602 136.959235 127.578156 138.675754
|
||||
z
|
||||
" clip-path="url(#pccb0f66489)" style="fill: #ff6800; fill-opacity: 0.6; stroke: #4c4c4c; stroke-width: 1.5; stroke-linejoin: miter"/>
|
||||
</g>
|
||||
<g id="patch_10">
|
||||
<path d="M 162.7272 83.88
|
||||
C 162.7272 73.525881 160.687664 63.272442 156.725314 53.706483
|
||||
C 152.762964 44.140524 146.954858 35.448078 139.63339 28.12661
|
||||
C 132.311922 20.805142 123.619476 14.997036 114.053517 11.034686
|
||||
C 104.487558 7.072336 94.234119 5.0328 83.88 5.0328
|
||||
C 73.525881 5.0328 63.272442 7.072336 53.706483 11.034686
|
||||
C 44.140524 14.997036 35.448078 20.805142 28.12661 28.12661
|
||||
C 20.805142 35.448078 14.997036 44.140524 11.034686 53.706483
|
||||
C 7.072336 63.272442 5.0328 73.525881 5.0328 83.88
|
||||
C 5.0328 94.234119 7.072336 104.487558 11.034686 114.053517
|
||||
C 14.997036 123.619476 20.805142 132.311922 28.12661 139.63339
|
||||
C 35.448078 146.954858 44.140524 152.762964 53.706483 156.725314
|
||||
C 63.272442 160.687664 73.525881 162.7272 83.88 162.7272
|
||||
C 94.234119 162.7272 104.487558 160.687664 114.053517 156.725314
|
||||
C 123.619476 152.762964 132.311922 146.954858 139.63339 139.63339
|
||||
C 146.954858 132.311922 152.762964 123.619476 156.725314 114.053517
|
||||
C 160.687664 104.487558 162.7272 94.234119 162.7272 83.88
|
||||
" style="fill: none; stroke: #11557c; stroke-width: 4; stroke-linejoin: miter; stroke-linecap: square"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="pccb0f66489">
|
||||
<path d="M 162.7272 83.88
|
||||
C 162.7272 73.525881 160.687664 63.272442 156.725314 53.706483
|
||||
C 152.762964 44.140524 146.954858 35.448078 139.63339 28.12661
|
||||
C 132.311922 20.805142 123.619476 14.997036 114.053517 11.034686
|
||||
C 104.487558 7.072336 94.234119 5.0328 83.88 5.0328
|
||||
C 73.525881 5.0328 63.272442 7.072336 53.706483 11.034686
|
||||
C 44.140524 14.997036 35.448078 20.805142 28.12661 28.12661
|
||||
C 20.805142 35.448078 14.997036 44.140524 11.034686 53.706483
|
||||
C 7.072336 63.272442 5.0328 73.525881 5.0328 83.88
|
||||
C 5.0328 94.234119 7.072336 104.487558 11.034686 114.053517
|
||||
C 14.997036 123.619476 20.805142 132.311922 28.12661 139.63339
|
||||
C 35.448078 146.954858 44.140524 152.762964 53.706483 156.725314
|
||||
C 63.272442 160.687664 73.525881 162.7272 83.88 162.7272
|
||||
C 94.234119 162.7272 104.487558 160.687664 114.053517 156.725314
|
||||
C 123.619476 152.762964 132.311922 146.954858 139.63339 139.63339
|
||||
C 146.954858 132.311922 152.762964 123.619476 156.725314 114.053517
|
||||
C 160.687664 104.487558 162.7272 94.234119 162.7272 83.88
|
||||
M 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
C 83.88 83.88 83.88 83.88 83.88 83.88
|
||||
M 162.7272 83.88
|
||||
z
|
||||
"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 15 KiB |
BIN
images/3rd/packaging.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
images/3rd/qt.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
images/gplv3.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
images/logo.png
|
Before Width: | Height: | Size: 206 KiB After Width: | Height: | Size: 210 KiB |
|
Before Width: | Height: | Size: 432 KiB After Width: | Height: | Size: 304 KiB |
31
main.py
@@ -1,20 +1,33 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
import sys
|
||||
import module.resources
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from ui.main import MainWindow
|
||||
import module.resources
|
||||
from utils.function import RELEASE_ENV
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = QApplication(sys.argv)
|
||||
window = MainWindow()
|
||||
try:
|
||||
if RELEASE_ENV:
|
||||
import pyi_splash
|
||||
|
||||
pyi_splash.close()
|
||||
except ImportError:
|
||||
pass
|
||||
finally:
|
||||
window.show()
|
||||
pyi_splash.update_text('正在启动...')
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
|
||||
@@ -20,7 +20,7 @@ splash = Splash(
|
||||
'images\\splash.png',
|
||||
binaries=a.binaries,
|
||||
datas=a.datas,
|
||||
text_pos=(5,378),
|
||||
text_pos=(35, 378),
|
||||
text_size=12,
|
||||
text_color='black',
|
||||
minify_script=True,
|
||||
@@ -47,6 +47,6 @@ exe = EXE(
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
name='建工工具箱',
|
||||
name='教学工具箱',
|
||||
icon=['images\\logo.png'],
|
||||
)
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
VER_NUM = '1.3.14'
|
||||
VERSION = f'Release {VER_NUM}'
|
||||
COMPATIBLE_VERSION = ['7.3', '7.4', '7.6', '7.7', '8.0']
|
||||
|
||||
23
module/about/schema.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
class ThirdParty:
|
||||
def __init__(self, name: str, url: str, qrc: str = None):
|
||||
self.name = name
|
||||
self.url = url
|
||||
self.qrc = qrc
|
||||
|
||||
def __repr__(self):
|
||||
return f"ThirdParty(name={self.name}, url={self.url})"
|
||||
16
module/achievement/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
# Copyright (c) 2025-2026 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
import io
|
||||
import os
|
||||
import traceback
|
||||
@@ -13,7 +28,7 @@ from docx.shared import Pt, Cm
|
||||
from docx.text.run import Run
|
||||
|
||||
from module import LOGLEVEL
|
||||
from module.achievement_excel import ExcelReader
|
||||
from module.achievement.excel import ExcelReader
|
||||
|
||||
|
||||
class DocxWriter:
|
||||
@@ -140,10 +155,15 @@ class DocxWriter:
|
||||
for performance in self.excel_reader.achievement_level[r_index]:
|
||||
non_none_count = 3 - performance.scores.count(None)
|
||||
if non_none_count > 1:
|
||||
cell_start = table.cell(row, col_span)
|
||||
cell_end = table.cell(row, col_span + non_none_count - 1)
|
||||
cell_start.merge(cell_end)
|
||||
col_span += non_none_count
|
||||
try:
|
||||
cell_start = table.cell(row, col_span)
|
||||
cell_end = table.cell(row, col_span + non_none_count - 1)
|
||||
cell_start.merge(cell_end)
|
||||
except IndexError:
|
||||
pass
|
||||
# self.signal(f"单元格合并失败:({row}, {col_span}),需要自行检查表格准确性",
|
||||
# LOGLEVEL.WARNING)
|
||||
col_span += non_none_count
|
||||
|
||||
start = rows - X + 3 + self.excel_reader.kpi_number
|
||||
if len(self.excel_reader.class_list) == 1:
|
||||
@@ -224,8 +244,8 @@ class DocxWriter:
|
||||
f". 课程目标达成情况的合理性评价")
|
||||
self.set_run_font(run, 14, 'Times New Roman', '黑体', True)
|
||||
|
||||
rows = 9
|
||||
cols = 4
|
||||
rows = 11
|
||||
cols = 6
|
||||
table = doc.add_table(rows=rows, cols=cols)
|
||||
# 设置外侧框线粗1.5磅,内侧框线粗0.5磅
|
||||
self.set_table_borders(table)
|
||||
@@ -235,13 +255,25 @@ class DocxWriter:
|
||||
cell_end = table.cell(0, cols - 1)
|
||||
cell_start.merge(cell_end)
|
||||
# 合并第二行至最后
|
||||
for i in range(1, 9):
|
||||
if i == 2:
|
||||
continue
|
||||
cell_start = table.cell(i, 1)
|
||||
cell_end = table.cell(i, cols - 1)
|
||||
cell_start.merge(cell_end)
|
||||
# 填充数据
|
||||
for i in range(1, rows):
|
||||
match i:
|
||||
case 2:
|
||||
table.cell(i, 2).merge(table.cell(i, 3))
|
||||
table.cell(i, 4).merge(table.cell(i, 5))
|
||||
table.cell(i, 1).width = Cm(7.42)
|
||||
table.cell(i, 2).width = Cm(7.42)
|
||||
table.cell(i, 4).width = Cm(7.41)
|
||||
case 8 | 10:
|
||||
table.cell(i - 1, 0).merge(table.cell(i, 0))
|
||||
table.cell(i, 1).width = Cm(11.23)
|
||||
table.cell(i, 2).width = Cm(1.48)
|
||||
table.cell(i, 3).width = Cm(3.4)
|
||||
table.cell(i, 4).width = Cm(1.39)
|
||||
case _:
|
||||
cell_start = table.cell(i, 1)
|
||||
cell_end = table.cell(i, cols - 1)
|
||||
cell_start.merge(cell_end)
|
||||
# 填充数据
|
||||
self.put_data_to_table(table, self.excel_reader.get_word_template_part_3)
|
||||
|
||||
# 应用样式
|
||||
@@ -261,9 +293,37 @@ class DocxWriter:
|
||||
|
||||
for t_index, table in enumerate(doc.tables):
|
||||
self.set_table_borders(table)
|
||||
# part_3_table_index 表格第9和11行(索引8和10)特殊边框处理
|
||||
if t_index in part_3_table_index:
|
||||
for r_idx in [8, 10]:
|
||||
row = table.rows[r_idx]
|
||||
prev_row = table.rows[r_idx - 1]
|
||||
# 上一行(第8行和第10行,索引7和9)第2-6列移除下边框
|
||||
for c_idx in range(1, 6):
|
||||
self.set_cell_border(prev_row.cells[c_idx], bottom=0)
|
||||
# 第2列(索引1):没有上边框和右边框
|
||||
self.set_cell_border(row.cells[1], top=0, right=0)
|
||||
# 第3-5列(索引2-4):没有上边框和左右边框
|
||||
for c_idx in [2, 3, 4]:
|
||||
self.set_cell_border(row.cells[c_idx], top=0, left=0, right=0)
|
||||
# 第6列(索引5):没有上边框和左边框
|
||||
self.set_cell_border(row.cells[5], top=0, left=0)
|
||||
# 插入签名图片
|
||||
if self.excel_reader.major_director_signature_image is not None:
|
||||
self.insert_pil_image(table.cell(8, 3),
|
||||
self.excel_reader.major_director_signature_image,
|
||||
height=Cm(1.2))
|
||||
if self.excel_reader.course_leader_signature_image is not None:
|
||||
self.insert_pil_image(table.cell(10, 3),
|
||||
self.excel_reader.course_leader_signature_image,
|
||||
height=Cm(1.2))
|
||||
for r_index, row in enumerate(table.rows):
|
||||
row.height_rule = WD_ROW_HEIGHT_RULE.AT_LEAST
|
||||
row.height = Cm(0.7)
|
||||
# part_3_table_index 表格第9和11行(索引8和10)行高为1.2cm
|
||||
if t_index in part_3_table_index and r_index in [8, 10]:
|
||||
row.height = Cm(1.2)
|
||||
else:
|
||||
row.height = Cm(0.7)
|
||||
for c_index, cell in enumerate(row.cells):
|
||||
cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
|
||||
self.set_cell_margins(cell, start=57, end=57)
|
||||
@@ -321,6 +381,8 @@ class DocxWriter:
|
||||
(6, 1),
|
||||
(7, 1),
|
||||
(8, 1),
|
||||
(9, 1),
|
||||
(10, 1),
|
||||
]
|
||||
if r_index == 0:
|
||||
for run in paragraph.runs:
|
||||
@@ -364,6 +426,7 @@ class DocxWriter:
|
||||
"""
|
||||
设置单元格边框
|
||||
kwargs: top, bottom, left, right, inside_h, inside_v
|
||||
值为0时移除边框,值大于0时设置边框粗细
|
||||
"""
|
||||
tc = cell._tc
|
||||
tcPr = tc.get_or_add_tcPr()
|
||||
@@ -376,10 +439,14 @@ class DocxWriter:
|
||||
if value is not None:
|
||||
tag = 'w:{}'.format(key)
|
||||
border = OxmlElement(tag)
|
||||
border.set(qn('w:val'), 'single')
|
||||
border.set(qn('w:sz'), str(int(value * 8)))
|
||||
border.set(qn('w:space'), '0')
|
||||
border.set(qn('w:color'), 'auto')
|
||||
if value == 0:
|
||||
# 移除边框
|
||||
border.set(qn('w:val'), 'nil')
|
||||
else:
|
||||
border.set(qn('w:val'), 'single')
|
||||
border.set(qn('w:sz'), str(int(value * 8)))
|
||||
border.set(qn('w:space'), '0')
|
||||
border.set(qn('w:color'), 'auto')
|
||||
tcBorders.append(border)
|
||||
|
||||
# 将边框添加到单元格属性中
|
||||
@@ -475,6 +542,15 @@ class DocxWriter:
|
||||
run = paragraph.add_run()
|
||||
run.add_picture(image_stream, width=width, height=height)
|
||||
|
||||
def insert_pil_image(self, cell, pil_image, width=None, height=Cm(4.5)):
|
||||
"""插入PIL Image对象到单元格"""
|
||||
image_stream = io.BytesIO()
|
||||
pil_image.save(image_stream, format='PNG')
|
||||
image_stream.seek(0)
|
||||
paragraph = cell.paragraphs[0]
|
||||
run = paragraph.add_run()
|
||||
run.add_picture(image_stream, width=width, height=height)
|
||||
|
||||
def is_chinese(self, char):
|
||||
"""判断字符是否为中文"""
|
||||
if '\u4e00' <= char <= '\u9fff':
|
||||
@@ -1,5 +1,21 @@
|
||||
# Copyright (c) 2025-2026 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
import datetime
|
||||
import traceback
|
||||
import io
|
||||
from typing import Optional, Callable
|
||||
|
||||
import openpyxl
|
||||
@@ -7,6 +23,7 @@ from openpyxl.utils import get_column_letter, column_index_from_string
|
||||
from openpyxl.workbook.workbook import Workbook
|
||||
from openpyxl.worksheet.worksheet import Worksheet
|
||||
from packaging import version
|
||||
from PIL import Image
|
||||
|
||||
from module import LOGLEVEL, COMPATIBLE_VERSION
|
||||
from module.schema import Performance
|
||||
@@ -33,6 +50,35 @@ class ExcelReader:
|
||||
question_data: dict[str, list[tuple[str, int]]]
|
||||
ignore_version_check: bool
|
||||
pic_list: list
|
||||
suggestion_template_list: list[Optional[str]]
|
||||
major_director_signature_image: Optional[Image.Image]
|
||||
course_leader_signature_image: Optional[Image.Image]
|
||||
|
||||
class _SheetImageLoader:
|
||||
"""Lightweight image loader scoped for ExcelReader use."""
|
||||
|
||||
def __init__(self, sheet: Worksheet):
|
||||
self._images: dict[str, Callable[[], bytes]] = {}
|
||||
for image in getattr(sheet, "_images", []):
|
||||
row = image.anchor._from.row + 1
|
||||
col = get_column_letter(image.anchor._from.col + 1)
|
||||
self._images[f"{col}{row}"] = image._data
|
||||
|
||||
def image_in(self, cell: str) -> bool:
|
||||
return cell in self._images
|
||||
|
||||
def get(self, cell: str) -> Image.Image:
|
||||
if cell not in self._images:
|
||||
raise ValueError(f"Cell {cell} doesn't contain an image")
|
||||
image = io.BytesIO(self._images[cell]())
|
||||
return Image.open(image)
|
||||
|
||||
class ValidError(Exception):
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
|
||||
def __str__(self):
|
||||
return self.message
|
||||
|
||||
def __init__(self, file_path: str, version_check: bool = False,
|
||||
signal: Callable[[str, str], None] = lambda x, y: print(x)):
|
||||
@@ -58,6 +104,9 @@ class ExcelReader:
|
||||
self.ignore_version_check = version_check
|
||||
self.pic_list = []
|
||||
self.signal = signal
|
||||
self.suggestion_template_list = []
|
||||
self.major_director_signature_image = None
|
||||
self.course_leader_signature_image = None
|
||||
|
||||
def parse_excel(self):
|
||||
try:
|
||||
@@ -65,6 +114,7 @@ class ExcelReader:
|
||||
sheet: Worksheet = wb['初始录入']
|
||||
# 读取版本号
|
||||
e_version = sheet['V4'].value if sheet['V4'].value is not None else sheet['U4'].value
|
||||
e_version = sheet['H1'].value if e_version is None else e_version
|
||||
if e_version is None:
|
||||
e_version = "0"
|
||||
status, _ = check_version(e_version, COMPATIBLE_VERSION)
|
||||
@@ -86,6 +136,13 @@ class ExcelReader:
|
||||
# 读取课程负责人
|
||||
self.course_lead_teacher_name = sheet["D8"].value
|
||||
|
||||
need_signature_images = CUR_VERSION >= version.parse("9.4") and sheet["H10"].value == "是"
|
||||
if need_signature_images:
|
||||
self._load_signature_images()
|
||||
else:
|
||||
self.major_director_signature_image = None
|
||||
self.course_leader_signature_image = None
|
||||
|
||||
# 读取班级和人数
|
||||
max_class_size = 4
|
||||
match CUR_VERSION:
|
||||
@@ -210,9 +267,26 @@ class ExcelReader:
|
||||
else:
|
||||
self.question_data[key] = [values]
|
||||
|
||||
self.validate_data()
|
||||
# 读取建议模板
|
||||
if CUR_VERSION >= version.parse("9.0"):
|
||||
sheet = wb['初始录入']
|
||||
|
||||
for i in range(29, 34):
|
||||
self.suggestion_template_list.append(sheet[f'I{i}'].value)
|
||||
|
||||
if len(self.suggestion_template_list) != 5:
|
||||
for i in range(len(self.suggestion_template_list), 5):
|
||||
self.suggestion_template_list.append(None)
|
||||
|
||||
if vd_lst := self.validate_data():
|
||||
raise self.ValidError("\n\n".join(vd_lst))
|
||||
|
||||
self.gen_picture()
|
||||
|
||||
except self.ValidError as ve:
|
||||
raise Exception(f"""
|
||||
数据验证失败:\n\n{str(ve)}
|
||||
""")
|
||||
except Exception as e:
|
||||
error_message = traceback.format_exc()
|
||||
raise Exception(f"""
|
||||
@@ -223,13 +297,40 @@ class ExcelReader:
|
||||
def set_file_path(self, file_path: str):
|
||||
self.file_path = file_path
|
||||
|
||||
def validate_data(self):
|
||||
def validate_data(self) -> list[str]:
|
||||
lst: list[str] = []
|
||||
self.signal("正在验证数据", LOGLEVEL.INFO)
|
||||
return 0
|
||||
if len(self.kpi_list) != self.kpi_number:
|
||||
self.signal("\"课程目标\"或\"目标支撑的毕业要求指标点\"数量与期望目标数量不符", LOGLEVEL.ERROR)
|
||||
lst.append(
|
||||
f"\"课程目标\"或\"目标支撑的毕业要求指标点\"数量与期望目标数量不符,请检查Excel表格中的\"课程目标\"和\"目标支撑的毕业要求指标点\"列是否填写完整。"
|
||||
f"期望得到 {self.kpi_number} 个,实际检测到 {len(self.kpi_list)} 个。"
|
||||
f"(如想暂时不填,请在Excel表格对应的位置添加一个空格)"
|
||||
)
|
||||
|
||||
return lst
|
||||
|
||||
def run(self):
|
||||
self.parse_excel()
|
||||
|
||||
def _load_signature_images(self):
|
||||
signature_cells = {
|
||||
"major_director_signature_image": ("I34", "K34"),
|
||||
"course_leader_signature_image": ("I35", "K35"),
|
||||
}
|
||||
wb_with_images: Workbook = openpyxl.load_workbook(self.file_path, data_only=True)
|
||||
try:
|
||||
sheet_with_images: Worksheet = wb_with_images["初始录入"]
|
||||
loader = self._SheetImageLoader(sheet_with_images)
|
||||
|
||||
for attr, (check_cell, image_cell) in signature_cells.items():
|
||||
if sheet_with_images[check_cell].value and loader.image_in(image_cell):
|
||||
setattr(self, attr, loader.get(image_cell))
|
||||
else:
|
||||
setattr(self, attr, None)
|
||||
finally:
|
||||
wb_with_images.close()
|
||||
|
||||
def clear_all_data(self):
|
||||
self.kpi_list = []
|
||||
self.kpi_number = 0
|
||||
@@ -249,6 +350,8 @@ class ExcelReader:
|
||||
self.hml_list = []
|
||||
self.question_data = {}
|
||||
self.pic_list = []
|
||||
self.major_director_signature_image = None
|
||||
self.course_leader_signature_image = None
|
||||
|
||||
def set_version_check(self, version_check: bool):
|
||||
self.ignore_version_check = version_check
|
||||
@@ -285,9 +388,14 @@ class ExcelReader:
|
||||
continue
|
||||
if index == 2:
|
||||
if len(self.n_evaluation_methods) == 6:
|
||||
yield f"期末考核\n({self.n_evaluation_methods[5]})"
|
||||
if self.n_evaluation_methods[5] == "试卷":
|
||||
yield "期末考核\n(试卷)"
|
||||
else:
|
||||
yield "期末考核"
|
||||
elif j[0] == "试卷":
|
||||
yield "期末考核\n(试卷)"
|
||||
else:
|
||||
yield f"期末考核\n({j[0]})"
|
||||
yield "期末考核"
|
||||
else:
|
||||
yield f"{j[0]}考核"
|
||||
|
||||
@@ -317,8 +425,9 @@ class ExcelReader:
|
||||
case 1:
|
||||
yield "\n".join([x for x in self.n_evaluation_methods[3:5] if x is not None])
|
||||
case 2:
|
||||
if (len(self.n_evaluation_methods) == 6 and self.n_evaluation_methods[5] != "试卷" or
|
||||
len(self.n_evaluation_methods) == 5 and self.evaluation_stage[2][0] != "试卷"):
|
||||
if len(self.n_evaluation_methods) == 6 and self.n_evaluation_methods[5] != "试卷":
|
||||
yield self.n_evaluation_methods[5]
|
||||
elif len(self.n_evaluation_methods) == 5 and self.evaluation_stage[2][0] != "试卷":
|
||||
yield self.evaluation_stage[2][0]
|
||||
else:
|
||||
# 中文数字到数字的映射
|
||||
@@ -450,8 +559,8 @@ class ExcelReader:
|
||||
yield analysis_results
|
||||
yield "改进措施"
|
||||
yield ("注:改进措施,包括课时分配、教材选用、教学方式、教学方法、教学内容、评分标准、过程评价及帮扶\n"
|
||||
"\n\n\n在这填入您的改进措施\n\n\n")
|
||||
for i in range(88888):
|
||||
f"{self.suggestion_template_list[0] if self.suggestion_template_list[0] is not None else '\n\n\n在这填入您的改进措施\n\n\n'}")
|
||||
while True:
|
||||
yield "如果您看到了本段文字,请联系开发者"
|
||||
|
||||
def get_word_template_part_2(self):
|
||||
@@ -540,20 +649,21 @@ class ExcelReader:
|
||||
f"达成值为{self.achievement_level[i][min_p_rate_index].achievement},"
|
||||
f"{'、'.join(o_c_str)}的达成情况较好;")
|
||||
analysis_results = analysis_results[:-1] + "。"
|
||||
analysis_results += "\n3.结果分析: \n在此填写您的结果分析\n\n"
|
||||
analysis_results += ("\n3.结果分析: \n"
|
||||
f"{self.suggestion_template_list[2] if self.suggestion_template_list[2] is not None else '\n\n\n在此填写您的结果分析\n\n\n'}")
|
||||
yield analysis_results
|
||||
yield "改进措施"
|
||||
yield "注:改进措施,包括课时分配、教材选用、教学方式、教学方法、教学内容、评分标准、过程评价及帮扶\n\n\n\n"
|
||||
for i in range(88888):
|
||||
yield ("注:改进措施,包括课时分配、教材选用、教学方式、教学方法、教学内容、评分标准、过程评价及帮扶\n"
|
||||
f"{self.suggestion_template_list[1] if self.suggestion_template_list[1] is not None else '\n\n\n在这填入您的改进措施\n\n\n'}")
|
||||
while True:
|
||||
yield "如果您看到了本段文字,请联系开发者"
|
||||
|
||||
@staticmethod
|
||||
def get_word_template_part_3():
|
||||
def get_word_template_part_3(self):
|
||||
yield "课程目标达成情况合理性评价"
|
||||
yield "评价样本的合理性"
|
||||
yield "R全体样本 £抽样样本"
|
||||
yield "评价依据的合理性"
|
||||
yield "考核方法 R合适 £不合适 "
|
||||
yield "考核方法 R合适 £不合适"
|
||||
yield "考核内容是否支撑课程目标 R是 £否"
|
||||
yield "评分标准 R明确 £不明确"
|
||||
yield "计算过程的合理性"
|
||||
@@ -566,17 +676,23 @@ class ExcelReader:
|
||||
yield "R合理 £基本合理 £不合理"
|
||||
yield "专业负责人/系主任(签字)"
|
||||
yield ("整改意见:\n"
|
||||
"\n\n\n\n\n"
|
||||
"\n\n\n"
|
||||
"\n\t\t\t\t\t\t\t\t\t签字:\t\t\t日期:{}\n".
|
||||
format(datetime.datetime.now().strftime("%Y-%m-%d")))
|
||||
f"{" " * 8}{self.suggestion_template_list[3] if self.suggestion_template_list[3] is not None else '\n\n\n'}\n\n\n")
|
||||
yield ""
|
||||
yield "签字:"
|
||||
yield ""
|
||||
yield "日期:"
|
||||
yield datetime.datetime.now().strftime("%Y-%m-%d")
|
||||
yield "课程负责人(签字)"
|
||||
yield ("拟整改计划与措施:\n"
|
||||
"\n\n\n\n\n"
|
||||
"\n\n\n"
|
||||
"\n\t\t\t\t\t\t\t\t\t签字:\t\t\t日期:{}\n".
|
||||
format(datetime.datetime.now().strftime("%Y-%m-%d")))
|
||||
for i in range(88888):
|
||||
f"{" " * 8}{self.suggestion_template_list[4] if self.suggestion_template_list[4] is not None else '\n\n\n'}\n\n\n")
|
||||
yield ""
|
||||
yield "签字:"
|
||||
yield ""
|
||||
yield "日期:"
|
||||
yield datetime.datetime.now().strftime("%Y-%m-%d")
|
||||
while True:
|
||||
yield "如果您看到了本段文字,请联系开发者"
|
||||
|
||||
|
||||
16
module/defense/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
import pathlib
|
||||
from copy import deepcopy
|
||||
|
||||
from docx import Document
|
||||
from docx.shared import Cm
|
||||
from docx.enum.text import WD_BREAK
|
||||
from docx.shared import Cm, Mm
|
||||
|
||||
from module.schema import Course, Student, Question
|
||||
from module.schema import Course, Student
|
||||
|
||||
|
||||
class DocPaper:
|
||||
@@ -14,16 +30,19 @@ class DocPaper:
|
||||
self._filename = filename
|
||||
|
||||
section = self._doc.sections[0]
|
||||
section.page_width = Mm(210)
|
||||
section.page_height = Mm(297)
|
||||
section.top_margin = Cm(2)
|
||||
section.bottom_margin = Cm(2)
|
||||
section.left_margin = Cm(2)
|
||||
section.right_margin = Cm(2)
|
||||
|
||||
def add_paper(self, course: Course, student: Student):
|
||||
temp_table = self._template.tables[0]
|
||||
new_table = deepcopy(temp_table)
|
||||
new_table = deepcopy(self._template.tables[0])
|
||||
|
||||
para = self._doc.add_paragraph()
|
||||
para._p.addprevious(new_table._element)
|
||||
para.add_run().add_break(WD_BREAK.PAGE)
|
||||
|
||||
data_list = {
|
||||
'%CNAME%': course.name,
|
||||
@@ -31,9 +50,7 @@ class DocPaper:
|
||||
'%SNAME%': student.name,
|
||||
'%NO%': student.no,
|
||||
'%SO%': student.so,
|
||||
'%Q1%': student.picked_questions[0].topic,
|
||||
'%Q2%': student.picked_questions[1].topic,
|
||||
'%Q3%': student.picked_questions[2].topic
|
||||
'%Q%': '\n'.join([f'\t{idx + 1}、{i.topic}' for idx, i in enumerate(student.picked_questions)])
|
||||
}
|
||||
|
||||
# 替换表格中的占位符
|
||||
@@ -43,7 +60,7 @@ class DocPaper:
|
||||
for run in para.runs:
|
||||
for key, val in data_list.items():
|
||||
if key in run.text:
|
||||
run.text = run.text.replace(key, val)
|
||||
run.text = run.text.replace(key, str(val))
|
||||
break
|
||||
|
||||
def save(self, path: str = './'):
|
||||
@@ -51,12 +68,4 @@ class DocPaper:
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
course = Course.load_from_xls('../files/21工程管理-工程造价Ⅱ-点名册-系统0828.xlsx')
|
||||
students = Student.load_from_xls('../files/21工程管理-工程造价Ⅱ-点名册-系统0828.xlsx')
|
||||
questions = Question.load_from_csv()
|
||||
|
||||
d = DocPaper()
|
||||
for student in students:
|
||||
student.pick_question(questions)
|
||||
d.add_paper(course, student)
|
||||
d.save()
|
||||
...
|
||||
16
module/picker/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
|
||||
205
module/picker/schema.py
Normal file
@@ -0,0 +1,205 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
import random
|
||||
from typing import Optional
|
||||
|
||||
from openpyxl.reader.excel import load_workbook
|
||||
from openpyxl.workbook.workbook import Workbook
|
||||
from openpyxl.worksheet.worksheet import Worksheet
|
||||
|
||||
|
||||
class PickerStudent:
|
||||
total_time: int = 0
|
||||
|
||||
def __init__(self, name: str, so: str, position: int, scores: list[int]):
|
||||
self._name = name
|
||||
self._so = str(so)
|
||||
self._position = position
|
||||
self._scores = scores
|
||||
self._modify = False
|
||||
self._scores = []
|
||||
self._scores.extend(scores)
|
||||
self._scores_pointer = -1
|
||||
|
||||
self.init_score()
|
||||
|
||||
def init_score(self):
|
||||
self._scores += [0] * (self.total_time - len(self._scores))
|
||||
|
||||
for idx, val in enumerate(self._scores):
|
||||
if val is None:
|
||||
self._scores[idx] = 0
|
||||
if self._scores[idx] == 0:
|
||||
self._scores_pointer = idx
|
||||
break
|
||||
|
||||
def append_score(self, score: int) -> int:
|
||||
if self._scores_pointer > self.total_time or self._scores_pointer < 0:
|
||||
raise IndexError
|
||||
self._scores[self._scores_pointer] = score
|
||||
self._scores_pointer += 1
|
||||
self.modified()
|
||||
return self._scores_pointer - 1
|
||||
|
||||
def change_score(self, new_score: int, index: int):
|
||||
if index < 0 or index > self.total_time:
|
||||
raise IndexError
|
||||
self._scores[index] = new_score
|
||||
self.modified()
|
||||
|
||||
@staticmethod
|
||||
def pick(student: list['PickerStudent']) -> Optional['PickerStudent']:
|
||||
filtered = [item for item in student if item.weight != 100 and item.weight > 0]
|
||||
if not filtered:
|
||||
return None
|
||||
|
||||
# 计算倒数权重
|
||||
weights = [1 / item.weight for item in filtered]
|
||||
total = sum(weights)
|
||||
r = random.uniform(0, total)
|
||||
|
||||
cumulative = 0
|
||||
for item, w in zip(filtered, weights):
|
||||
cumulative += w
|
||||
if r <= cumulative:
|
||||
return item
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def set_total_time(cls, total_time: int):
|
||||
cls.total_time = total_time
|
||||
|
||||
@property
|
||||
def position(self) -> int:
|
||||
return self._position
|
||||
|
||||
@property
|
||||
def scores(self) -> list[int]:
|
||||
return self._scores
|
||||
|
||||
@property
|
||||
def modify(self) -> bool:
|
||||
return self._modify
|
||||
|
||||
@property
|
||||
def weight(self) -> int:
|
||||
return int(sum(1 for x in self._scores if x != 0) / self.total_time * 100)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def so(self) -> str:
|
||||
return self._so
|
||||
|
||||
def saved(self):
|
||||
self._modify = False
|
||||
|
||||
def modified(self):
|
||||
self._modify = True
|
||||
|
||||
def __str__(self):
|
||||
return f"PickerStudent {self._name} at {self.position}, with scores: {','.join(str(x) for x in self._scores if x)}"
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
|
||||
class PickerExcel:
|
||||
wb: Optional[Workbook]
|
||||
ws: Optional[Worksheet]
|
||||
path: str = ''
|
||||
max_time_position = 'M1'
|
||||
start_row = 5
|
||||
|
||||
def __init__(self, path: str):
|
||||
self.open(path)
|
||||
|
||||
@classmethod
|
||||
def open(cls, path: str):
|
||||
cls.path = path
|
||||
cls.wb = load_workbook(path, keep_vba=True)
|
||||
cls.ws = cls.wb.active
|
||||
PickerStudent.set_total_time(cls.ws[cls.max_time_position].value)
|
||||
|
||||
@classmethod
|
||||
def read_student(cls, path: Optional[str] = None) -> list[PickerStudent]:
|
||||
if path:
|
||||
cls.open(path)
|
||||
|
||||
if cls.wb and cls.ws:
|
||||
ret = []
|
||||
for index, row in enumerate(cls.ws.iter_rows(
|
||||
min_row=cls.start_row,
|
||||
max_col=4 + cls.ws[cls.max_time_position].value,
|
||||
values_only=True)
|
||||
):
|
||||
if (name := row[2]) is None:
|
||||
break
|
||||
ret.append(PickerStudent(name, row[1], cls.start_row + index, row[4:]))
|
||||
return ret
|
||||
raise Exception('No Workbook or Worksheet')
|
||||
|
||||
@classmethod
|
||||
def read_total_time(cls, path: Optional[str] = None) -> int:
|
||||
if path:
|
||||
cls.open(path)
|
||||
|
||||
if cls.wb and cls.ws:
|
||||
val = cls.ws[cls.max_time_position].value
|
||||
if isinstance(val, int):
|
||||
return val
|
||||
raise Exception(f'总次数读取错误,期待类型 {type(1)} 但实际为 {type(val)}\n'
|
||||
f'可能的解决方法:\n'
|
||||
f'1、确保总次数位于 \'{cls.max_time_position}\' 单元格;\n'
|
||||
f'2、确保选择了正确的 Excel 文件。')
|
||||
raise Exception('No Workbook or Worksheet')
|
||||
|
||||
@classmethod
|
||||
def save_total_time(cls, path: Optional[str] = None, value: Optional[int] = None) -> None:
|
||||
if path:
|
||||
cls.open(path)
|
||||
|
||||
if cls.wb and cls.ws and value:
|
||||
cls.ws[cls.max_time_position].value = value
|
||||
cls.save()
|
||||
|
||||
@classmethod
|
||||
def write_back(cls, student: PickerStudent):
|
||||
start_col = 5 # E列
|
||||
row = student.position
|
||||
|
||||
for i, val in enumerate(student.scores):
|
||||
cls.ws.cell(row=row, column=start_col + i, value=val if val != 0 else '')
|
||||
|
||||
student.saved()
|
||||
cls.save()
|
||||
|
||||
@classmethod
|
||||
def write_all(cls, students: list[PickerStudent]):
|
||||
for student in students:
|
||||
if student.modify:
|
||||
cls.write_back(student)
|
||||
|
||||
@classmethod
|
||||
def save(cls):
|
||||
if cls.ws:
|
||||
cls.wb.save(cls.path)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
...
|
||||
28072
module/resources.py
@@ -1,5 +1,20 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
import random
|
||||
from typing import Optional, Tuple
|
||||
from typing import Optional, Tuple, NoReturn
|
||||
|
||||
from openpyxl.reader.excel import load_workbook
|
||||
|
||||
@@ -8,6 +23,7 @@ class Question:
|
||||
def __init__(self, no: str, topic: str):
|
||||
self._no: str = no
|
||||
self._topic: str = topic
|
||||
self._count: int = 0
|
||||
|
||||
def __str__(self):
|
||||
return f"Question<No: {self._no}, Topic: {self._topic}>"
|
||||
@@ -43,6 +59,12 @@ class Question:
|
||||
def topic(self) -> str:
|
||||
return self._topic
|
||||
|
||||
def increase_count(self) -> None:
|
||||
self._count += 1
|
||||
|
||||
def can_pick(self, max_count: int) -> bool:
|
||||
return self._count <= max_count
|
||||
|
||||
|
||||
class Student:
|
||||
def __init__(self, no: str, so: str, name: str, major: str, class_name: str):
|
||||
@@ -60,12 +82,20 @@ class Student:
|
||||
return self.__str__()
|
||||
|
||||
@staticmethod
|
||||
def load_from_xls(path: str) -> list['Student']:
|
||||
def load_from_xls(path: str) -> list['Student'] | NoReturn:
|
||||
wb = load_workbook(path, read_only=True)
|
||||
ws = wb.active
|
||||
students = []
|
||||
for row in ws.iter_rows(min_row=6, max_col=5, values_only=True):
|
||||
students.append(Student(*row))
|
||||
|
||||
if ws.title == 'Sheet1':
|
||||
for row in ws.iter_rows(min_row=6, max_col=5, values_only=True):
|
||||
students.append(Student(*row))
|
||||
elif ws.title == '初始录入':
|
||||
for row in ws.iter_rows(min_row=13, max_col=5, values_only=True):
|
||||
students.append(Student(*row))
|
||||
else:
|
||||
raise Exception("无法解析学生名单")
|
||||
|
||||
wb.close()
|
||||
return [x for x in students if x.valid]
|
||||
|
||||
@@ -97,10 +127,15 @@ class Student:
|
||||
def class_name(self) -> str:
|
||||
return self._class_name
|
||||
|
||||
def pick_question(self, questions: list[Question], num: int = 3) -> None:
|
||||
if len(questions) < num:
|
||||
raise ValueError("Not enough questions to pick from.")
|
||||
self._picked_questions = random.sample(questions, num)
|
||||
def pick_question(self, questions: list[Question], num: int = 3, max_count: int = 3) -> None:
|
||||
available_questions = [q for q in questions if q.can_pick(max_count)]
|
||||
|
||||
if len(available_questions) < num:
|
||||
raise ValueError("Not enough questions with count <= max_count")
|
||||
self._picked_questions = random.sample(available_questions, num)
|
||||
|
||||
for q in self._picked_questions:
|
||||
q.increase_count()
|
||||
|
||||
|
||||
class Course:
|
||||
@@ -114,12 +149,17 @@ class Course:
|
||||
return self.__str__()
|
||||
|
||||
@staticmethod
|
||||
def load_from_xls(path: str) -> 'Course':
|
||||
def load_from_xls(path: str) -> 'Course' | NoReturn:
|
||||
wb = load_workbook(path, read_only=True)
|
||||
ws = wb.active
|
||||
name: str = ws['E3'].value
|
||||
if ws.title == 'Sheet1':
|
||||
name: str = ws['E3'].value[5:]
|
||||
elif ws.title == '初始录入':
|
||||
name: str = ws['D5'].value
|
||||
else:
|
||||
raise Exception("无法解析课程名")
|
||||
wb.close()
|
||||
return Course(name[5:])
|
||||
return Course(name)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
|
||||
@@ -1,18 +1,36 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
import math
|
||||
import os
|
||||
import traceback
|
||||
from typing import Literal
|
||||
|
||||
import pythoncom
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
from win32com import client
|
||||
|
||||
from module.achievement_doc import DocxWriter
|
||||
from module.achievement_excel import ExcelReader
|
||||
from module.doc import DocPaper
|
||||
from module.achievement.doc import DocxWriter
|
||||
from module.achievement.excel import ExcelReader
|
||||
from module.defense.doc import DocPaper
|
||||
from module.schema import Course, Student, Question
|
||||
from utils.function import resource_path
|
||||
|
||||
|
||||
class DTGWorker(QObject):
|
||||
progress = Signal(int)
|
||||
progress = Signal((int,), (str,))
|
||||
finished = Signal()
|
||||
error = Signal(str, str)
|
||||
|
||||
@@ -21,13 +39,15 @@ class DTGWorker(QObject):
|
||||
input_student_filepath: str,
|
||||
input_question_filepath: str,
|
||||
output_filepath: str,
|
||||
output_filename: str
|
||||
output_filename: str,
|
||||
output_type: Literal['pdf', 'word'] = 'pdf'
|
||||
):
|
||||
super().__init__()
|
||||
self.input_filepath = input_student_filepath
|
||||
self.input_question_filepath = input_question_filepath
|
||||
self.output_filepath = output_filepath
|
||||
self.output_filename = output_filename
|
||||
self.output_type = output_type
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
@@ -35,35 +55,45 @@ class DTGWorker(QObject):
|
||||
students = Student.load_from_xls(self.input_filepath)
|
||||
questions = Question.load_from_xls(self.input_question_filepath)
|
||||
|
||||
d = DocPaper(self.output_filename, template_path=resource_path("template/template.docx"))
|
||||
max_question_count = math.ceil(len(students) * 3 / len(questions))
|
||||
|
||||
d = DocPaper(self.output_filename, resource_path("template/template-defense-paper-paper.docx"))
|
||||
for index, student in enumerate(students):
|
||||
if (p := int((index + 1) / len(students) * 100)) != 100:
|
||||
self.progress.emit(p)
|
||||
else:
|
||||
self.progress.emit(99)
|
||||
student.pick_question(questions)
|
||||
self.progress[int].emit(p)
|
||||
student.pick_question(questions, max_count=max_question_count)
|
||||
d.add_paper(course, student)
|
||||
d.save(self.output_filepath)
|
||||
self.progress.emit(100)
|
||||
|
||||
word_file = self.output_filepath + "/" + self.output_filename + ".docx"
|
||||
pdf_file = self.output_filepath + "/" + self.output_filename + ".pdf"
|
||||
|
||||
if os.path.exists(pdf_file):
|
||||
os.remove(pdf_file)
|
||||
if self.output_type == 'pdf':
|
||||
self.progress[int].emit(101)
|
||||
self.progress[str].emit("正在转换文件")
|
||||
|
||||
word = client.Dispatch("Word.Application")
|
||||
doc = word.Documents.Open(word_file)
|
||||
doc.SaveAs(pdf_file, 17)
|
||||
doc.Close()
|
||||
word.Quit()
|
||||
|
||||
os.remove(word_file)
|
||||
os.startfile(pdf_file)
|
||||
except Exception as _:
|
||||
error_msg = traceback.format_exc()
|
||||
self.error.emit("😢 不好出错了", error_msg)
|
||||
self.progress.emit(-1)
|
||||
if os.path.exists(pdf_file):
|
||||
os.remove(pdf_file)
|
||||
pythoncom.CoInitialize()
|
||||
# https://stackoverflow.com/questions/71292585/python-docx2pdf-attributeerror-open-saveas
|
||||
word = client.Dispatch("Word.Application")
|
||||
doc = word.Documents.Open(word_file)
|
||||
try:
|
||||
doc.SaveAs(pdf_file, 17)
|
||||
doc.Close()
|
||||
os.remove(word_file)
|
||||
os.startfile(pdf_file)
|
||||
except Exception as e:
|
||||
doc.Close()
|
||||
os.startfile(word_file)
|
||||
raise Exception("PDF转换失败,但Word文档已生成,已打开Word文档") from e
|
||||
finally:
|
||||
word.Quit()
|
||||
elif self.output_type == 'word':
|
||||
os.startfile(word_file)
|
||||
except Exception:
|
||||
self.error.emit("😢 不好出错了", traceback.format_exc())
|
||||
self.progress[int].emit(-1)
|
||||
finally:
|
||||
self.finished.emit()
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
pytest
|
||||
openpyxl~=3.1.5
|
||||
pyside6~=6.9.0
|
||||
python-docx~=1.1.2
|
||||
matplotlib~=3.10.3
|
||||
PySide6-Fluent-Widgets[full]
|
||||
packaging~=25.0
|
||||
packaging~=25.0
|
||||
pyinstaller
|
||||
pywin32
|
||||
@@ -1,6 +1,28 @@
|
||||
<!--
|
||||
- Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
- #
|
||||
- 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/>.
|
||||
-->
|
||||
|
||||
<!DOCTYPE RCC>
|
||||
<RCC version="1.0">
|
||||
<qresource>
|
||||
<file>./images/logo.png</file>
|
||||
<file>./images/3rd/qfluentwidgets.png</file>
|
||||
<file>./images/3rd/qt.png</file>
|
||||
<file>./images/3rd/matplotlib.svg</file>
|
||||
<file>./images/3rd/packaging.png</file>
|
||||
<file>./images/gplv3.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
</RCC>
|
||||
|
||||
BIN
template/template-achievement-file.xlsm
Normal file
BIN
template/template-defense-oral-paper.docx
Normal file
BIN
template/template-defense-oral-student.xlsm
Normal file
BIN
template/template-defense-paper-paper.docx
Normal file
BIN
template/template-defense-paper-questions.xlsm
Normal file
BIN
template/template-defense-paper-student-1.xlsm
Normal file
BIN
template/template-defense-paper-student-2.xlsm
Normal file
BIN
template/template-pick-student.xlsm
Normal file
109
toolbox/config/achievement.default.excel.json
Normal file
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"name": "achievement",
|
||||
"version": "9.0",
|
||||
"compatibleVersion": [
|
||||
"9.0"
|
||||
],
|
||||
"config": [
|
||||
{
|
||||
"name": "version",
|
||||
"position": "H1",
|
||||
"type": "single"
|
||||
},
|
||||
{
|
||||
"name": "class_full_name",
|
||||
"position": "D10",
|
||||
"type": "single"
|
||||
},
|
||||
{
|
||||
"name": "course_name",
|
||||
"position": "D5",
|
||||
"type": "single"
|
||||
},
|
||||
{
|
||||
"name": "teacher_name",
|
||||
"position": "D7",
|
||||
"type": "single"
|
||||
},
|
||||
{
|
||||
"name": "master_name",
|
||||
"position": "D8",
|
||||
"type": "single"
|
||||
},
|
||||
{
|
||||
"name": "class_single_name",
|
||||
"position": "K",
|
||||
"type": "range",
|
||||
"start": 2,
|
||||
"end": 5
|
||||
},
|
||||
{
|
||||
"name": "class_single_number",
|
||||
"position": "M",
|
||||
"type": "range",
|
||||
"start": 2,
|
||||
"end": 5
|
||||
},
|
||||
{
|
||||
"name": "kpi_number",
|
||||
"position": "H8",
|
||||
"type": "single"
|
||||
},
|
||||
{
|
||||
"name": "hml",
|
||||
"position": "H",
|
||||
"type": "range",
|
||||
"start": 22,
|
||||
"end": null
|
||||
},
|
||||
{
|
||||
"name": "hml_goal",
|
||||
"position": "I",
|
||||
"type": "range",
|
||||
"start": 22,
|
||||
"end": null
|
||||
},
|
||||
{
|
||||
"name": "hml_indicate",
|
||||
"position": "Q",
|
||||
"type": "range",
|
||||
"start": 22,
|
||||
"end": null
|
||||
},
|
||||
{
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
0
toolbox/config/override.excel.json
Normal file
112
toolbox/models/config.py
Normal file
@@ -0,0 +1,112 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
import json
|
||||
from abc import ABC, abstractmethod
|
||||
from os import PathLike
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class BaseExcelConfig(ABC):
|
||||
@property
|
||||
@abstractmethod
|
||||
def name(self) -> str:
|
||||
...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def position(self) -> str:
|
||||
...
|
||||
|
||||
|
||||
class RangeExcelConfigMixin(ABC):
|
||||
@property
|
||||
@abstractmethod
|
||||
def start(self) -> int:
|
||||
...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def end(self) -> Optional[int]:
|
||||
...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def fposition(self) -> str:
|
||||
...
|
||||
|
||||
|
||||
class SingleExcelConfigItem(BaseExcelConfig):
|
||||
|
||||
def __init__(self, name: str, position: str):
|
||||
self._name = name
|
||||
self._position = position
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def position(self) -> str:
|
||||
return self._position
|
||||
|
||||
|
||||
class RangeExcelConfigItem(SingleExcelConfigItem, RangeExcelConfigMixin):
|
||||
|
||||
def __init__(self, name: str, position: str, start: int, end: Optional[int] = None):
|
||||
super().__init__(name, position)
|
||||
self._start = start
|
||||
self._end = end
|
||||
|
||||
@property
|
||||
def start(self) -> int:
|
||||
return self._start
|
||||
|
||||
@property
|
||||
def end(self) -> Optional[int]:
|
||||
return self._end
|
||||
|
||||
@property
|
||||
def fposition(self) -> str:
|
||||
return self.position + '{}'
|
||||
|
||||
|
||||
class AESConfig:
|
||||
_config_list: list[SingleExcelConfigItem | RangeExcelConfigItem]
|
||||
|
||||
def __init__(self, file_path: str | PathLike):
|
||||
self._file_path = file_path
|
||||
self._config_list = []
|
||||
self._init_config()
|
||||
|
||||
def __getitem__(self, item: str):
|
||||
return self.get_config(item)
|
||||
|
||||
def _init_config(self):
|
||||
with open(self._file_path, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
f.close()
|
||||
|
||||
for item in config['config']:
|
||||
itype = item.pop('type', None)
|
||||
if itype == 'range':
|
||||
self._config_list.append(RangeExcelConfigItem(**item))
|
||||
elif itype == 'single':
|
||||
self._config_list.append(SingleExcelConfigItem(**item))
|
||||
|
||||
def get_config(self, name: str) -> Optional[RangeExcelConfigItem | SingleExcelConfigItem]:
|
||||
for config in self._config_list:
|
||||
if config.name == name:
|
||||
return config
|
||||
return None
|
||||
32
toolbox/models/data_model.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ClassInfo:
|
||||
full_name = ""
|
||||
|
||||
class_name: str
|
||||
class_number: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class CourseInfo:
|
||||
course_name: str
|
||||
# 任课教师
|
||||
course_teacher_name: str
|
||||
# 课程负责人
|
||||
course_master_name: str
|
||||
119
toolbox/services/excel_service.py
Normal file
@@ -0,0 +1,119 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
import json
|
||||
from abc import ABC, abstractmethod
|
||||
from os import PathLike
|
||||
from typing import Optional
|
||||
|
||||
import openpyxl
|
||||
from openpyxl.workbook import Workbook
|
||||
from openpyxl.worksheet.worksheet import Worksheet
|
||||
|
||||
from toolbox.models.data_model import ClassInfo
|
||||
from toolbox.models.config import AESConfig
|
||||
|
||||
|
||||
class BaseExcelService(ABC):
|
||||
_file_path: str | PathLike
|
||||
_workbook: Optional[Workbook]
|
||||
_sheet: None
|
||||
|
||||
@abstractmethod
|
||||
def open(self, *args, **kwargs) -> 'BaseExcelService':
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def save(self) -> 'BaseExcelService':
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def close(self) -> None:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def active_sheet(self, sheet_name: str) -> 'BaseExcelService':
|
||||
...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def cur_active_sheet(self) -> Worksheet:
|
||||
...
|
||||
|
||||
|
||||
class ExcelService(BaseExcelService):
|
||||
def __init__(self, file_path: str | PathLike):
|
||||
self._file_path = file_path
|
||||
self._workbook = None
|
||||
self._sheet = None
|
||||
|
||||
def open(self, *args, **kwargs):
|
||||
self._workbook = openpyxl.load_workbook(self._file_path, *args, **kwargs)
|
||||
return self
|
||||
|
||||
def save(self):
|
||||
self._workbook.save(self._file_path)
|
||||
return self
|
||||
|
||||
def close(self):
|
||||
self._workbook.close()
|
||||
|
||||
def active_sheet(self, sheet_name: str):
|
||||
self._sheet = self._workbook[sheet_name]
|
||||
return self
|
||||
|
||||
@property
|
||||
def cur_active_sheet(self):
|
||||
return self._sheet
|
||||
|
||||
def load_value(self, cell: str):
|
||||
if self._sheet is None:
|
||||
raise ValueError("No active sheet. Please set an active sheet first.")
|
||||
return self._sheet[cell].value
|
||||
|
||||
|
||||
class AchievementExcelService(ExcelService):
|
||||
version = ''
|
||||
config = {}
|
||||
|
||||
def __init__(self, file_path: str | PathLike):
|
||||
super().__init__(file_path)
|
||||
self.open(read_only=True, data_only=True)
|
||||
|
||||
def load_config(self, config_path: str | PathLike):
|
||||
self.config = AESConfig(config_path)
|
||||
|
||||
def read_class_info(self) -> list[ClassInfo]:
|
||||
lst = []
|
||||
self.active_sheet('初始录入')
|
||||
full_name = self.load_value(self.config['class_full_name'].position)
|
||||
|
||||
for i in range(self.config['class_single_name'].start, self.config['class_single_name'].end):
|
||||
name = self.load_value(self.config['class_single_name'].fposition.format(i))
|
||||
number = self.load_value(self.config['class_single_number'].fposition.format(i))
|
||||
|
||||
if name is None or number is None:
|
||||
break
|
||||
|
||||
ci = ClassInfo(name, number)
|
||||
ci.full_name = full_name
|
||||
lst.append(ci)
|
||||
|
||||
if len(lst) == 0:
|
||||
raise ValueError("No class information found in the Excel file.")
|
||||
|
||||
return lst
|
||||
|
||||
def read_course_info(self):
|
||||
...
|
||||
19
toolbox/tests/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
PACKAGE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
TEST_FILE_PATH = Path(os.path.join(PACKAGE_DIR, 'files'))
|
||||
41
toolbox/tests/test_config_model.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
from toolbox.models.config import AESConfig, SingleExcelConfigItem, RangeExcelConfigItem
|
||||
from toolbox.tests import TEST_FILE_PATH
|
||||
|
||||
|
||||
def test_config_model():
|
||||
aesc = AESConfig(TEST_FILE_PATH / 'test_config_model_01.json')
|
||||
|
||||
a = aesc.get_config('A')
|
||||
assert isinstance(a, SingleExcelConfigItem)
|
||||
assert a.position == 'H1'
|
||||
|
||||
b = aesc.get_config('B')
|
||||
assert isinstance(b, SingleExcelConfigItem)
|
||||
assert b.position == 'D10'
|
||||
|
||||
c = aesc.get_config('C')
|
||||
assert isinstance(c, RangeExcelConfigItem)
|
||||
assert c.position == 'K'
|
||||
assert c.fposition.format(1) == 'K1'
|
||||
assert c.start == 2
|
||||
assert c.end == 5
|
||||
|
||||
d = aesc.get_config('D')
|
||||
assert isinstance(d, RangeExcelConfigItem)
|
||||
assert d.position == 'Q'
|
||||
assert d.fposition.format(d.start) == 'Q22'
|
||||
assert d.end is None
|
||||
79
toolbox/tests/test_excel_services.py
Normal file
@@ -0,0 +1,79 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from openpyxl.workbook import Workbook
|
||||
|
||||
from toolbox.services.excel_service import ExcelService, AchievementExcelService
|
||||
from toolbox.tests import TEST_FILE_PATH
|
||||
|
||||
SAVE_TEMP_FILE = False
|
||||
|
||||
|
||||
class TestExcelService:
|
||||
es = ExcelService(TEST_FILE_PATH / 'test_excel_services_01.xlsx')
|
||||
es.open(data_only=True)
|
||||
|
||||
def test_open(self):
|
||||
assert isinstance(self.es._workbook, Workbook)
|
||||
assert self.es._sheet is None
|
||||
|
||||
def test_open_failed(self):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
ExcelService(TEST_FILE_PATH / 'non_existent_file.xlsx').open()
|
||||
|
||||
def test_active_sheet(self):
|
||||
self.es.active_sheet('Sheet1')
|
||||
assert self.es._sheet.title == 'Sheet1'
|
||||
self.es.active_sheet('Sheet2')
|
||||
assert self.es._sheet.title == 'Sheet2'
|
||||
|
||||
def test_cur_active_sheet(self):
|
||||
self.es.active_sheet('Sheet1')
|
||||
assert self.es.cur_active_sheet.title == 'Sheet1'
|
||||
self.es.active_sheet('Sheet2')
|
||||
assert self.es.cur_active_sheet.title == 'Sheet2'
|
||||
|
||||
def test_save_and_close(self):
|
||||
temp_excel_file = TEST_FILE_PATH / 'test_excel_services_01_temp.xlsx'
|
||||
shutil.copy(self.es._file_path, temp_excel_file)
|
||||
|
||||
self.es.active_sheet('Sheet1').cur_active_sheet['A1'] = 'Modified'
|
||||
self.es.save().close()
|
||||
|
||||
es2 = ExcelService(temp_excel_file).open().active_sheet('Sheet1')
|
||||
assert es2.cur_active_sheet['A1'].value == 'Modified'
|
||||
es2.close()
|
||||
|
||||
if not SAVE_TEMP_FILE:
|
||||
Path(TEST_FILE_PATH / 'test_excel_services_01_temp.xlsx').unlink()
|
||||
|
||||
|
||||
class TestAchievementExcelService:
|
||||
aes = AchievementExcelService(TEST_FILE_PATH / 'test_achievement_excel_service_01.xlsm')
|
||||
aes.load_config(TEST_FILE_PATH / 'test_achievement.default.excel_01.json')
|
||||
|
||||
def test_read_class_info(self):
|
||||
cis = self.aes.read_class_info()
|
||||
assert len(cis) == 2
|
||||
assert cis[0].full_name == '22工程管理(1)(2)'
|
||||
assert cis[1].full_name == '22工程管理(1)(2)'
|
||||
|
||||
assert cis[0].class_name == '22工程管理(1)'
|
||||
assert cis[0].class_number == 34
|
||||
assert cis[1].class_name == '22工程管理(2)'
|
||||
assert cis[1].class_number == 36
|
||||
17
ui/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
MAIN_THEME_COLOR = "#0064b0"
|
||||
BLUE_BACKGROUND_COLOR = "#dbeafe"
|
||||
16
ui/components/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QIcon
|
||||
from qfluentwidgets import InfoBar, InfoBarPosition, InfoBarIcon, FluentIconBase, ProgressBar, IndeterminateProgressBar, \
|
||||
|
||||
@@ -1,4 +1,27 @@
|
||||
from PySide6.QtWidgets import QFrame
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
import random
|
||||
from typing import Union
|
||||
|
||||
from PySide6.QtCore import QTimer, Qt, Signal
|
||||
from PySide6.QtGui import QIcon
|
||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QFrame, QLayout
|
||||
from qfluentwidgets import DisplayLabel, LargeTitleLabel, GroupHeaderCardWidget, FluentIconBase, CardGroupWidget
|
||||
|
||||
from module.picker.schema import PickerStudent
|
||||
|
||||
|
||||
class Widget(QFrame):
|
||||
@@ -7,3 +30,91 @@ class Widget(QFrame):
|
||||
super().__init__(parent=parent)
|
||||
# 必须给子界面设置全局唯一的对象名
|
||||
self.setObjectName(key.replace(' ', '-'))
|
||||
|
||||
|
||||
class RollingTextWidget(QWidget):
|
||||
finishSignal = Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.current_index = 0
|
||||
self.items = []
|
||||
|
||||
self.soLabel = LargeTitleLabel("", self)
|
||||
self.nameLabel = DisplayLabel("", self)
|
||||
self.soLabel.setAlignment(Qt.AlignCenter)
|
||||
self.nameLabel.setAlignment(Qt.AlignCenter)
|
||||
|
||||
self.layout = QVBoxLayout()
|
||||
self.layout.addWidget(self.soLabel)
|
||||
self.layout.addWidget(self.nameLabel)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
self.rolling_timer = QTimer(self)
|
||||
self.rolling_timer.setInterval(50) # 滚动速度(毫秒)
|
||||
self.rolling_timer.timeout.connect(self.update_text)
|
||||
|
||||
self.stop_timer = QTimer(self)
|
||||
self.stop_timer.setSingleShot(True)
|
||||
self.stop_timer.timeout.connect(self.stop_rolling)
|
||||
|
||||
def update_text(self):
|
||||
# 每次显示下一个字符
|
||||
self.current_index = (self.current_index + 1) % len(self.items)
|
||||
stu = self.items[self.current_index]
|
||||
self.soLabel.setText(stu.so)
|
||||
self.nameLabel.setText(stu.name)
|
||||
|
||||
def start_rolling(self):
|
||||
if not self.rolling_timer.isActive():
|
||||
self.rolling_timer.start()
|
||||
self.stop_timer.start(2000) # 2秒后停止滚动
|
||||
|
||||
def stop_rolling(self):
|
||||
self.rolling_timer.stop()
|
||||
self.finishSignal.emit()
|
||||
|
||||
def set_items(self, items: list[PickerStudent]):
|
||||
self.items = items[:]
|
||||
random.shuffle(self.items)
|
||||
|
||||
def show_result(self, student: PickerStudent):
|
||||
self.soLabel.setText(student.so)
|
||||
self.nameLabel.setText(student.name)
|
||||
|
||||
def clear_text(self):
|
||||
self.soLabel.clear()
|
||||
self.nameLabel.clear()
|
||||
|
||||
|
||||
class MyCardGroupWidget(CardGroupWidget):
|
||||
def addLayout(self, layout: QLayout, stretch=0):
|
||||
self.hBoxLayout.addLayout(layout, stretch=stretch)
|
||||
|
||||
|
||||
class MyGroupHeaderCardWidget(GroupHeaderCardWidget):
|
||||
def addGroup(self, icon: Union[str, FluentIconBase, QIcon], title: str, content: str,
|
||||
object: Union[QWidget, QLayout],
|
||||
stretch=0) -> CardGroupWidget:
|
||||
group = MyCardGroupWidget(icon, title, content, self)
|
||||
|
||||
if isinstance(object, QWidget):
|
||||
group.addWidget(object, stretch=stretch)
|
||||
elif isinstance(object, QLayout):
|
||||
group.addLayout(object, stretch=stretch)
|
||||
|
||||
if self.groupWidgets:
|
||||
self.groupWidgets[-1].setSeparatorVisible(True)
|
||||
|
||||
self.groupLayout.addWidget(group)
|
||||
self.groupWidgets.append(group)
|
||||
return group
|
||||
|
||||
|
||||
class NotImplementedWidget(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.label = DisplayLabel("🚧", self)
|
||||
self.label.setAlignment(Qt.AlignCenter)
|
||||
self.layout.addWidget(self.label)
|
||||
|
||||
31
ui/components/window.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
from PySide6.QtWidgets import QHBoxLayout
|
||||
from qfluentwidgets import FluentTitleBar
|
||||
from qfluentwidgets.window.fluent_window import FluentWindowBase
|
||||
|
||||
|
||||
class MyWindow(FluentWindowBase):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setTitleBar(FluentTitleBar(self))
|
||||
|
||||
self.widgetLayout = QHBoxLayout(self)
|
||||
self.widgetLayout.setContentsMargins(0, 48, 0, 0)
|
||||
self.hBoxLayout.addLayout(self.widgetLayout)
|
||||
|
||||
self.titleBar.raise_()
|
||||
self.showMaximized()
|
||||
134
ui/main.py
@@ -1,41 +1,137 @@
|
||||
from PySide6.QtGui import QIcon
|
||||
from qfluentwidgets import FluentIcon, MSFluentWindow, NavigationItemPosition, MessageBox
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable
|
||||
|
||||
from PySide6.QtGui import QIcon, QShowEvent
|
||||
from qfluentwidgets import FluentIcon, MSFluentWindow, NavigationItemPosition, MessageBox, setThemeColor
|
||||
|
||||
from ui import MAIN_THEME_COLOR, BLUE_BACKGROUND_COLOR
|
||||
from ui.pyui.about_ui import AboutWidget
|
||||
from ui.pyui.achievement_ui import AchievementWidget
|
||||
from ui.pyui.defense_ui import DefenseWidget
|
||||
from ui.pyui.picker_ui import PickerWidget
|
||||
from ui.pyui.test_ui import TestWidget
|
||||
from utils.function import is_frozen
|
||||
from utils.function import RELEASE_ENV
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class InterfaceSpec:
|
||||
key: str
|
||||
factory: Callable[[], object]
|
||||
icon: FluentIcon
|
||||
nav_text: str
|
||||
position: NavigationItemPosition = NavigationItemPosition.TOP
|
||||
enabled: bool = True
|
||||
|
||||
|
||||
class MainWindow(MSFluentWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.achievementInterface = AchievementWidget('Achievement Interface', self)
|
||||
self.defenseInterface = DefenseWidget('Defense Interface', self)
|
||||
self.aboutInterface = AboutWidget('About Interface', self)
|
||||
if not is_frozen():
|
||||
self.testInterface = TestWidget('Test Interface', self)
|
||||
|
||||
self.achievementInterface.error.connect(self.showError)
|
||||
self.defenseInterface.errorSignal.connect(self.showError)
|
||||
setThemeColor(MAIN_THEME_COLOR)
|
||||
self.setCustomBackgroundColor(BLUE_BACKGROUND_COLOR, BLUE_BACKGROUND_COLOR)
|
||||
self.interface_specs = self.build_interface_specs()
|
||||
self.interfaces = self.create_interfaces(self.interface_specs)
|
||||
self.bind_error_handlers()
|
||||
|
||||
self.initNavigation()
|
||||
self.initWindow()
|
||||
|
||||
def initNavigation(self):
|
||||
self.addSubInterface(self.achievementInterface, FluentIcon.SPEED_HIGH, '达成度')
|
||||
self.addSubInterface(self.defenseInterface, FluentIcon.FEEDBACK, '答辩')
|
||||
if not is_frozen():
|
||||
self.addSubInterface(self.testInterface, FluentIcon.VIEW, '测试')
|
||||
def build_interface_specs(self) -> list[InterfaceSpec]:
|
||||
return [
|
||||
InterfaceSpec(
|
||||
key="achievement",
|
||||
factory=lambda: AchievementWidget('Achievement Interface', self),
|
||||
icon=FluentIcon.SPEED_HIGH,
|
||||
nav_text='达成度',
|
||||
enabled=True,
|
||||
),
|
||||
InterfaceSpec(
|
||||
key="defense",
|
||||
factory=lambda: DefenseWidget('Defense Interface', self),
|
||||
icon=FluentIcon.FEEDBACK,
|
||||
nav_text='答辩',
|
||||
enabled=True,
|
||||
),
|
||||
InterfaceSpec(
|
||||
key="picker",
|
||||
factory=lambda: PickerWidget('Picker Interface', self),
|
||||
icon=FluentIcon.PEOPLE,
|
||||
nav_text='提问',
|
||||
enabled=not RELEASE_ENV,
|
||||
),
|
||||
InterfaceSpec(
|
||||
key="test",
|
||||
factory=lambda: TestWidget('Test Interface', self),
|
||||
icon=FluentIcon.VIEW,
|
||||
nav_text='测试',
|
||||
enabled=not RELEASE_ENV,
|
||||
),
|
||||
InterfaceSpec(
|
||||
key="about",
|
||||
factory=lambda: AboutWidget('About Interface', self),
|
||||
icon=FluentIcon.INFO,
|
||||
nav_text='关于',
|
||||
position=NavigationItemPosition.BOTTOM,
|
||||
enabled=True
|
||||
),
|
||||
]
|
||||
|
||||
self.addSubInterface(self.aboutInterface, FluentIcon.INFO, '关于', position=NavigationItemPosition.BOTTOM)
|
||||
def create_interfaces(self, specs: list[InterfaceSpec]) -> dict[str, object]:
|
||||
interfaces: dict[str, object] = {}
|
||||
for spec in specs:
|
||||
if not spec.enabled:
|
||||
continue
|
||||
widget = spec.factory()
|
||||
interfaces[spec.key] = widget
|
||||
setattr(self, f"{spec.key}Interface", widget)
|
||||
return interfaces
|
||||
|
||||
def bind_error_handlers(self):
|
||||
achievement = self.interfaces.get("achievement")
|
||||
defense = self.interfaces.get("defense")
|
||||
|
||||
if achievement and hasattr(achievement, "error"):
|
||||
achievement.error.connect(self.showError)
|
||||
if defense and hasattr(defense, "errorSignal"):
|
||||
defense.errorSignal.connect(self.showError)
|
||||
|
||||
def initNavigation(self):
|
||||
for spec in self.interface_specs:
|
||||
widget = self.interfaces.get(spec.key)
|
||||
if not widget:
|
||||
continue
|
||||
self.addSubInterface(widget, spec.icon, spec.nav_text, position=spec.position)
|
||||
|
||||
def initWindow(self):
|
||||
self.resize(900, 700)
|
||||
self.setWindowTitle('建工工具箱')
|
||||
self.setWindowTitle('教学工具箱')
|
||||
self.setWindowIcon(QIcon(':/images/logo.png'))
|
||||
|
||||
def showError(self, title: str, message: str):
|
||||
MessageBox(title, message, self).exec()
|
||||
box = MessageBox(title, message, self)
|
||||
box.yesButton.setText("关闭")
|
||||
box.cancelButton.hide()
|
||||
box.exec()
|
||||
|
||||
def showEvent(self, event: QShowEvent):
|
||||
super().showEvent(event)
|
||||
if RELEASE_ENV:
|
||||
import pyi_splash
|
||||
pyi_splash.update_text('正在加载...')
|
||||
pyi_splash.close()
|
||||
|
||||
@@ -1,58 +1,170 @@
|
||||
from PySide6.QtGui import QDesktopServices, Qt
|
||||
from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout
|
||||
from qfluentwidgets import PrimaryPushSettingCard, FluentIcon, GroupHeaderCardWidget, PushButton, ImageLabel, TitleLabel
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
|
||||
from PySide6.QtGui import QDesktopServices, Qt, QColor
|
||||
from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout, QWidget
|
||||
from qfluentwidgets import FluentIcon, GroupHeaderCardWidget, PushButton, ImageLabel, \
|
||||
TitleLabel, HeaderCardWidget, BodyLabel, HyperlinkLabel, SingleDirectionScrollArea
|
||||
from qfluentwidgets.components.widgets.card_widget import CardSeparator
|
||||
|
||||
from module.about.schema import ThirdParty
|
||||
from ui.components.widget import Widget
|
||||
from utils.function import RELEASE_ENV
|
||||
|
||||
if RELEASE_ENV:
|
||||
from build_info import *
|
||||
|
||||
|
||||
class AboutCard(HeaderCardWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setTitle('关于本程序')
|
||||
|
||||
self.vBoxLayout = QVBoxLayout(self)
|
||||
self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.lineHBoxLayout = QHBoxLayout(self)
|
||||
self.lineHBoxLayout.setContentsMargins(24, 16, 24, 8)
|
||||
self.lineVBoxLayout = QVBoxLayout(self)
|
||||
self.lineVBoxLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.textVBoxLayout = QVBoxLayout(self)
|
||||
self.textVBoxLayout.setContentsMargins(24, 8, 24, 16)
|
||||
self.viewLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.gplv3Image = ImageLabel(':/images/gplv3.png')
|
||||
self.lineHBoxLayout.addLayout(self.lineVBoxLayout)
|
||||
self.lineHBoxLayout.addWidget(self.gplv3Image)
|
||||
|
||||
self.addLine('程序名称', '教学工具箱')
|
||||
if RELEASE_ENV:
|
||||
self.addLine('程序版本', '1.0.0#' + GIT_HASH)
|
||||
self.addLine('作者', '许方杰')
|
||||
if RELEASE_ENV:
|
||||
self.addLine('构建时间', BUILD_TIME)
|
||||
self.addLine('许可证', 'GNU 通用公共许可证 第三版(GPLv3)')
|
||||
self.addLineUseLink('项目主页', 'https://cantyonion.site/git/cantyonion/JITToolBox')
|
||||
|
||||
self.addText(
|
||||
'教学工具箱是自由软件;您可以依据自由软件基金会发布的 GNU 通用公共许可证第三版条款,重新发布或修改它;许可证应使用第三版或(按您的选择)任何其更新的版本。')
|
||||
self.addText(
|
||||
'教学工具箱是以希望它有用为目的而发布的,但不附带任何担保;甚至没有适销性或特定用途适用性的隐含担保。请参看 GNU GPL 第三版了解更详细的内容。')
|
||||
self.addTextWithLink('您应该已收到一份 GNU 通用公共许可证的副本;如果没有,请查看<',
|
||||
'https://www.gnu.org/licenses/gpl-3.0.html')
|
||||
|
||||
self.vBoxLayout.addLayout(self.lineHBoxLayout)
|
||||
self.vBoxLayout.addWidget(CardSeparator(self))
|
||||
self.vBoxLayout.addLayout(self.textVBoxLayout)
|
||||
self.viewLayout.addLayout(self.vBoxLayout)
|
||||
|
||||
def addLine(self, title: str, content: str):
|
||||
hBox = QHBoxLayout(self)
|
||||
mTitlte = BodyLabel(title, self)
|
||||
mContent = BodyLabel(content, self)
|
||||
mContent.setTextColor(QColor(96, 96, 96), QColor(206, 206, 206))
|
||||
mTitlte.setFixedWidth(100)
|
||||
|
||||
hBox.addWidget(mTitlte)
|
||||
hBox.addWidget(mContent)
|
||||
|
||||
self.lineVBoxLayout.addLayout(hBox)
|
||||
|
||||
def addLineUseLink(self, title: str, content: str):
|
||||
hBox = QHBoxLayout(self)
|
||||
mTitle = BodyLabel(title, self)
|
||||
mContent = HyperlinkLabel(content, content)
|
||||
mTitle.setFixedWidth(100)
|
||||
|
||||
hBox.addWidget(mTitle)
|
||||
hBox.addWidget(mContent)
|
||||
|
||||
self.lineVBoxLayout.addLayout(hBox)
|
||||
mContent.clicked.connect(lambda: QDesktopServices.openUrl(content))
|
||||
|
||||
def addText(self, text: str):
|
||||
label = BodyLabel(text, self)
|
||||
label.setWordWrap(True)
|
||||
self.textVBoxLayout.addWidget(label)
|
||||
|
||||
def addLink(self, text: str, url: str):
|
||||
link = HyperlinkLabel(url, text)
|
||||
link.setUrl(url)
|
||||
self.textVBoxLayout.addWidget(link)
|
||||
|
||||
def addTextWithLink(self, text: str, url: str):
|
||||
hBox = QHBoxLayout(self)
|
||||
label = BodyLabel(text, self)
|
||||
link = HyperlinkLabel(url, url)
|
||||
link.setContentsMargins(0, 0, 0, 0)
|
||||
hBox.addWidget(label)
|
||||
hBox.addWidget(link)
|
||||
hBox.addWidget(BodyLabel(">。", self))
|
||||
self.textVBoxLayout.addLayout(hBox)
|
||||
link.clicked.connect(lambda: QDesktopServices.openUrl(url))
|
||||
|
||||
|
||||
class AboutMain(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.logoImage = ImageLabel(':/images/logo.png')
|
||||
self.logoImage.scaledToHeight(100)
|
||||
self.appNameLabel = TitleLabel('教学工具箱 🛠️')
|
||||
|
||||
self.hBox = QHBoxLayout()
|
||||
self.hBox.addWidget(self.logoImage, 0, Qt.AlignLeft)
|
||||
self.hBox.addWidget(self.appNameLabel, 1, Qt.AlignLeft)
|
||||
|
||||
third_parties = [
|
||||
ThirdParty("PySide6", "https://qt.io", ":/images/3rd/qt.png"),
|
||||
ThirdParty("QFluentWidgets", "https://qfluentwidgets.com", ":/images/3rd/qfluentwidgets.png"),
|
||||
ThirdParty("openpyxl", "https://openpyxl.readthedocs.io/en/stable"),
|
||||
ThirdParty("python-docx", "https://github.com/python-openxml/python-docx"),
|
||||
ThirdParty("Matplotlib", "https://matplotlib.org", ":/images/3rd/matplotlib.svg"),
|
||||
ThirdParty("packaging", "https://github.com/pypa/packaging", ":/images/3rd/packaging.png"),
|
||||
ThirdParty("pywin32", "https://github.com/mhammond/pywin32")
|
||||
]
|
||||
third_parties.sort(key=lambda item: item.name.lower())
|
||||
|
||||
self.group_card = GroupHeaderCardWidget(self)
|
||||
self.group_card.setTitle("第三方框架")
|
||||
self.vbox = QVBoxLayout(self)
|
||||
|
||||
self.vbox.addLayout(self.hBox)
|
||||
self.vbox.addWidget(AboutCard(self))
|
||||
self.vbox.addWidget(self.group_card)
|
||||
self.vbox.addStretch(1)
|
||||
|
||||
[self.addThirdParty(x) for x in third_parties]
|
||||
|
||||
def addThirdParty(self, third_party: ThirdParty):
|
||||
button = PushButton(FluentIcon.LINK, "访问网站")
|
||||
button.setFixedWidth(120)
|
||||
self.group_card.addGroup(third_party.qrc if third_party.qrc else FluentIcon.LAYOUT, third_party.name,
|
||||
third_party.url, button)
|
||||
button.clicked.connect(lambda: QDesktopServices.openUrl(third_party.url))
|
||||
|
||||
|
||||
class AboutWidget(Widget):
|
||||
def __init__(self, key: str, parent=None):
|
||||
super().__init__(key, parent)
|
||||
|
||||
self.logoImage = ImageLabel(':/images/logo.png')
|
||||
self.logoImage.scaledToHeight(100)
|
||||
self.appNameLabel = TitleLabel('建工工具箱🛠️')
|
||||
self.scrollArea = SingleDirectionScrollArea(orient=Qt.Vertical)
|
||||
self.scrollArea.setWidget(AboutMain(self))
|
||||
self.scrollArea.setWidgetResizable(True)
|
||||
self.scrollArea.enableTransparentBackground()
|
||||
|
||||
self.hBox = QHBoxLayout()
|
||||
self.hBox.addWidget(self.logoImage, 0, Qt.AlignLeft)
|
||||
self.hBox.addWidget(self.appNameLabel, 1, Qt.AlignLeft)
|
||||
|
||||
self.version_card = PrimaryPushSettingCard(
|
||||
text="获取源码",
|
||||
icon=FluentIcon.INFO,
|
||||
title="关于",
|
||||
content="作者:许方杰。当前版本:1.0.0\n本软件使用 GPLv3 开源协议进行分发,作者不对使用本软件造成的任何损失负责。"
|
||||
)
|
||||
self.button_list = [
|
||||
PushButton("访问网站"),
|
||||
PushButton("访问网站"),
|
||||
PushButton("访问网站"),
|
||||
PushButton("访问网站"),
|
||||
]
|
||||
self.url_list = [
|
||||
"https://qt.io",
|
||||
"https://qfluentwidgets.com",
|
||||
"https://openpyxl.readthedocs.io/en/stable",
|
||||
"https://github.com/python-openxml/python-docx"
|
||||
]
|
||||
self.group_card = GroupHeaderCardWidget(self)
|
||||
self.group_card.setTitle("第三方框架")
|
||||
self.vbox = QVBoxLayout(self)
|
||||
|
||||
self.vbox.addLayout(self.hBox)
|
||||
self.vbox.addWidget(self.version_card)
|
||||
self.vbox.addWidget(self.group_card)
|
||||
self.vbox.addStretch(1)
|
||||
|
||||
self.group_card.addGroup("", "PySide6", self.url_list[0], self.button_list[0])
|
||||
self.group_card.addGroup("", "QFluentWidgets", self.url_list[1], self.button_list[1])
|
||||
self.group_card.addGroup("", "openpyxl", self.url_list[2], self.button_list[2])
|
||||
self.group_card.addGroup("", "python-docx", self.url_list[3], self.button_list[3])
|
||||
|
||||
self.version_card.clicked.connect(
|
||||
lambda: QDesktopServices.openUrl("https://cantyonion.site/git/cantyonion/DefenseTopicGenerator")
|
||||
)
|
||||
self.button_list[0].clicked.connect(lambda: QDesktopServices.openUrl(self.url_list[0]))
|
||||
self.button_list[1].clicked.connect(lambda: QDesktopServices.openUrl(self.url_list[1]))
|
||||
self.button_list[2].clicked.connect(lambda: QDesktopServices.openUrl(self.url_list[2]))
|
||||
self.button_list[3].clicked.connect(lambda: QDesktopServices.openUrl(self.url_list[3]))
|
||||
self.vBox = QVBoxLayout(self)
|
||||
self.vBox.setContentsMargins(0, 0, 0, 0)
|
||||
self.vBox.setSpacing(0)
|
||||
self.vBox.addWidget(self.scrollArea)
|
||||
|
||||
@@ -1,19 +1,36 @@
|
||||
# Copyright (c) 2025-2026 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
import os
|
||||
from functools import wraps
|
||||
from typing import Callable, Literal
|
||||
|
||||
from PySide6.QtCore import Qt, Signal, QThread
|
||||
from PySide6.QtWidgets import QVBoxLayout, QFileDialog, QHBoxLayout
|
||||
from qfluentwidgets import GroupHeaderCardWidget, FluentIcon, PushButton, LineEdit, IconWidget, InfoBarIcon, BodyLabel, \
|
||||
PrimaryPushButton, SwitchButton
|
||||
from qfluentwidgets import GroupHeaderCardWidget, FluentIcon, PushButton, LineEdit, IconWidget, BodyLabel, \
|
||||
PrimaryPushButton, SwitchButton, HyperlinkButton, InfoBar, InfoBarPosition
|
||||
|
||||
from module import LOGLEVEL
|
||||
from module.worker import ARGWorker
|
||||
from ui import MAIN_THEME_COLOR
|
||||
from ui.components.infobar import ProgressInfoBar
|
||||
from ui.components.widget import Widget
|
||||
from ui.components.widget import Widget, MyGroupHeaderCardWidget
|
||||
from utils.function import open_template
|
||||
|
||||
|
||||
class InputSettingCard(GroupHeaderCardWidget):
|
||||
class InputSettingCard(MyGroupHeaderCardWidget):
|
||||
chooseSignal = Signal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
@@ -21,15 +38,20 @@ class InputSettingCard(GroupHeaderCardWidget):
|
||||
|
||||
self.setTitle("输入选项")
|
||||
self.setBorderRadius(8)
|
||||
self.btnHBoxLayout = QHBoxLayout(self)
|
||||
|
||||
self.openTemplateButton = HyperlinkButton("", "模板下载")
|
||||
self.chooseFileButton = PushButton("打开")
|
||||
|
||||
self.chooseFileButton.setFixedWidth(120)
|
||||
self.btnHBoxLayout.addWidget(self.openTemplateButton)
|
||||
self.btnHBoxLayout.addWidget(self.chooseFileButton)
|
||||
|
||||
self.inputGroup = self.addGroup(FluentIcon.DOCUMENT, "目标文件", "选择达成度计算表", self.chooseFileButton)
|
||||
self.inputGroup = self.addGroup(FluentIcon.DOCUMENT, "目标文件", "选择达成度计算表", self.btnHBoxLayout)
|
||||
|
||||
# ============================
|
||||
self.chooseFileButton.clicked.connect(self.choose_file)
|
||||
self.openTemplateButton.clicked.connect(lambda: open_template('template-achievement-file.xlsm', self))
|
||||
|
||||
def choose_file(self):
|
||||
file_path, _ = QFileDialog.getOpenFileName(self, "选择文件", "", "Excel 文件 (*.xlsm);")
|
||||
@@ -56,12 +78,13 @@ class OutputSettingCard(GroupHeaderCardWidget):
|
||||
|
||||
self.autoOpenSwitch.setChecked(True)
|
||||
self.bottomLayout = QHBoxLayout()
|
||||
self.hintIcon = IconWidget(InfoBarIcon.INFORMATION)
|
||||
self.hintIcon = IconWidget(FluentIcon.INFO.icon(color=MAIN_THEME_COLOR))
|
||||
self.hintLabel = BodyLabel("点击开始按钮以开始生成 👉")
|
||||
self.startButton.setEnabled(False)
|
||||
|
||||
# 设置底部工具栏布局
|
||||
self.hintIcon.setFixedSize(16, 16)
|
||||
self.hintIcon.autoFillBackground()
|
||||
self.bottomLayout.setSpacing(10)
|
||||
self.bottomLayout.setContentsMargins(24, 15, 24, 20)
|
||||
self.bottomLayout.addWidget(self.hintIcon, 0, Qt.AlignLeft)
|
||||
@@ -236,3 +259,23 @@ class AchievementWidget(Widget):
|
||||
def show_info(self, content: str, level: str):
|
||||
if level == LOGLEVEL.INFO:
|
||||
self.pib.set_title(content)
|
||||
elif level == LOGLEVEL.WARNING:
|
||||
InfoBar.warning(
|
||||
title='提示',
|
||||
content=content,
|
||||
orient=Qt.Horizontal,
|
||||
isClosable=True,
|
||||
position=InfoBarPosition.TOP_RIGHT,
|
||||
duration=5000,
|
||||
parent=self
|
||||
)
|
||||
elif level == LOGLEVEL.ERROR:
|
||||
InfoBar.error(
|
||||
title='错误',
|
||||
content=content,
|
||||
orient=Qt.Horizontal,
|
||||
isClosable=True,
|
||||
position=InfoBarPosition.TOP_RIGHT,
|
||||
duration=-1,
|
||||
parent=self
|
||||
)
|
||||
|
||||
@@ -1,17 +1,60 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
from functools import wraps
|
||||
from typing import Literal, Callable
|
||||
|
||||
from PySide6.QtCore import Qt, Signal, QThread
|
||||
from PySide6.QtWidgets import QHBoxLayout, QVBoxLayout, QFileDialog
|
||||
from qfluentwidgets import GroupHeaderCardWidget, PushButton, IconWidget, InfoBarIcon, \
|
||||
BodyLabel, PrimaryPushButton, FluentIcon, LineEdit
|
||||
from PySide6.QtCore import Qt, Signal, QThread, QEvent
|
||||
from PySide6.QtWidgets import QHBoxLayout, QVBoxLayout, QFileDialog, QButtonGroup, QWidget, QApplication, QStackedWidget
|
||||
from qfluentwidgets import GroupHeaderCardWidget, PushButton, IconWidget, BodyLabel, PrimaryPushButton, FluentIcon, \
|
||||
LineEdit, RadioButton, HyperlinkButton, FlyoutViewBase, TeachingTip, TeachingTipTailPosition, SegmentedWidget, \
|
||||
SimpleCardWidget, DisplayLabel
|
||||
|
||||
from module.worker import DTGWorker
|
||||
from ui import MAIN_THEME_COLOR
|
||||
from ui.components.infobar import ProgressInfoBar
|
||||
from ui.components.widget import Widget
|
||||
from ui.components.widget import Widget, MyGroupHeaderCardWidget, NotImplementedWidget
|
||||
from ui.pyui.sub.defense import ODModeExportSettings, ODModeSettings
|
||||
from utils.function import open_template
|
||||
|
||||
|
||||
class InitSettingCard(GroupHeaderCardWidget):
|
||||
class ChooseTemplateView(FlyoutViewBase):
|
||||
closed = Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.vBoxLayout = QVBoxLayout(self)
|
||||
QApplication.instance().installEventFilter(self)
|
||||
|
||||
def paintEvent(self, e):
|
||||
...
|
||||
|
||||
def eventFilter(self, watched, event):
|
||||
if event.type() == QEvent.MouseButtonPress:
|
||||
if not self.rect().contains(self.mapFromGlobal(event.globalPosition().toPoint())):
|
||||
self.closed.emit()
|
||||
return super().eventFilter(watched, event)
|
||||
|
||||
def addTemplate(self, content: str, cb: Callable[[], None]):
|
||||
label = HyperlinkButton("", content)
|
||||
self.vBoxLayout.addWidget(label)
|
||||
label.clicked.connect(cb)
|
||||
label.clicked.connect(self.closed.emit)
|
||||
|
||||
|
||||
class InitSettingCard(MyGroupHeaderCardWidget):
|
||||
chooseSignal = Signal(str, str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
@@ -20,19 +63,29 @@ class InitSettingCard(GroupHeaderCardWidget):
|
||||
self.setTitle("输入选项")
|
||||
self.setBorderRadius(8)
|
||||
|
||||
self.sBtnHBoxLayout = QHBoxLayout(self)
|
||||
self.qBtnHBoxLayout = QHBoxLayout(self)
|
||||
self.sTemplateButton = HyperlinkButton("", "模板下载")
|
||||
self.chooseStudentButton = PushButton("打开")
|
||||
self.qTemplateButton = HyperlinkButton("", "模板下载")
|
||||
self.chooseQuestionButton = PushButton("打开")
|
||||
|
||||
self.chooseStudentButton.setFixedWidth(120)
|
||||
self.chooseQuestionButton.setFixedWidth(120)
|
||||
self.sBtnHBoxLayout.addWidget(self.sTemplateButton)
|
||||
self.sBtnHBoxLayout.addWidget(self.chooseStudentButton)
|
||||
self.qBtnHBoxLayout.addWidget(self.qTemplateButton)
|
||||
self.qBtnHBoxLayout.addWidget(self.chooseQuestionButton)
|
||||
|
||||
self.stuGroup = self.addGroup(FluentIcon.DOCUMENT, "学生名单", "选择学生名单文件", self.chooseStudentButton)
|
||||
self.QueGroup = self.addGroup(FluentIcon.DOCUMENT, "题库", "选择题库文件", self.chooseQuestionButton)
|
||||
self.stuGroup = self.addGroup(FluentIcon.DOCUMENT, "学生名单", "选择学生名单文件", self.sBtnHBoxLayout)
|
||||
self.QueGroup = self.addGroup(FluentIcon.DOCUMENT, "题库", "选择题库文件", self.qBtnHBoxLayout)
|
||||
|
||||
self.chooseStudentButton.clicked.connect(
|
||||
lambda: self.choose_file(self.stuGroup.setContent, "已选择文件:", lambda x: self.chooseSignal.emit('s', x)))
|
||||
self.chooseQuestionButton.clicked.connect(
|
||||
lambda: self.choose_file(self.QueGroup.setContent, "已选择文件:", lambda x: self.chooseSignal.emit('q', x)))
|
||||
self.qTemplateButton.clicked.connect(lambda: open_template('template-defense-paper-questions.xlsm', self))
|
||||
self.sTemplateButton.clicked.connect(self.show_template_list_view)
|
||||
|
||||
def choose_file(
|
||||
self,
|
||||
@@ -46,6 +99,19 @@ class InitSettingCard(GroupHeaderCardWidget):
|
||||
if cb:
|
||||
cb(file_path)
|
||||
|
||||
def show_template_list_view(self):
|
||||
view = ChooseTemplateView(self)
|
||||
view.addTemplate("普通模板", lambda: open_template("template-defense-paper-student-1.xlsm", self))
|
||||
view.addTemplate("达成度模板", lambda: open_template("template-defense-paper-student-2.xlsm", self))
|
||||
w = TeachingTip.make(
|
||||
target=self.sTemplateButton,
|
||||
view=view,
|
||||
tailPosition=TeachingTipTailPosition.TOP,
|
||||
duration=-1,
|
||||
parent=self
|
||||
)
|
||||
view.closed.connect(w.close)
|
||||
|
||||
|
||||
class ExportSettingsCard(GroupHeaderCardWidget):
|
||||
startSignal = Signal()
|
||||
@@ -54,14 +120,24 @@ class ExportSettingsCard(GroupHeaderCardWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setTitle("输入选项")
|
||||
self.setTitle("输出选项")
|
||||
self.setBorderRadius(8)
|
||||
|
||||
self.chooseExportDirectoryButton = PushButton("选择")
|
||||
self.exportFileNameLineEdit = LineEdit()
|
||||
self.startButton = PrimaryPushButton(FluentIcon.PLAY_SOLID, "开始")
|
||||
self.pdfRadio = RadioButton("PDF")
|
||||
self.wordRadio = RadioButton("Word")
|
||||
self.radioWidget = QWidget(self)
|
||||
self.radioHbox = QHBoxLayout(self.radioWidget)
|
||||
self.radioGroup = QButtonGroup(self.radioWidget)
|
||||
|
||||
self.hintIcon = IconWidget(InfoBarIcon.INFORMATION)
|
||||
self.radioGroup.addButton(self.pdfRadio)
|
||||
self.radioGroup.addButton(self.wordRadio)
|
||||
self.radioHbox.addWidget(self.pdfRadio)
|
||||
self.radioHbox.addWidget(self.wordRadio)
|
||||
self.wordRadio.setChecked(True)
|
||||
self.hintIcon = IconWidget(FluentIcon.INFO.icon(color=MAIN_THEME_COLOR))
|
||||
self.hintLabel = BodyLabel("点击开始按钮以开始生成 👉")
|
||||
self.chooseExportDirectoryButton.setFixedWidth(120)
|
||||
self.startButton.setFixedWidth(120)
|
||||
@@ -84,7 +160,9 @@ class ExportSettingsCard(GroupHeaderCardWidget):
|
||||
self.chooseExportDirectoryButton)
|
||||
self.fnGroup = self.addGroup(FluentIcon.DOCUMENT, "导出文件名", "输入导出文件的名称",
|
||||
self.exportFileNameLineEdit)
|
||||
self.fnGroup.setSeparatorVisible(True)
|
||||
self.exportFormatGroup = self.addGroup(FluentIcon.DOCUMENT, "导出文件格式", "选择导出文件的格式",
|
||||
self.radioWidget)
|
||||
self.exportFormatGroup.setSeparatorVisible(True)
|
||||
|
||||
self.vBoxLayout.addLayout(self.bottomLayout)
|
||||
|
||||
@@ -108,15 +186,16 @@ class ExportSettingsCard(GroupHeaderCardWidget):
|
||||
self.updateSignal.emit('n', f_name)
|
||||
|
||||
|
||||
class DefenseWidget(Widget):
|
||||
class DPMode(QWidget):
|
||||
errorSignal = Signal(str, str)
|
||||
|
||||
def __init__(self, key: str, parent=None):
|
||||
super().__init__(key, parent)
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.initCard = InitSettingCard(self)
|
||||
self.exportCard = ExportSettingsCard(self)
|
||||
self.vbox = QVBoxLayout(self)
|
||||
self.vbox.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.vbox.addWidget(self.initCard)
|
||||
self.vbox.addWidget(self.exportCard)
|
||||
@@ -147,12 +226,12 @@ class DefenseWidget(Widget):
|
||||
self.pib.set_title('请稍后')
|
||||
|
||||
def set_pb_value(self, value: int) -> None:
|
||||
self.pib.set_progress(value)
|
||||
if value == 100:
|
||||
self.pib.set_progress(101)
|
||||
self.pib.set_title('正在转换文件')
|
||||
elif value == -1:
|
||||
if value == -1:
|
||||
self.successFlag = False
|
||||
self.pib.set_progress(value)
|
||||
|
||||
def set_pb_msg(self, value: str) -> None:
|
||||
self.pib.set_title(value)
|
||||
|
||||
def enable_start_check(func: Callable):
|
||||
@wraps(func)
|
||||
@@ -203,7 +282,8 @@ class DefenseWidget(Widget):
|
||||
self.input_student_filepath,
|
||||
self.input_question_filepath,
|
||||
self.output_filepath,
|
||||
self.output_filename
|
||||
self.output_filename,
|
||||
self.exportCard.radioGroup.checkedButton().text().lower()
|
||||
)
|
||||
self.worker.moveToThread(self.thread)
|
||||
|
||||
@@ -213,7 +293,8 @@ class DefenseWidget(Widget):
|
||||
|
||||
# 线程启动与信号连接
|
||||
self.thread.started.connect(self.worker.run)
|
||||
self.worker.progress.connect(self.set_pb_value)
|
||||
self.worker.progress[int].connect(self.set_pb_value)
|
||||
self.worker.progress[str].connect(self.set_pb_msg)
|
||||
self.worker.error.connect(self.show_error)
|
||||
|
||||
self.worker.finished.connect(self.thread.quit)
|
||||
@@ -238,3 +319,53 @@ class DefenseWidget(Widget):
|
||||
|
||||
def show_error(self, title: str, content: str):
|
||||
self.errorSignal.emit(title, content)
|
||||
|
||||
|
||||
class DOMode(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.vbox = QVBoxLayout(self)
|
||||
self.vbox.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.odSettings = ODModeSettings(self)
|
||||
self.odExportSettings = ODModeExportSettings(self)
|
||||
|
||||
self.vbox.addWidget(self.odSettings)
|
||||
self.vbox.addWidget(self.odExportSettings)
|
||||
self.vbox.addStretch(1)
|
||||
|
||||
|
||||
class DefenseWidget(Widget):
|
||||
errorSignal = Signal(str, str)
|
||||
|
||||
def __init__(self, key: str, parent=None):
|
||||
super().__init__(key, parent)
|
||||
|
||||
self.vbox = QVBoxLayout(self)
|
||||
self.stack = QStackedWidget(self)
|
||||
self.menu = SegmentedWidget(self)
|
||||
self.dpMode = DPMode(self)
|
||||
self.doMode = NotImplementedWidget(self)
|
||||
|
||||
self.addSubInterface(self.dpMode, 'DPMode', '书面答辩')
|
||||
self.addSubInterface(self.doMode, 'DOMode', '口头答辩')
|
||||
|
||||
self.menu.setCurrentItem('DPMode')
|
||||
self.vbox.addWidget(self.menu)
|
||||
self.vbox.addWidget(self.stack)
|
||||
self.vbox.addStretch(1)
|
||||
|
||||
def addSubInterface(self, widget: QWidget, objectName: str, text: str):
|
||||
widget.setObjectName(objectName)
|
||||
self.stack.addWidget(widget)
|
||||
|
||||
# 使用全局唯一的 objectName 作为路由键
|
||||
self.menu.addItem(
|
||||
routeKey=objectName,
|
||||
text=text,
|
||||
onClick=lambda: self.stack.setCurrentWidget(widget)
|
||||
)
|
||||
|
||||
def onCurrentIndexChanged(self, index):
|
||||
widget = self.stack.widget(index)
|
||||
self.menu.setCurrentItem(widget.objectName())
|
||||
|
||||
151
ui/pyui/picker_ui.py
Normal file
@@ -0,0 +1,151 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
from PySide6.QtCore import Qt, Signal, QTimer
|
||||
from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout, QWidget, QFileDialog
|
||||
from qfluentwidgets import PushButton, FluentIcon, PrimaryPushButton, IconWidget, BodyLabel, \
|
||||
SpinBox, HyperlinkButton
|
||||
|
||||
from module.picker.schema import PickerExcel, PickerStudent
|
||||
from ui import MAIN_THEME_COLOR
|
||||
from ui.components.widget import Widget, MyGroupHeaderCardWidget
|
||||
from ui.pyui.sub.picker import PickStudentLabelUi
|
||||
from utils.function import open_template
|
||||
|
||||
|
||||
class PickStudentMode(QWidget):
|
||||
errorSignal = Signal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.card = MyGroupHeaderCardWidget(self)
|
||||
self.vbox = QVBoxLayout(self)
|
||||
self.vbox.setContentsMargins(0, 0, 0, 0)
|
||||
self.btnHBox = QHBoxLayout(self)
|
||||
|
||||
self.openTemplateBtn = HyperlinkButton("", "模板下载")
|
||||
self.chooseBtn = PushButton("打开")
|
||||
self.startButton = PrimaryPushButton(FluentIcon.PLAY_SOLID, "开始")
|
||||
self.bottomLayout = QHBoxLayout()
|
||||
self.hintIcon = IconWidget(FluentIcon.INFO.icon(color=MAIN_THEME_COLOR))
|
||||
self.hintLabel = BodyLabel("点击开始按钮以开始抽签 👉")
|
||||
self.spinbox = SpinBox()
|
||||
self.psui = PickStudentLabelUi(self)
|
||||
|
||||
self.card.setTitle("输入选项")
|
||||
self.chooseBtn.setFixedWidth(120)
|
||||
self.startButton.setFixedWidth(120)
|
||||
self.startButton.setEnabled(False)
|
||||
self.spinbox.setRange(0, 6)
|
||||
self.spinbox.setFixedWidth(120)
|
||||
self.spinbox.setEnabled(False)
|
||||
self.psui.hide()
|
||||
|
||||
self.hintIcon.setFixedSize(16, 16)
|
||||
self.hintIcon.autoFillBackground()
|
||||
self.bottomLayout.setSpacing(10)
|
||||
self.bottomLayout.setContentsMargins(24, 15, 24, 20)
|
||||
self.bottomLayout.addWidget(self.hintIcon, 0, Qt.AlignLeft)
|
||||
self.bottomLayout.addWidget(self.hintLabel, 0, Qt.AlignLeft)
|
||||
self.bottomLayout.addStretch(1)
|
||||
self.bottomLayout.addWidget(self.startButton, 0, Qt.AlignRight)
|
||||
self.bottomLayout.setAlignment(Qt.AlignVCenter)
|
||||
self.btnHBox.addWidget(self.openTemplateBtn)
|
||||
self.btnHBox.addWidget(self.chooseBtn)
|
||||
|
||||
self.group = self.card.addGroup(FluentIcon.DOCUMENT, "学生名单", "选择学生名单", self.btnHBox)
|
||||
self.spinGroup = self.card.addGroup(FluentIcon.SETTING, "提问次数", "设置提问的最大次数", self.spinbox)
|
||||
self.spinGroup.setSeparatorVisible(True)
|
||||
self.card.vBoxLayout.addLayout(self.bottomLayout)
|
||||
|
||||
self.vbox.addWidget(self.card)
|
||||
self.vbox.addWidget(self.psui)
|
||||
self.vbox.addStretch(1)
|
||||
|
||||
# ==============================
|
||||
self.chooseBtn.clicked.connect(self.choose_file)
|
||||
self.spinbox.valueChanged.connect(lambda: PickerExcel.save_total_time(value=self.spinbox.value()))
|
||||
self.startButton.clicked.connect(self.start_rolling)
|
||||
self.psui.rollingText.finishSignal.connect(self.finish_rolling)
|
||||
self.openTemplateBtn.clicked.connect(lambda: open_template("template-pick-student.xlsm", self))
|
||||
# ==============================
|
||||
self.filepath = ""
|
||||
self.students = []
|
||||
|
||||
def choose_file(self):
|
||||
file_path, _ = QFileDialog.getOpenFileName(self, "选择文件", "", "Excel 文件 (*.xlsm);")
|
||||
if file_path:
|
||||
self.group.setContent("已选择文件:" + file_path)
|
||||
self.filepath = file_path
|
||||
self.startButton.setEnabled(True)
|
||||
self.init_spinbox_value()
|
||||
|
||||
def init_spinbox_value(self):
|
||||
if not self.filepath:
|
||||
return
|
||||
|
||||
try:
|
||||
PickerExcel.open(self.filepath)
|
||||
self.spinbox.setValue(PickerExcel.read_total_time())
|
||||
self.spinbox.setEnabled(True)
|
||||
except Exception as e:
|
||||
self.errorSignal.emit(str(e))
|
||||
self.spinbox.setEnabled(False)
|
||||
self.startButton.setEnabled(False)
|
||||
|
||||
def start_rolling(self):
|
||||
self.students = PickerExcel.read_student()
|
||||
self.psui.show()
|
||||
self.psui.rollingText.set_items(self.students)
|
||||
self.psui.rollingText.start_rolling()
|
||||
self.startButton.setEnabled(False)
|
||||
|
||||
def finish_rolling(self):
|
||||
stu = PickerStudent.pick(self.students)
|
||||
if not (stu.so and stu.name):
|
||||
self.errorSignal.emit("学生信息读取失败")
|
||||
self.psui.rollingText.show_result(stu)
|
||||
|
||||
timer = QTimer(self)
|
||||
timer.setSingleShot(True)
|
||||
timer.timeout.connect(lambda: self.show_screen(stu))
|
||||
timer.start(1000)
|
||||
|
||||
def show_screen(self, stu: PickerStudent):
|
||||
self.psui.show_scoring()
|
||||
self.psui.scoring.submitSignal.connect(lambda score: self.scoring_finished(score, stu))
|
||||
|
||||
def scoring_finished(self, score: int, student: PickerStudent):
|
||||
student.append_score(score)
|
||||
PickerExcel.write_back(student)
|
||||
self.psui.hide()
|
||||
self.startButton.setEnabled(True)
|
||||
|
||||
|
||||
class PickerWidget(Widget):
|
||||
errorSignal = Signal(str, str)
|
||||
|
||||
def __init__(self, key: str, parent=None):
|
||||
super().__init__(key, parent)
|
||||
|
||||
self.vbox = QVBoxLayout(self)
|
||||
self.psm = PickStudentMode(self)
|
||||
|
||||
self.vbox.addWidget(self.psm)
|
||||
self.vbox.addStretch(1)
|
||||
|
||||
# ===========================
|
||||
self.psm.errorSignal.connect(lambda n: self.errorSignal.emit("😢 不好出错了", n))
|
||||
88
ui/pyui/sub/defense.py
Normal file
@@ -0,0 +1,88 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
from PySide6.QtWidgets import QHBoxLayout
|
||||
from qfluentwidgets import PushButton, HyperlinkButton, SpinBox, LineEdit, PrimaryPushButton, FluentIcon, \
|
||||
GroupHeaderCardWidget
|
||||
|
||||
from ui.components.widget import MyGroupHeaderCardWidget
|
||||
|
||||
|
||||
class ODModeSettings(MyGroupHeaderCardWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setTitle('输入选项')
|
||||
|
||||
self.sBtn = PushButton('打开', self)
|
||||
self.sBtnTemplate = HyperlinkButton('', '模板下载')
|
||||
|
||||
self.qBtn = PushButton('打开', self)
|
||||
self.bBtnTemplate = HyperlinkButton('', '模板下载')
|
||||
|
||||
self.qNumber = SpinBox(self)
|
||||
self.qNumber.setRange(0, 999)
|
||||
|
||||
self.defenseNameLineEdit = LineEdit(self)
|
||||
self.defenseNameLineEdit.setPlaceholderText('输入答辩名称')
|
||||
|
||||
self.so = SpinBox(self)
|
||||
self.so.setRange(0, 999)
|
||||
|
||||
self.sname = LineEdit(self)
|
||||
self.sname.setPlaceholderText('姓名')
|
||||
|
||||
self.startBtn = PrimaryPushButton(FluentIcon.PLAY_SOLID, '开始')
|
||||
|
||||
self.init_layout()
|
||||
|
||||
def init_layout(self):
|
||||
self.sBtn.setFixedWidth(120)
|
||||
self.qBtn.setFixedWidth(120)
|
||||
self.defenseNameLineEdit.setFixedWidth(360)
|
||||
self.so.setFixedWidth(120)
|
||||
self.sname.setFixedWidth(120)
|
||||
self.qNumber.setFixedWidth(120)
|
||||
self.startBtn.setFixedWidth(120)
|
||||
|
||||
hbox1 = QHBoxLayout(self)
|
||||
hbox1.addWidget(self.sBtnTemplate)
|
||||
hbox1.addWidget(self.sBtn)
|
||||
hbox2 = QHBoxLayout(self)
|
||||
hbox2.addWidget(self.bBtnTemplate)
|
||||
hbox2.addWidget(self.qBtn)
|
||||
|
||||
self.stuGroup = self.addGroup(FluentIcon.DOCUMENT, '学生名单', '选择学生名单文件', hbox1)
|
||||
self.qGroup = self.addGroup(FluentIcon.DOCUMENT, '题库', '选择题库文件', hbox2)
|
||||
self.addGroup(FluentIcon.SETTING, '题目数量', '输入题目数量', self.qNumber)
|
||||
self.addGroup(FluentIcon.SETTING, '答辩名称', '输入答辩名称', self.defenseNameLineEdit)
|
||||
self.addGroup(FluentIcon.SETTING, '答辩序号', '输入答辩序号', self.so)
|
||||
self.addGroup(FluentIcon.SETTING, '学生姓名', '输入学生姓名', self.sname)
|
||||
|
||||
|
||||
class ODModeExportSettings(GroupHeaderCardWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setTitle('导出选项')
|
||||
self.exportFilename = LineEdit(self)
|
||||
self.exportBtn = PrimaryPushButton(FluentIcon.PLAY_SOLID, '导出')
|
||||
|
||||
self.init_layout()
|
||||
|
||||
def init_layout(self):
|
||||
self.exportFilename.setPlaceholderText('输入导出的文件名')
|
||||
self.exportFilename.setFixedWidth(360)
|
||||
self.exportBtn.setFixedWidth(120)
|
||||
|
||||
self.addGroup(FluentIcon.DOCUMENT, '导出文件名', '输入导出文件名', self.exportFilename)
|
||||
119
ui/pyui/sub/picker.py
Normal file
@@ -0,0 +1,119 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
import sys
|
||||
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QApplication, QGridLayout
|
||||
from qfluentwidgets import PushButton, SpinBox, PrimaryPushButton, \
|
||||
BodyLabel, SimpleCardWidget
|
||||
from qfluentwidgets.components.widgets.card_widget import CardSeparator
|
||||
|
||||
from ui.components.widget import RollingTextWidget
|
||||
|
||||
|
||||
class QuickScoringKeyBoard(QWidget):
|
||||
scoringSignal = Signal(int)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.vBoxLayout = QVBoxLayout(self)
|
||||
self.keyBoardLayout = QGridLayout()
|
||||
buttons = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 100]
|
||||
|
||||
for i, num in enumerate(buttons):
|
||||
row = i // 3
|
||||
col = i % 3
|
||||
btn = PushButton(str(num))
|
||||
btn.setFixedWidth(120)
|
||||
btn.clicked.connect(lambda _, n=num: self.scoringSignal.emit(n))
|
||||
self.keyBoardLayout.addWidget(btn, row, col)
|
||||
|
||||
self.vBoxLayout.addLayout(self.keyBoardLayout)
|
||||
|
||||
|
||||
class QuickScoring(QWidget):
|
||||
submitSignal = Signal(int)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.hBoxLayout = QHBoxLayout(self)
|
||||
self.btnVBoxLayout = QVBoxLayout(self)
|
||||
self.spinboxLayout = QHBoxLayout(self)
|
||||
self.spinboxLabel = BodyLabel("得分", self)
|
||||
self.gradeSpinBox = SpinBox(self)
|
||||
self.submitButton = PrimaryPushButton("确定", self)
|
||||
self.resetButton = PushButton("重置", self)
|
||||
self.keyboard = QuickScoringKeyBoard(self)
|
||||
|
||||
self.gradeSpinBox.setRange(0, 100)
|
||||
self.gradeSpinBox.setFixedWidth(150)
|
||||
self.submitButton.setFixedWidth(120)
|
||||
self.resetButton.setFixedWidth(120)
|
||||
|
||||
self.spinboxLayout.addWidget(self.spinboxLabel)
|
||||
self.spinboxLayout.addWidget(self.gradeSpinBox)
|
||||
|
||||
self.btnVBoxLayout.addStretch()
|
||||
self.btnVBoxLayout.addWidget(self.submitButton)
|
||||
self.btnVBoxLayout.addWidget(self.resetButton)
|
||||
self.btnVBoxLayout.addStretch()
|
||||
|
||||
self.hBoxLayout.addStretch()
|
||||
self.hBoxLayout.addLayout(self.spinboxLayout)
|
||||
self.hBoxLayout.addLayout(self.btnVBoxLayout)
|
||||
self.hBoxLayout.addWidget(self.keyboard)
|
||||
self.hBoxLayout.addStretch()
|
||||
|
||||
self.keyboard.scoringSignal.connect(lambda x: self.gradeSpinBox.setValue(x))
|
||||
self.resetButton.clicked.connect(lambda: self.gradeSpinBox.clear())
|
||||
self.submitButton.clicked.connect(lambda: self.submitSignal.emit(self.gradeSpinBox.value()))
|
||||
|
||||
|
||||
class PickStudentLabelUi(SimpleCardWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.vBoxLayout = QVBoxLayout(self)
|
||||
self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.rollingText = RollingTextWidget(self)
|
||||
self.scoring = QuickScoring(self)
|
||||
self.separator = CardSeparator(self)
|
||||
|
||||
self.scoring.hide()
|
||||
self.separator.hide()
|
||||
|
||||
self.vBoxLayout.addWidget(self.rollingText)
|
||||
self.vBoxLayout.addWidget(self.separator)
|
||||
self.vBoxLayout.addWidget(self.scoring)
|
||||
self.vBoxLayout.addStretch()
|
||||
|
||||
def show_scoring(self):
|
||||
self.scoring.show()
|
||||
self.separator.show()
|
||||
|
||||
def hideEvent(self, event, /):
|
||||
super().hideEvent(event)
|
||||
self.scoring.gradeSpinBox.clear()
|
||||
self.scoring.hide()
|
||||
self.separator.hide()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = QApplication(sys.argv)
|
||||
window = PickStudentLabelUi()
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
@@ -1,3 +1,18 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QVBoxLayout
|
||||
from qfluentwidgets import PushButton, InfoBarIcon, InfoBarPosition
|
||||
|
||||
@@ -1,9 +1,31 @@
|
||||
import os
|
||||
import sys
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
import io
|
||||
import matplotlib
|
||||
from matplotlib import pyplot as plt
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
from typing import Optional
|
||||
|
||||
import matplotlib
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QWidget
|
||||
from matplotlib import pyplot as plt
|
||||
from qfluentwidgets import InfoBar, InfoBarPosition
|
||||
|
||||
|
||||
def format_ranges(nums):
|
||||
@@ -184,5 +206,35 @@ def resource_path(relative_path: str) -> str:
|
||||
return os.path.join(base_path, relative_path)
|
||||
|
||||
|
||||
def is_frozen() -> bool:
|
||||
return getattr(sys, 'frozen', False)
|
||||
def open_template(file_name: str, widget: QWidget) -> None:
|
||||
"""将模板文件复制到临时目录并打开"""
|
||||
file_path = resource_path("template/" + file_name)
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
raise FileNotFoundError(f"Template file '{file_name}' not found.")
|
||||
|
||||
# 复制到临时目录
|
||||
tmp_dir = tempfile.gettempdir()
|
||||
tmp_file_path = os.path.join(tmp_dir, file_name)
|
||||
shutil.copy(file_path, tmp_file_path)
|
||||
|
||||
# 打开文件
|
||||
if sys.platform.startswith('win'):
|
||||
os.startfile(tmp_file_path)
|
||||
elif sys.platform.startswith('darwin'):
|
||||
os.system(f'open "{tmp_file_path}"')
|
||||
else:
|
||||
os.system(f'xdg-open "{tmp_file_path}"')
|
||||
|
||||
InfoBar.info(
|
||||
title='已打开文件',
|
||||
content="编辑后请将文件另存为,保存至其他目录",
|
||||
orient=Qt.Horizontal,
|
||||
isClosable=True,
|
||||
position=InfoBarPosition.TOP_RIGHT,
|
||||
duration=2000,
|
||||
parent=widget
|
||||
)
|
||||
|
||||
|
||||
RELEASE_ENV = getattr(sys, 'frozen', False)
|
||||
|
||||
40
utils/hook.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
||||
# #
|
||||
# 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/>.
|
||||
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def gen_build_info():
|
||||
try:
|
||||
hash_str = subprocess.check_output(
|
||||
['git', 'rev-parse', '--short', 'HEAD'],
|
||||
stderr=subprocess.DEVNULL
|
||||
).decode('utf-8').strip()
|
||||
except FileNotFoundError:
|
||||
# git 未安装
|
||||
hash_str = 'unknown'
|
||||
except subprocess.CalledProcessError:
|
||||
# 不是 git 仓库(如从压缩包下载)
|
||||
hash_str = 'unknown'
|
||||
|
||||
with open('build_info.py', 'w', encoding='utf-8') as f:
|
||||
f.write(f"# Auto-generated build info\n")
|
||||
f.write(f"BUILD_TIME = '{datetime.now().isoformat(sep=' ', timespec='seconds')}'\n")
|
||||
f.write(f"GIT_HASH = '{hash_str}'\n")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
gen_build_info()
|
||||