forked from postwoman/forTeam
update
This commit is contained in:
parent
b1e43a53a3
commit
acb70d4909
|
@ -1,20 +1,25 @@
|
|||
# ---> Actionscript
|
||||
# Build and Release Folders
|
||||
bin-debug/
|
||||
bin-release/
|
||||
[Oo]bj/
|
||||
[Bb]in/
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
# Other files and folders
|
||||
.settings/
|
||||
|
||||
# Executables
|
||||
*.swf
|
||||
*.air
|
||||
*.ipa
|
||||
*.apk
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Project files, i.e. `.project`, `.actionScriptProperties` and `.flexProperties`
|
||||
# should NOT be excluded as they contain compiler settings and other important
|
||||
# information for Eclipse / Flash Builder.
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
out/
|
|
@ -0,0 +1,78 @@
|
|||
|
||||
## v1.07
|
||||
- 码表选择直接在窗口中完成
|
||||
- 更新 app 图标
|
||||
|
||||
fix:
|
||||
- 自动识别文件换行符 `\r\n`、`\n`
|
||||
- 分组全选无法选择的问题
|
||||
|
||||
|
||||
## v1.06
|
||||
- 设置编码生成参考码表
|
||||
- 批量生成编码
|
||||
- 查看调试窗口
|
||||
- 打开工具配置文件存放文件夹
|
||||
fix:
|
||||
- 编码生成
|
||||
|
||||
|
||||
## v1.05
|
||||
- shift 批量选词
|
||||
- 工具窗口
|
||||
- 设定码表编码词条分隔方式
|
||||
- 设定码表格式:一码多词、一码一词、一词一码
|
||||
- 编辑任意码表文件
|
||||
- 查重
|
||||
- 批量移动到对应码表文件
|
||||
enhance:
|
||||
- 滚动条宽度调整,更方便滚动
|
||||
fix:
|
||||
- ipc 多次响应侦听
|
||||
|
||||
|
||||
## v1.04 `2021-10-16`
|
||||
- 添加配置页面
|
||||
- 指定初始载入码表
|
||||
- 回车键是搜索 | 添加新用户词
|
||||
- 添加|删除 词后是否立即布署
|
||||
- 搜索框清空按钮,相关逻辑
|
||||
|
||||
|
||||
## v1.03
|
||||
- 主码表不显示移至主码表按钮
|
||||
- 根据词条自动生成编码
|
||||
- 词条排序,全部或者单分组
|
||||
- 任意词条 -> 任意位置
|
||||
|
||||
<img width="912" alt="Screen Shot 2021-08-17 at 20 19 31" src="https://user-images.githubusercontent.com/12215982/129724470-ccffd5f4-6177-4291-8f68-02d91a8793a0.png">
|
||||
|
||||
|
||||
## v1.02
|
||||
- 显示程序图标
|
||||
- 关于界面填充
|
||||
- 展示当前码表文件名
|
||||
- `footer` 信息调整
|
||||
- 切换码表保留 code 输入框内容
|
||||
|
||||
<img width="396" alt="Screen Shot 2021-08-10 at 22 19 30" src="https://user-images.githubusercontent.com/12215982/128883939-0a627819-47c4-4729-b5a3-65ce1b2536cf.png">
|
||||
|
||||
|
||||
## v1.01beta2 `2021-08-09`
|
||||
- 分组列表展示
|
||||
- 暗黑模式适配 `macOS`
|
||||
|
||||
<img width="812" alt="Screen Shot 2021-08-09 at 21 35 34" src="https://user-images.githubusercontent.com/12215982/128715114-8e9f82ff-2bdb-4837-87ed-ecbf0ea7ee28.png">
|
||||
<img width="812" alt="Screen Shot 2021-08-09 at 21 35 45" src="https://user-images.githubusercontent.com/12215982/128715098-8dc4c6d6-76f8-4428-9434-e3ac86ab2072.png">
|
||||
|
||||
|
||||
## v1.00beta `2021-08-09`
|
||||
- 展示码表内容
|
||||
- 添加、删除词条
|
||||
- 分组展示词条内容
|
||||
- 移动词条到主码表
|
||||
- 搜索
|
||||
- 批量删除词条,删除分组
|
||||
|
||||
<img width="712" alt="Screen Shot 2021-08-07 at 23 38 13" src="https://user-images.githubusercontent.com/12215982/128605586-47399a2e-c036-4729-aeef-8c67281b45ba.png">
|
||||
|
629
LICENSE
629
LICENSE
|
@ -1,12 +1,621 @@
|
|||
Copyright (C) 2006 by Rob Landley <rob@landley.net>
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose
|
||||
with or without fee is hereby granted.
|
||||
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.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
||||
OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
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
|
||||
|
|
159
README.md
159
README.md
|
@ -1,20 +1,155 @@
|
|||
#### 从命令行创建一个新的仓库
|
||||
<img src="https://user-images.githubusercontent.com/12215982/139462759-e6d8ebc6-180d-4d18-8c3c-68234f0ff1c0.png" width="150" />
|
||||
|
||||
|
||||
# 五笔助手 for [Rime](https://github.com/rime)
|
||||
一个管理 Rime 五笔词库的工具,添加、删除词条,可批量导入外部词库
|
||||
|
||||
有其它问题,欢迎加群讨论: [878750538](https://jq.qq.com/?_wv=1027&k=st8cY2sI)
|
||||
|
||||
|
||||
|
||||
<img width="812" alt="Screen Shot 2021-08-09 at 21 35 34" src="https://user-images.githubusercontent.com/12215982/128715114-8e9f82ff-2bdb-4837-87ed-ecbf0ea7ee28.png">
|
||||
<img width="812" alt="Screen Shot 2021-11-02 at 23 16 34" src="https://user-images.githubusercontent.com/12215982/139876204-aef77bb8-683b-4042-8ec1-f366641eaae5.png">
|
||||
<img width="812" alt="Screen Shot 2021-11-02 at 23 17 27" src="https://user-images.githubusercontent.com/12215982/139876211-00e58bbc-9b49-43f0-83c2-8922109e0660.png">
|
||||
|
||||
## 支持平台:
|
||||
Windows, macOS, (Linux 未测试)
|
||||
|
||||
## 下载
|
||||
|
||||
[> 去往下载页面 <](https://github.com/KyleBing/wubi-dict-editor/releases)
|
||||
|
||||
## 安装
|
||||
|
||||
__windows__ 直接解压打开 `.exe` 文件即可
|
||||
|
||||
__macOS__ 如果提示无法打开,文件损坏什么的,将 app 移到应用程序 `Applications` 文件夹后,打开终端 `Terminal`,这样操作:
|
||||
|
||||
```bash
|
||||
touch README.md
|
||||
git init
|
||||
git add README.md
|
||||
git commit -m "first commit"
|
||||
git remote add origin http://testgitea2.trustie.net/firstTeam-caishi-n/forTeam.git
|
||||
git push -u origin master
|
||||
|
||||
sudo xattr -rd com.apple.quarantine /Applications/五笔助手.app/
|
||||
```
|
||||
|
||||
#### 从命令行推送已经创建的仓库
|
||||
这样应该就能打开了。
|
||||
|
||||
|
||||
## 用到的技术
|
||||
- `nodejs`
|
||||
- `javascript` `scss` `html`
|
||||
- `vue 2` [`electron`](https://github.com/electron/electron)
|
||||
|
||||
## 开发计划
|
||||
|
||||
#### 进程截图记录:
|
||||
> [https://github.com/KyleBing/wubi-dict-editor/discussions/11](https://github.com/KyleBing/wubi-dict-editor/discussions/11)
|
||||
|
||||
#### 纯工具模块
|
||||
- [x] 工具窗口 `2021-10-18`
|
||||
- [x] 设定码表编码词条分隔方式 `\t` `空格` `2021-10-18`
|
||||
- [x] 设定码表格式:一码多词、一码一词、一词一码 `2021-10-18`
|
||||
- [x] 编辑任意码表文件 `2021-10-18`
|
||||
- [x] 批量移动到任意码表文件 `2021-10-22`
|
||||
- [x] 生成不同版本五笔的编码码表,保存 `2021-10-20`
|
||||
- [x] 字数筛选 `2021-10-18`
|
||||
- [x] 查重 `2021-10-20`
|
||||
- [x] 批量添加词条编码 `2021-10-25`
|
||||
- [ ] 编码查错修正
|
||||
|
||||
#### 1. 词条
|
||||
- [x] 展示词库内容 `2021-07-25`
|
||||
- [x] 成组显示 组为以 `##` 开头`2021-07-25`
|
||||
- [x] 搜索词条 `2021-07-26`
|
||||
- [x] 基于编码、内容 `2021-07-29`
|
||||
- [x] 添加自定义短语 `2021-07-26`
|
||||
- [x] 自动生成编码 `2021-08-12`
|
||||
- [x] 删除词条 `2021-07-27`
|
||||
- [x] 批量 `2021-07-27`
|
||||
- [x] 单个 `2021-08-06`
|
||||
- [x] 批量删除词条 `2021-07-27`
|
||||
- [x] 上下移动词条 `2021-07-27`
|
||||
- [x] 通过键盘上下移动 `2021-07-27`
|
||||
- [x] 非分组状态下的移动 `2021-07-29`
|
||||
- [x] 展示:分组 | 非分组 码表 `2021-07-28`
|
||||
- [x] 展示总词数 | 当前词数 | 分组模式 `2021-08-01`
|
||||
- [x] 按输入码排序 `2021-08-12`
|
||||
- [x] 任意词条移动到任意码表中
|
||||
- [x] 右击编辑任意词条内容 `2021-10-23`
|
||||
- [x] 搜索框添加清空内容的按钮 `2021-10-16`
|
||||
- [x] shift 批量选词 `2021-10-17`
|
||||
- [x] 直接在窗口内部切换码表 `2021-11-22`
|
||||
|
||||
#### 2. 主码表文件
|
||||
- [x] 词条添加到主码表文件 `2021-08-04`
|
||||
- [x] 插入时匹配词条位置 `2021-08-04`
|
||||
- [x] 普通词条 -> 主码表 `2021-08-04`
|
||||
- [x] 分组词条 -> 主码表 `2021-08-04`
|
||||
- [x] 删除已移动的词条 `2021-08-04`
|
||||
- [x] 主码表展示用时优化 100ms 左右 `2021-08-01`
|
||||
- [x] 纯代码处理 8 万多条数据,只用不到 100ms `2021-07-30`
|
||||
- [x] 改用 `vue-virtual-scroller` 作为列表载体,加载多少都不会卡 `2021-08-01`
|
||||
|
||||
|
||||
#### 3. 分组管理
|
||||
- [x] 分组类型的码表以 `dict_grouped: true` 开头 `2021-07-29`
|
||||
- [x] 分组修改组名 `2021-07-27`
|
||||
- [x] 删除词条后,如果组内词条为空,删除该组 `2021-08-01`
|
||||
- [x] 分组添加 `2021-08-06`
|
||||
- [x] 分组删除 `2021-08-06`
|
||||
- [x] 分组列表,切换展示内容 `2021-08-09`
|
||||
- [x] 适配暗黑模式 `2021-08-09`
|
||||
- [x] 列表滚动条样式 `2021-08-09`
|
||||
- [x] 词条在分组之间移动
|
||||
|
||||
#### 4. 系统相关
|
||||
- [x] 保存文件后,自动调用 rime 布署方法进行布署
|
||||
- [x] macOS `2021-07-28`
|
||||
- [x] Windows `2021-07-30`
|
||||
- [ ] Linux
|
||||
|
||||
#### 5. 文件操作
|
||||
- [x] 写入词库内容 `2021-07-26`
|
||||
- [x] <kbd>ctrl</kbd> + <kbd>s</kbd> 快捷键保存 `2021-07-27`
|
||||
- [x] 非分组时保存到文件 `2021-07-29`
|
||||
- [x] 默认编辑器打开对应的码表源文本文件 `2021-07-28`
|
||||
|
||||
|
||||
#### 6. 配置页面
|
||||
- [x] 添加配置页面 `2021-10-14`
|
||||
- [x] 指定初始载入码表 `2021-10-14`
|
||||
- [x] 保存后是否立即布署 `2021-10-15`
|
||||
- [x] 回车键是搜索 | 添加新用户词 `2021-10-15`
|
||||
- [x] 搜索时,编码 | 词条 | 同时 | 任一 `2021-10-16`
|
||||
- [x] 记录最后一次选中的分组 `2021-10-16`
|
||||
- [x] 暗黑模式切换 `2021-10-16`
|
||||
- [x] 添加自定义的编码生成用的参考码表 `2021-10-25`
|
||||
- [ ] 删除元素时,如果组内词条为空,是否删除该组 `待定`
|
||||
- [ ] 配置项:切换码表是否自动搜索
|
||||
- [x] 手动打开调试窗口
|
||||
|
||||
|
||||
#### 7. 其它
|
||||
- [x] macOS 暗黑模式适配 `2021-08-08`
|
||||
- [ ] 使用帮助页面
|
||||
- [x] 关于窗口信息 `2021-08-10`
|
||||
|
||||
#### 8. 其它想法
|
||||
- [ ] 全民维护一个增量词库
|
||||
- [ ] 多用户
|
||||
- [ ] 能提升词条优先级
|
||||
|
||||
|
||||
## 布署指令
|
||||
|
||||
macOS
|
||||
```bash
|
||||
git remote add origin http://testgitea2.trustie.net/firstTeam-caishi-n/forTeam.git
|
||||
git push -u origin master
|
||||
|
||||
"/Library/Input Methods/Squirrel.app/Contents/MacOS/Squirrel" --reload
|
||||
```
|
||||
|
||||
windows
|
||||
```bash
|
||||
cd C:\Program Files (x86)\Rime\weasel-0.14.3
|
||||
WeaselDeployer.exe /deploy
|
||||
```
|
||||
|
||||
## 解决的难题
|
||||
1. 查重并提取出所有重复的内容
|
||||
2. 词条根据词条编码判断插入位置
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 49 KiB |
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="10px" height="10px" viewBox="0 0 10 10" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>delete</title>
|
||||
<g id="delete" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
|
||||
<line x1="1" y1="1" x2="9" y2="9" id="Line" stroke="#000000" stroke-width="2"></line>
|
||||
<line x1="1" y1="1" x2="9" y2="9" id="Line" stroke="#000000" stroke-width="2" transform="translate(5.000000, 5.000000) scale(-1, 1) translate(-5.000000, -5.000000) "></line>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 607 B |
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="10px" height="10px" viewBox="0 0 10 10" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>delete</title>
|
||||
<g id="delete" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
|
||||
<line x1="1" y1="1" x2="9" y2="9" id="Line" stroke="#FFFFFF" stroke-width="2"></line>
|
||||
<line x1="1" y1="1" x2="9" y2="9" id="Line" stroke="#FFFFFF" stroke-width="2" transform="translate(5.000000, 5.000000) scale(-1, 1) translate(-5.000000, -5.000000) "></line>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 608 B |
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="10px" height="10px" viewBox="0 0 10 10" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>down</title>
|
||||
<g id="down" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M5.89442719,2.78885438 L9.2763932,9.5527864 C9.52338245,10.0467649 9.3231581,10.6474379 8.82917961,10.8944272 C8.69032417,10.9638549 8.53721111,11 8.38196601,11 L1.61803399,11 C1.06574924,11 0.618033989,10.5522847 0.618033989,10 C0.618033989,9.8447549 0.654179081,9.69164184 0.723606798,9.5527864 L4.10557281,2.78885438 C4.35256206,2.29487588 4.9532351,2.09465154 5.4472136,2.34164079 C5.640741,2.43840449 5.79766349,2.59532698 5.89442719,2.78885438 Z" id="Triangle-Copy" fill="#000000" transform="translate(5.000000, 6.000000) scale(1, -1) translate(-5.000000, -6.000000) "></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 904 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10"><title>edit</title><g id="图层_2" data-name="图层 2"><g id="图层_1-2" data-name="图层 1"><path d="M9.61.39a1.3,1.3,0,0,0-1.86,0L1.27,6.86,1.18,7,.63,8.42,0,10l1.58-.62L3,8.83l.16-.1L9.61,2.25A1.3,1.3,0,0,0,9.61.39Z"/></g></g></svg>
|
After Width: | Height: | Size: 299 B |
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="10px" height="10px" viewBox="0 0 10 10" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>up</title>
|
||||
<g id="up" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M5.89442719,0.788854382 L9.2763932,7.5527864 C9.52338245,8.0467649 9.3231581,8.64743794 8.82917961,8.89442719 C8.69032417,8.96385491 8.53721111,9 8.38196601,9 L1.61803399,9 C1.06574924,9 0.618033989,8.55228475 0.618033989,8 C0.618033989,7.8447549 0.654179081,7.69164184 0.723606798,7.5527864 L4.10557281,0.788854382 C4.35256206,0.294875885 4.9532351,0.0946515378 5.4472136,0.341640786 C5.640741,0.438404488 5.79766349,0.59532698 5.89442719,0.788854382 Z" id="Triangle" fill="#000000"></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 809 B |
|
@ -0,0 +1,39 @@
|
|||
.animated{
|
||||
-webkit-animation-duration: .25s;
|
||||
animation-duration: .25s;
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
@-webkit-keyframes shake {
|
||||
from, to {
|
||||
@include transform(translate3d(0, 0, 0))
|
||||
}
|
||||
|
||||
0%, 50%, 100% {
|
||||
@include transform(translate3d(-5px, 0, 0))
|
||||
}
|
||||
|
||||
25%, 75% {
|
||||
@include transform(translate3d(5px, 0, 0))
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
from, to {
|
||||
@include transform(translate3d(0, 0, 0))
|
||||
}
|
||||
|
||||
0%, 50%, 100% {
|
||||
@include transform(translate3d(-5px, 0, 0))
|
||||
}
|
||||
|
||||
25%, 75% {
|
||||
@include transform(translate3d(5px, 0, 0))
|
||||
}
|
||||
}
|
||||
|
||||
.shake {
|
||||
-webkit-animation-name: shake;
|
||||
animation-name: shake;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
.app-container{
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
}
|
|
@ -0,0 +1,219 @@
|
|||
$padding-top: 3px;
|
||||
$padding-lr: 6px;
|
||||
$padding-lr-radio: 8px;
|
||||
$height-btn: $height-input;
|
||||
$radius-btn: $radius;
|
||||
$radius-switch: 15px;
|
||||
$btn-padding-tb: 2px;
|
||||
$btn-padding-lr: 8px;
|
||||
$fz-button: 12px;
|
||||
|
||||
|
||||
|
||||
$checkbox-shadow: false;
|
||||
|
||||
.tool-bar{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
|
||||
.center{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
$btns: (
|
||||
'primary' : white,
|
||||
'green' : $green,
|
||||
'cyan' : $cyan,
|
||||
'blue' : $blue,
|
||||
'purple' : $purple,
|
||||
'yellow' : $yellow,
|
||||
'orange' : $orange,
|
||||
'roseo' : $roseo,
|
||||
'red' : $red,
|
||||
'gray' : $gray,
|
||||
);
|
||||
|
||||
.btn-ellipsis{
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
.btn{
|
||||
font-size: $fz-button;
|
||||
color: inherit;
|
||||
line-height: $height-btn - $btn-padding-tb * 2;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
@include border-radius($radius-btn);
|
||||
white-space: nowrap;
|
||||
padding: $btn-padding-tb $btn-padding-lr ;
|
||||
background-color: white;
|
||||
@extend .unselectable;
|
||||
&:active{
|
||||
@include transform(translateY(2px));
|
||||
@include box-shadow(inset 0 1px 2px rgba(0,0,0,0.1));
|
||||
@include transition(all 0s);
|
||||
}
|
||||
@include transition(all 0.3s);
|
||||
&:hover{
|
||||
@include transition(all 0.3s);
|
||||
&:active{
|
||||
@include transition(all 0s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$height-btn-sm: $height-input - 6;
|
||||
$btn-sm-padding-tb: 2px;
|
||||
$btn-sm-padding-lr: 3px;
|
||||
|
||||
|
||||
.btn-sm {
|
||||
font-size: $fz-button - 1;
|
||||
line-height: $height-btn-sm - $btn-sm-padding-tb * 2;
|
||||
padding: $btn-sm-padding-tb $btn-sm-padding-lr ;
|
||||
}
|
||||
|
||||
.no-border{
|
||||
border: none !important;
|
||||
@include box-shadow(0 0 1px rgba(0,0,0,0.4))
|
||||
}
|
||||
|
||||
|
||||
@each $name, $color in $btns {
|
||||
// BTN
|
||||
.btn-#{$name}{
|
||||
@if $name == primary {
|
||||
color: $text-main;
|
||||
background-color: $color;
|
||||
border: 1px solid darken($color, 15%);
|
||||
} @else {
|
||||
color: white;
|
||||
background-color: $color;
|
||||
border: 1px solid $color;
|
||||
}
|
||||
&:hover{
|
||||
@if $name == primary {
|
||||
background-color: darken($color, 3%);
|
||||
border-color: darken($color, 18%);
|
||||
} @else {
|
||||
background-color: darken($color, 8%);
|
||||
border-color: darken($color, 8%);
|
||||
}
|
||||
}
|
||||
&:active{
|
||||
background-color: darken($color, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
// BTN-ALT
|
||||
.btn-alt-#{$name}{
|
||||
border: 1px solid transparent;
|
||||
background-color: transparent;
|
||||
@if $name == primary {
|
||||
color: $text-main;
|
||||
} @else {
|
||||
color: $color;
|
||||
}
|
||||
&:hover{
|
||||
@if $name == primary {
|
||||
border-color: $text-main;
|
||||
color: $text-main;
|
||||
} @else {
|
||||
color: $color;
|
||||
border-color: $color;
|
||||
}
|
||||
}
|
||||
&:active{
|
||||
@if $name == primary {
|
||||
color: $text-main;
|
||||
} @else {
|
||||
color: $color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input[type=checkbox],
|
||||
input[type=radio]
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
|
||||
$height-checkbox: 14px;
|
||||
$border-color-checkbox: #d6d6d6;
|
||||
input[type=checkbox] + label.checkbox{
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
width: $height-checkbox;
|
||||
background-color: white;
|
||||
height: $height-checkbox;
|
||||
@include border-radius(3px);
|
||||
border: 1px solid $border-color-checkbox;
|
||||
position: relative;
|
||||
@if $checkbox-shadow{
|
||||
@include box-shadow(inset 1px 1px 2px rgb(0 0 0 / 12%))
|
||||
}
|
||||
}
|
||||
|
||||
label.checkbox:hover{
|
||||
border-color: $color-main !important;
|
||||
@if $checkbox-shadow {
|
||||
@include box-shadow(inset 1px 1px 2px transparentize($color-main, 0.8) !important) ;
|
||||
}
|
||||
}
|
||||
|
||||
input[type=checkbox]:checked + label.checkbox{
|
||||
&:after{
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
height: $height-checkbox - 6;
|
||||
width: $height-checkbox - 6;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
@include border-radius(3px);
|
||||
background-color: $color-main;
|
||||
}
|
||||
border-color: $color-main;
|
||||
}
|
||||
|
||||
input[type=radio] + label.radio{
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
width: $height-checkbox;
|
||||
background-color: white;
|
||||
height: $height-checkbox;
|
||||
@include border-radius(10px);
|
||||
border: 1px solid $border-color-checkbox;
|
||||
position: relative;
|
||||
@if $checkbox-shadow{
|
||||
@include box-shadow(inset 1px 1px 2px rgb(0 0 0 / 12%))
|
||||
}
|
||||
}
|
||||
|
||||
label.radio:hover{
|
||||
border-color: $color-main !important;
|
||||
@if $checkbox-shadow {
|
||||
@include box-shadow(inset 1px 1px 2px transparentize($color-main, 0.8) !important) ;
|
||||
}
|
||||
}
|
||||
|
||||
input[type=radio]:checked + label.radio{
|
||||
&:after{
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
height: $height-checkbox - 8;
|
||||
width: $height-checkbox - 8;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
@include border-radius(10px);
|
||||
background-color: $color-main;
|
||||
}
|
||||
border-color: $color-main;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
|
||||
.config-file-list{
|
||||
.config-file-list-item{
|
||||
@extend .unselectable;
|
||||
padding: 3px 8px;
|
||||
border-bottom: 1px solid $color-border;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.name{
|
||||
color: $text-title;
|
||||
}
|
||||
.path{
|
||||
margin-left: 30px;
|
||||
color: $text-subtitle;
|
||||
}
|
||||
&:last-child{
|
||||
border-bottom: none;
|
||||
}
|
||||
&:hover{
|
||||
@include border-radius(3px);
|
||||
overflow: hidden;
|
||||
border-bottom-color: transparent;
|
||||
background-color: $bg-dropdown-highlight;
|
||||
}
|
||||
&.active{
|
||||
@include border-radius(3px);
|
||||
overflow: hidden;
|
||||
background-color: $color-main;
|
||||
border-bottom-color: $color-main;
|
||||
.name, .path{
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.config{
|
||||
.container {
|
||||
padding: 10px 25px;
|
||||
}
|
||||
|
||||
section{
|
||||
margin-bottom: 30px;
|
||||
.section-title{
|
||||
padding: 10px 0;
|
||||
h3{
|
||||
font-weight: bold;
|
||||
font-size: $fz-title;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.desc{
|
||||
font-size: $fz-button;
|
||||
color: $text-subtitle;
|
||||
}
|
||||
}
|
||||
.section-content{
|
||||
@include border-radius(5px);
|
||||
background-color: $bg-panel;
|
||||
padding: 10px 10px 10px 50px;
|
||||
.check-item{
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
& > *:first-child{
|
||||
margin-right: 5px;
|
||||
}
|
||||
.label{
|
||||
width: 80px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
&:last-child{
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.check-list{
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
.check-item{
|
||||
margin-right: 20px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$height-switch: 14px;
|
||||
|
||||
.audio-switch{
|
||||
input{
|
||||
display: none;
|
||||
}
|
||||
input+label{
|
||||
cursor: pointer;
|
||||
box-sizing: content-box;
|
||||
border: 2px solid $text-subtitle;
|
||||
background-color: $text-subtitle;
|
||||
position: relative;
|
||||
display: block;
|
||||
width: $height-switch * 1.8;
|
||||
@include border-radius(30px);
|
||||
height: $height-switch;
|
||||
@include transition(all .3s);
|
||||
&:before{
|
||||
z-index: 10;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #eeeeee;
|
||||
content: '';
|
||||
display: block;
|
||||
@include box-shadow(1px 3px 6px rgba(0,0,0,0.5));
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
width: $height-switch;
|
||||
height: $height-switch;
|
||||
@include border-radius(30px);
|
||||
background-color: white;
|
||||
@include transition(all .3s);
|
||||
|
||||
}
|
||||
}
|
||||
input:checked+label{
|
||||
@include transition(all .3s);
|
||||
border-color: $green;
|
||||
background-color: $green;
|
||||
&:before{
|
||||
top: 0;
|
||||
left: $height-switch * .8;
|
||||
background-color: #fff;
|
||||
@include transition(all .3s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dict-map-content{
|
||||
height: 100px;
|
||||
overflow-y: auto;
|
||||
}
|
|
@ -0,0 +1,365 @@
|
|||
$black-color-border-highlight: lighten(black, 22%);
|
||||
$black-body-bg: #0f0f13;
|
||||
$black-bg: #1c1f24;
|
||||
$black-bg-hover: lighten(#1c1f24, 5%);
|
||||
$black-bg-highlight: #383f4a;
|
||||
$black-color-border: lighten($black-bg, 5%);
|
||||
$black-color-text: lighten(black, 90%);
|
||||
$black-color-text-info: lighten(black, 65%);
|
||||
$black-color-text-subtitle: lighten(black, 70%);
|
||||
$black-color-text-comment: lighten(black, 55%);
|
||||
$black-btn-bg: lighten(black, 25%);
|
||||
$black-btn-bg-hover: lighten(black, 40%);
|
||||
$black-color-main: #81fffa;
|
||||
|
||||
@mixin dark-mode(){
|
||||
// 暗黑模式下的滚动条样式
|
||||
::-webkit-scrollbar {
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: $black-color-border !important;
|
||||
}
|
||||
:hover::-webkit-scrollbar-thumb {
|
||||
background-color: $black-color-border-highlight !important;
|
||||
}
|
||||
body {
|
||||
background-color: $black-bg !important;
|
||||
}
|
||||
.list-container {
|
||||
}
|
||||
.search-bar {
|
||||
background-color: $black-bg;
|
||||
border-bottom-color: $black-color-border;
|
||||
}
|
||||
.input-item {
|
||||
.btn-clear {
|
||||
background-color: $black-bg-highlight;
|
||||
&:hover{
|
||||
background-color: $color-main;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.modal{
|
||||
background-color: rgba(0,0,0,0.2);
|
||||
.modal-panel{
|
||||
border-color: $black-color-border-highlight;
|
||||
background-color: $black-bg;
|
||||
}
|
||||
}
|
||||
|
||||
.modal{
|
||||
&-header{
|
||||
color: $black-color-text;
|
||||
}
|
||||
&-body{
|
||||
input{
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
&-footer{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.group {
|
||||
color: $black-color-text;
|
||||
.group-header {
|
||||
color: $black-color-main;
|
||||
&:hover {
|
||||
background-color: $black-bg-hover;
|
||||
}
|
||||
&.active {
|
||||
background-color: $black-bg-highlight;
|
||||
}
|
||||
}
|
||||
}
|
||||
.operation{
|
||||
.up, .down{
|
||||
}
|
||||
}
|
||||
.word-item {
|
||||
&:hover {
|
||||
background: $black-bg-hover;
|
||||
.checkbox-cell {
|
||||
label {
|
||||
border-color: $black-color-main;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:after {
|
||||
background-color: $black-color-border;
|
||||
}
|
||||
.code {
|
||||
color: $black-color-text-subtitle;
|
||||
}
|
||||
.word {
|
||||
}
|
||||
}
|
||||
.catalog-list {
|
||||
color: $black-color-text;
|
||||
border-right-color: $black-color-border !important;
|
||||
.catalog-item {
|
||||
&:hover {
|
||||
background-color: $black-bg-hover !important;
|
||||
}
|
||||
&.active {
|
||||
color: black !important;
|
||||
background-color: $black-color-main !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.schema-list {
|
||||
color: $black-color-text;
|
||||
border-right-color: $black-color-border !important;
|
||||
.schema-list-item {
|
||||
&:hover {
|
||||
background-color: $black-bg-hover !important;
|
||||
}
|
||||
&.active {
|
||||
color: black !important;
|
||||
background-color: $black-color-main !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.footer {
|
||||
background-color: $black-bg;
|
||||
border-top-color: $black-color-border-highlight;
|
||||
color: $black-color-text;
|
||||
.link {
|
||||
&:hover {
|
||||
color: $black-color-main !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.dropdown {}
|
||||
.shadow{
|
||||
@include box-shadow(1px 2px 5px rgba(0,0,0,0.5))
|
||||
}
|
||||
.file-list {
|
||||
color: $black-color-text;
|
||||
background-color: $black-bg;
|
||||
border-color: #00448e;
|
||||
&-item {
|
||||
border-bottom-color: $black-color-border;
|
||||
&:hover {
|
||||
background-color: $black-bg-highlight;
|
||||
}
|
||||
&.active, &:active {
|
||||
background-color: $blue;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
.checkbox-cell{
|
||||
.checkbox-item{
|
||||
background-color: $black-bg-highlight;
|
||||
border-color: $black-bg-highlight;
|
||||
}
|
||||
}
|
||||
|
||||
/*CONFIG*/
|
||||
.config {
|
||||
section {
|
||||
margin-bottom: 30px;
|
||||
.section-title {
|
||||
color: $black-color-text;
|
||||
.desc {
|
||||
color: $black-color-text-subtitle;
|
||||
}
|
||||
}
|
||||
.section-content {
|
||||
background-color: $black-body-bg;
|
||||
color: $black-color-text;
|
||||
}
|
||||
}
|
||||
}
|
||||
.config-file-list {
|
||||
.config-file-list-item {
|
||||
border-bottom-color: $black-color-border;
|
||||
.name {
|
||||
color: $black-color-text;
|
||||
}
|
||||
.path {
|
||||
color: $black-color-text-subtitle;
|
||||
}
|
||||
&.active {
|
||||
background-color: $black-color-main;
|
||||
border-bottom-color: $black-color-main;
|
||||
.name, .path {
|
||||
color: $text-main;
|
||||
}
|
||||
&:hover {
|
||||
background-color: $black-color-main;
|
||||
border-bottom-color: $black-color-main;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
background-color: $black-bg-highlight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*BUTTONS*/
|
||||
input[type=radio] + label.radio {
|
||||
background-color: $black-bg-highlight;
|
||||
border-color: $black-color-border-highlight;
|
||||
}
|
||||
input[type=radio]:checked + label.radio {
|
||||
border-color: $black-color-main;
|
||||
}
|
||||
label.radio:hover {
|
||||
border-color: $black-color-main !important;
|
||||
}
|
||||
input[type=radio]:checked + label.radio {
|
||||
&:after {
|
||||
background-color: $black-color-main;
|
||||
}
|
||||
}
|
||||
.audio-switch {
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
input + label {
|
||||
box-sizing: content-box;
|
||||
border: 2px solid $text-subtitle;
|
||||
background-color: $text-subtitle;
|
||||
&:before {
|
||||
border-color: transparent;
|
||||
background-color: $black-bg;
|
||||
}
|
||||
}
|
||||
input:checked + label {
|
||||
border-color: $green;
|
||||
background-color: $green;
|
||||
}
|
||||
}
|
||||
input, select {
|
||||
color: $black-color-text;
|
||||
&:focus {
|
||||
border-color: $black-color-main;
|
||||
@include box-shadow(0 0 0 2px transparentize($black-color-main, 0.5))
|
||||
}
|
||||
}
|
||||
input[type=checkbox] + label.checkbox {
|
||||
background-color: $black-body-bg;
|
||||
border-color: $black-color-border;
|
||||
}
|
||||
input[type=checkbox]:checked + label.checkbox {
|
||||
border-color: $black-color-main;
|
||||
}
|
||||
label.checkbox:hover {
|
||||
border-color: $black-color-main !important;
|
||||
}
|
||||
input[type=checkbox]:checked + label.checkbox {
|
||||
&:after {
|
||||
background-color: $black-color-main;
|
||||
}
|
||||
}
|
||||
input {
|
||||
background-color: $black-body-bg;
|
||||
border-color: $black-color-border-highlight;
|
||||
}
|
||||
|
||||
@each $name, $color in $btns {
|
||||
// BTN
|
||||
.btn-#{$name} {
|
||||
@if $name == primary {
|
||||
color: $black-color-text;
|
||||
background-color: $black-btn-bg;
|
||||
border: 1px solid $black-btn-bg;
|
||||
} @else {
|
||||
color: white;
|
||||
background-color: $color;
|
||||
border: 1px solid $color;
|
||||
}
|
||||
&:hover {
|
||||
@if $name == primary {
|
||||
color: $black-color-text;
|
||||
background-color: $black-btn-bg-hover;
|
||||
border: 1px solid $black-btn-bg-hover;
|
||||
} @else {
|
||||
background-color: darken($color, 8%);
|
||||
border-color: darken($color, 8%);
|
||||
}
|
||||
}
|
||||
&:active {
|
||||
@if $name == primary {
|
||||
} @else {
|
||||
background-color: darken($color, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BTN-ALT
|
||||
.btn-alt-#{$name} {
|
||||
border-color: transparent;
|
||||
background-color: transparent;
|
||||
color: $color;
|
||||
&:hover {
|
||||
border-color: $color;
|
||||
color: $color;
|
||||
}
|
||||
&:active {
|
||||
color: $color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*TOOL*/
|
||||
.tool{
|
||||
.tool-panel{
|
||||
border-right-color: $black-color-border;
|
||||
background-color: $black-bg;
|
||||
.title{
|
||||
color: $black-color-text;
|
||||
}
|
||||
.content{
|
||||
background-color: $black-body-bg;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border: none;
|
||||
background-color: $black-color-border-highlight;
|
||||
}
|
||||
}
|
||||
hr{
|
||||
border-color: $black-color-border;
|
||||
}
|
||||
|
||||
|
||||
.readme{
|
||||
.readme-item{
|
||||
h3{
|
||||
color: $black-color-text;
|
||||
}
|
||||
p{
|
||||
color: $black-color-text-subtitle;
|
||||
i{
|
||||
color: $black-color-main;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.dark-mode {
|
||||
@include dark-mode();
|
||||
}
|
||||
|
||||
.auto-mode{
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@include dark-mode();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
$bg-dropdown: $blue;
|
||||
$bg-dropdown-highlight: transparentize($bg-dropdown, 0.95);
|
||||
|
||||
.dropdown{
|
||||
position: relative;
|
||||
&-link{
|
||||
|
||||
}
|
||||
&-body{
|
||||
z-index: 100;
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
left: 0;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.file-list{
|
||||
margin-left: 5px;
|
||||
max-height: 400px;
|
||||
border: 1px solid lighten($bg-dropdown, 20%);
|
||||
@include border-radius($radius);
|
||||
overflow: hidden;
|
||||
color: $text-main;
|
||||
flex-shrink: 0;
|
||||
background-color: white;
|
||||
overflow-y: auto;
|
||||
&-item{
|
||||
min-width: 100px;
|
||||
@extend .unselectable;
|
||||
text-align: center;
|
||||
line-height: $catalog-height - $catalog-pd-tb * 2;
|
||||
padding: $catalog-pd-tb $catalog-pd-lr;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid $color-border;
|
||||
&:last-child{
|
||||
border-bottom: none;
|
||||
}
|
||||
> div{
|
||||
height: $catalog-height - $catalog-pd-tb * 2;
|
||||
}
|
||||
&:hover{
|
||||
background-color: $bg-dropdown-highlight;
|
||||
}
|
||||
&.active, &:active{
|
||||
background-color: $bg-dropdown;
|
||||
border-bottom-color: $bg-dropdown;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
&::-webkit-scrollbar {
|
||||
z-index: 50;
|
||||
width: 1px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: $color-border;
|
||||
}
|
||||
&:hover::-webkit-scrollbar-thumb {
|
||||
background-color: transparentize($bg-dropdown, 0.3);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
@font-face {
|
||||
font-family: "Roboto-v27-light";
|
||||
src: url("../fonts/Robotov27-light.woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Roboto-v27-regular";
|
||||
src: url("../fonts/Robotov27-regular.woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Comfortaa-v30";
|
||||
src: url("../fonts/Comfortaa-v30.woff2");
|
||||
}
|
||||
|
||||
|
||||
.font-roboto{
|
||||
font-family: "Roboto-v27-light", Roboto, SansSerif;
|
||||
}
|
||||
|
||||
.font-roboto-regular{
|
||||
font-family: "Roboto-v27-regular", Roboto, SansSerif;
|
||||
}
|
||||
|
||||
.font-comfortaa{
|
||||
font-family: "Comfortaa-v30", Roboto, SansSerif;
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
.footer{
|
||||
background-color: $bg-panel;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
border-top: 1px solid $color-border;
|
||||
padding: 5px 15px;
|
||||
font-size: $fz-button;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: $text-main;
|
||||
|
||||
.tip{
|
||||
color: $green;
|
||||
}
|
||||
|
||||
.footer-toolbar{
|
||||
> *{
|
||||
margin-right: 30px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
.link-list{
|
||||
.link{
|
||||
@extend .unselectable;
|
||||
margin-right: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
&:hover{
|
||||
transition: all 0.1s;
|
||||
color: $color-main;
|
||||
}
|
||||
}
|
||||
.origin{
|
||||
font-weight: bold;
|
||||
//@extend .font-roboto-regular;
|
||||
}
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.info-list{
|
||||
.number{
|
||||
@extend .font-roboto-regular;
|
||||
margin-left: 2px;
|
||||
}
|
||||
> *{
|
||||
margin-right: 10px;
|
||||
}
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
$catalog-height: 30px;
|
||||
$catalog-pd-tb: 5px;
|
||||
$catalog-pd-lr: 10px;
|
||||
|
||||
.group-container{
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
.catalog-list{
|
||||
border-right: 1px solid $border-color-panel;
|
||||
width: 110px;
|
||||
flex-shrink: 0;
|
||||
overflow-y: auto;
|
||||
.catalog-item{
|
||||
@extend .unselectable;
|
||||
text-align: center;
|
||||
line-height: $catalog-height - $catalog-pd-tb * 2;
|
||||
padding: $catalog-pd-tb $catalog-pd-lr;
|
||||
cursor: pointer;
|
||||
> div{
|
||||
height: $catalog-height - $catalog-pd-tb * 2;
|
||||
}
|
||||
&:hover{
|
||||
background-color: $bg-td-highlighted;
|
||||
}
|
||||
&.active{
|
||||
background-color: $color-main;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
&::-webkit-scrollbar {
|
||||
z-index: 50;
|
||||
width: 1px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: $bg-scroll-thumb;
|
||||
}
|
||||
}
|
||||
|
||||
.group-list{
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
$timer: 5px;
|
||||
// common
|
||||
@for $item from 0 through 8 {
|
||||
.mt-#{$item}{margin-top: $timer * $item !important;}
|
||||
.mb-#{$item}{margin-bottom: $timer * $item !important;}
|
||||
.ml-#{$item}{margin-left: $timer * $item !important;}
|
||||
.mr-#{$item}{margin-right: $timer * $item !important;}
|
||||
.m-#{$item}{margin: $timer * $item !important;}
|
||||
|
||||
.pt-#{$item}{padding-top: $timer * $item !important;}
|
||||
.pb-#{$item}{padding-bottom: $timer * $item !important;}
|
||||
.pl-#{$item}{padding-left: $timer * $item !important;}
|
||||
.pr-#{$item}{padding-right: $timer * $item !important;}
|
||||
.p-#{$item}{padding: $timer * $item !important;}
|
||||
}
|
||||
|
||||
.w-10{
|
||||
width: 100%;
|
||||
}
|
|
@ -0,0 +1,195 @@
|
|||
$fz-list: 13px;
|
||||
$bg-td-highlighted: transparentize($color-main, 0.95);
|
||||
$height-word-item: 24px;
|
||||
|
||||
.virtual-list{
|
||||
height: 100%;
|
||||
padding-bottom: 10px; // virtual-scroller 时的末尾 padding
|
||||
}
|
||||
|
||||
.list-container{
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.group{
|
||||
font-size: $fz-list;
|
||||
color: $text-main;
|
||||
.group-header{
|
||||
padding: 5px 10px 5px 35px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: $color-main;
|
||||
&.active{
|
||||
background-color: transparentize($color-main, 0.95);
|
||||
h3{
|
||||
}
|
||||
}
|
||||
&:hover{
|
||||
@include transition(all 0.3s);
|
||||
background-color: transparentize($color-main, 0.95);
|
||||
.operation{
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
input{
|
||||
margin-right: 10px;
|
||||
}
|
||||
h3{
|
||||
//flex-grow: 1;
|
||||
font-size: $fz-list + 1;
|
||||
line-height: $height-input + 2;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.operation{
|
||||
margin-right: 10px;
|
||||
opacity: 0;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
> * {
|
||||
margin-left:5px;
|
||||
}
|
||||
@include transition(all 0.3s);
|
||||
}
|
||||
.tip{
|
||||
flex-grow: 1;
|
||||
text-align: right;
|
||||
font-size: $fz-list - 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
.word-list{
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
$checkbox-shadow: false;
|
||||
$height-checkbox: 14px;
|
||||
$border-color-checkbox: #d6d6d6;
|
||||
|
||||
.word-item{
|
||||
@extend .unselectable;
|
||||
height: $height-word-item;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
position: relative;
|
||||
padding: 0 20px;
|
||||
&.active{
|
||||
background-color: $color-main;
|
||||
}
|
||||
&:after{
|
||||
content: '';
|
||||
height: 1px;
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
width: calc(100% - 45px);
|
||||
right: 0;
|
||||
background-color: $color-border;
|
||||
}
|
||||
.checkbox-cell{
|
||||
padding-right: 20px;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
.checkbox-item{
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
width: $height-checkbox;
|
||||
background-color: white;
|
||||
height: $height-checkbox;
|
||||
@include border-radius(3px);
|
||||
border: 1px solid $border-color-checkbox;
|
||||
position: relative;
|
||||
@if $checkbox-shadow{
|
||||
@include box-shadow(inset 1px 1px 2px rgb(0 0 0 / 12%))
|
||||
}
|
||||
|
||||
&.active{
|
||||
border-color: $color-main;
|
||||
&:after{
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
height: $height-checkbox - 6;
|
||||
width: $height-checkbox - 6;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
@include border-radius(3px);
|
||||
background-color: $color-main;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.code{
|
||||
//@extend .font-roboto;
|
||||
line-height: $height-word-item;
|
||||
min-width: 15%;
|
||||
padding-right: 20px;
|
||||
color: $text-title;
|
||||
}
|
||||
.word{
|
||||
padding-right: 10px;
|
||||
line-height: $height-word-item;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.priority{
|
||||
@extend .font-roboto;
|
||||
padding-right: 10px;
|
||||
text-align: right;
|
||||
width: 10%;
|
||||
color: $text-subtitle;
|
||||
font-size: $fz-list - 2;
|
||||
}
|
||||
.id{
|
||||
@extend .font-roboto;
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
width: $fz-list * 4 + 20;
|
||||
color: $text-subtitle;
|
||||
font-size: $fz-list - 2;
|
||||
}
|
||||
.operation{
|
||||
width: 100px;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.2;
|
||||
@extend .unselectable;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
.up, .down{
|
||||
cursor: pointer;
|
||||
padding: 5px 6px;
|
||||
@include border-radius(2px);
|
||||
img{
|
||||
display: block;
|
||||
width: 8px;
|
||||
}
|
||||
&:hover{
|
||||
background-color: white;
|
||||
}
|
||||
&:active{
|
||||
transform: translateY(1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover{
|
||||
background-color: $bg-td-highlighted;
|
||||
.checkbox-cell{
|
||||
.checkbox-item{
|
||||
border-color: $color-main !important;
|
||||
@if $checkbox-shadow {
|
||||
@include box-shadow(inset 1px 1px 2px transparentize($color-main, 0.8) !important) ;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.operation{
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
.unselectable {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
.schema-list{
|
||||
border-right: 1px solid $border-color-panel;
|
||||
flex-shrink: 0;
|
||||
overflow-y: auto;
|
||||
.schema-list-item{
|
||||
@extend .unselectable;
|
||||
text-align: center;
|
||||
line-height: $catalog-height - $catalog-pd-tb * 2;
|
||||
padding: $catalog-pd-tb 30px;
|
||||
cursor: pointer;
|
||||
> div{
|
||||
height: $catalog-height - $catalog-pd-tb * 2;
|
||||
}
|
||||
&:hover{
|
||||
background-color: $bg-td-highlighted;
|
||||
}
|
||||
&.active{
|
||||
background-color: $color-main;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
&::-webkit-scrollbar {
|
||||
z-index: 50;
|
||||
width: 1px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: $bg-scroll-thumb;
|
||||
}
|
||||
}
|
||||
|
||||
.file-list{
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
@use "sass:math";
|
||||
|
||||
.search-bar{
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
border-bottom: 1px solid $color-border;
|
||||
background-color: $bg-panel;
|
||||
padding: 10px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
> *{
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
$fz-notice: 11px;
|
||||
.notice{
|
||||
margin-left: 20px;
|
||||
font-size: $fz-notice;
|
||||
color: $text-subtitle;
|
||||
}
|
||||
|
||||
$height-clear: 16px;
|
||||
|
||||
.input-item{
|
||||
position: relative;
|
||||
.btn-clear{
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: ($height-input + $padding-input-tb - $height-clear)/2;
|
||||
z-index: 10;
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
padding: 4px;
|
||||
background-color: $bg-td;
|
||||
@include border-radius(10px);
|
||||
img{
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
&:hover{
|
||||
background-color: $color-main;
|
||||
}
|
||||
&:active{
|
||||
transform: translateY(1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input, select{
|
||||
font-size: inherit;
|
||||
line-height: $height-input - $padding-input-tb * 2;
|
||||
border: 1px solid $border-color-input;
|
||||
@include border-radius($radius);
|
||||
padding: $padding-input-tb 8px;
|
||||
&.code{
|
||||
//font-family: monospace;
|
||||
flex-shrink: 0;
|
||||
width: 5rem;
|
||||
}
|
||||
&.word{
|
||||
flex-shrink: 0;
|
||||
width: 12rem;
|
||||
}
|
||||
&:focus{
|
||||
border-color: transparentize($color-main, 0.4);
|
||||
@include box-shadow(0 0 0 2px transparentize($color-main, 0.7))
|
||||
}
|
||||
@include transition(all, 0.5s)
|
||||
}
|
||||
select{
|
||||
min-width: 80px;
|
||||
option{
|
||||
padding: 10px;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.modal{
|
||||
z-index: 999;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
background-color: rgba(255,255,255,0.2);
|
||||
@include backdrop-filter(saturate(180%) blur(8px));
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.modal-panel{
|
||||
@include border-radius(10px);
|
||||
border: 2px solid $color-border;
|
||||
padding: 20px;
|
||||
margin-bottom: 80px;
|
||||
background-color: white;
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.modal{
|
||||
&-header{
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: $fz-title;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
&-body{
|
||||
padding: 15px 0;
|
||||
input{
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
&-footer{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
.tool-container{
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
|
||||
.tool-panel{
|
||||
border-right: 1px solid $color-border;
|
||||
width: 400px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.list-container{
|
||||
flex-grow: 1;
|
||||
.word-item{
|
||||
padding: 0 5px 0 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hr{
|
||||
margin: 20px 0;
|
||||
border-bottom: 1px solid $border-color-checkbox;
|
||||
}
|
||||
|
||||
.tool-panel{
|
||||
overflow-y: auto;
|
||||
margin-top: 1px;
|
||||
background-color: $bg-panel-tool;
|
||||
padding: 10px 15px 15px;
|
||||
&::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border: none;
|
||||
background-color: $border-color-checkbox;
|
||||
}
|
||||
&:hover::-webkit-scrollbar-thumb{
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
section{
|
||||
margin-bottom: 15px;
|
||||
.title{
|
||||
font-size: $fz-button;
|
||||
margin-bottom: 7px;
|
||||
font-weight: bold;
|
||||
color: $text-tool-title;
|
||||
}
|
||||
.content {
|
||||
@include border-radius(5px);
|
||||
padding: 10px;
|
||||
background-color: $bg-panel;
|
||||
}
|
||||
&:last-child{
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.load-list{
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-flow: row nowrap;
|
||||
.btn-load{
|
||||
flex-grow: 1;
|
||||
}
|
||||
.btn-reload{
|
||||
margin-left: 10px;
|
||||
flex-shrink: 0;
|
||||
width: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-list {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 10px;
|
||||
.btn-item {
|
||||
margin-right: 10px;
|
||||
}
|
||||
&:last-child{
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.readme{
|
||||
.readme-item{
|
||||
margin-bottom: 20px;
|
||||
h3{
|
||||
color: $text-tool-title;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
p{
|
||||
font-size: $fz-notice;
|
||||
color: $text-subtitle;
|
||||
line-height: 1.5;
|
||||
i{
|
||||
white-space: nowrap;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
color: $color-main;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
// box-shadow
|
||||
@mixin box-shadow($value...){
|
||||
-webkit-box-shadow: $value;
|
||||
-moz-box-shadow: $value;
|
||||
box-shadow: $value;
|
||||
}
|
||||
|
||||
// border-radius
|
||||
@mixin border-radius($corner...){
|
||||
-webkit-border-radius: $corner;
|
||||
-moz-border-radius: $corner;
|
||||
border-radius: $corner;
|
||||
}
|
||||
|
||||
@mixin clearfix(){
|
||||
&:after{
|
||||
content: '';
|
||||
display: block;
|
||||
clear: both;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin transform($value){
|
||||
-webkit-transform: $value;
|
||||
-moz-transform: $value;
|
||||
-ms-transform: $value;
|
||||
-o-transform: $value;
|
||||
transform: $value;
|
||||
}
|
||||
|
||||
@mixin transition($value...){
|
||||
-webkit-transition: $value;
|
||||
-moz-transition: $value;
|
||||
-ms-transition: $value;
|
||||
-o-transition: $value;
|
||||
transition: $value;
|
||||
}
|
||||
|
||||
@mixin animation($value){
|
||||
animation: $value;
|
||||
-webkit-animation: $value;
|
||||
}
|
||||
|
||||
@mixin linear-gradient($direct, $colors){
|
||||
background: linear-gradient($direct, $colors);
|
||||
background: -webkit-linear-gradient($direct, $colors);
|
||||
background: -moz-linear-gradient($direct, $colors);
|
||||
}
|
||||
|
||||
@mixin backdrop-filter($value){
|
||||
backdrop-filter: $value ;
|
||||
-webkit-backdrop-filter: $value;
|
||||
}
|
||||
|
||||
// 1像素
|
||||
@mixin divider-1px(){
|
||||
content:'';
|
||||
height: 1px;
|
||||
display: block;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
background-color: $color-border;
|
||||
@include transform(scaleY(.5))
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
$height-input: 24px;
|
||||
$padding-input-tb: 3px;
|
||||
$radius: 3px;
|
||||
|
||||
// Colors
|
||||
$green : #4CD964;
|
||||
$cyan : #5AC8FA;
|
||||
$blue : #007AFF;
|
||||
$purple : #5856D6;
|
||||
$roseo : #FF2D70;
|
||||
$red : #FF3B30;
|
||||
$orange : #FF9500;
|
||||
$yellow : #FFCC00;
|
||||
$gray : #8E8E93;
|
||||
|
||||
//ELEMTENT COLORS
|
||||
$color-main : $roseo;
|
||||
$border-color-nav: transparentize($color-main, 0.8);
|
||||
|
||||
//BACKGROUND COLOR
|
||||
$bg-td: #eee;
|
||||
$bg-panel: #f3f3f3;
|
||||
$bg-panel-tool: #f9f9f9;
|
||||
|
||||
// BG Scrollbar
|
||||
$bg-scroll-thumb: #cdcdcd;
|
||||
|
||||
|
||||
$fz-title: 15px;
|
||||
|
||||
|
||||
$color-primary : #409EFF;
|
||||
$color-success : #67C23A;
|
||||
$color-warning : #E6A23C;
|
||||
$color-danger : #F56C6C;
|
||||
$color-info : #909399;
|
||||
|
||||
$color-border: #eee;
|
||||
$border-color-panel: #e6e6e6;
|
||||
$border-color-input : #ccc;
|
||||
|
||||
$text-main : #000;
|
||||
$text-title : #333;
|
||||
$text-subtitle : #848484;
|
||||
$text-comment : #c2c2c2;
|
||||
$text-tool-title: #383f4a;
|
||||
|
||||
// USER CENTER
|
||||
$gradient-blue : $cyan, $blue;
|
||||
$gradient-gray : $gray, darken($gray, 15%);
|
||||
$gradient-green : $green, darken($green, 15%);
|
||||
$gradient-orange : $orange, darken($orange, 5%);
|
||||
$gradient-red : $red, darken($red, 15%);
|
||||
$gradient-purple : $purple, darken($purple, 15%);
|
||||
$gradient-magenta : $roseo, darken($roseo, 15%);
|
||||
$gradient-yellow : $yellow, darken($yellow, 15%);
|
||||
$gradient-time : #79C1F0, #7E87EF;
|
|
@ -0,0 +1,73 @@
|
|||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
color: inherit;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
font-family: "PingFang SC", "Microsoft Yahei UI", "Microsoft Yahei", "Helvetica", sans-serif;;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
select {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
code,
|
||||
kbd,
|
||||
pre,
|
||||
samp,
|
||||
tt {
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
}
|
||||
:focus,
|
||||
:active {
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
outline: none;
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
z-index: 50;
|
||||
width: 10px;
|
||||
height: 3px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
-webkit-border-radius: 5px;
|
||||
-moz-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
transition: all .2s;
|
||||
height: 20px;
|
||||
}
|
||||
:hover::-webkit-scrollbar-thumb {
|
||||
transition: all .2s;
|
||||
}
|
||||
::-webkit-scrollbar-button {
|
||||
display: none;
|
||||
}
|
||||
::-webkit-scrollbar-corner {
|
||||
display: none;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #ddd;
|
||||
}
|
||||
:hover::-webkit-scrollbar-thumb {
|
||||
background-color: #ccc;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,50 @@
|
|||
@import "variables";
|
||||
@import "reset.css";
|
||||
@import "font";
|
||||
@import "utility";
|
||||
@import "normalization";
|
||||
@import "gutter";
|
||||
@import "animate";
|
||||
@import "list";
|
||||
@import "button";
|
||||
@import "searchbar";
|
||||
@import "footer";
|
||||
@import "group";
|
||||
@import "dropdown";
|
||||
@import "config";
|
||||
@import "tool";
|
||||
@import "schema-list";
|
||||
@import "app";
|
||||
@import "dark";
|
||||
|
||||
|
||||
|
||||
body{
|
||||
font-size: 12px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.container{
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content{
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.shadow{
|
||||
@include box-shadow(1px 2px 3px rgba(0,0,0,0.2))
|
||||
}
|
||||
|
||||
.hidden{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.prototype{
|
||||
@extend .hidden;
|
||||
}
|
||||
|
||||
[v-cloak] {
|
||||
display: none
|
||||
}
|
|
@ -0,0 +1,286 @@
|
|||
// 字典对象
|
||||
const Word = require("./Word")
|
||||
const WordGroup = require("./WordGroup")
|
||||
const {shakeDom, log, shakeDomFocus} = require('./Utility')
|
||||
|
||||
const os = require('os')
|
||||
|
||||
class Dict {
|
||||
constructor(fileContent, fileName, filePath) {
|
||||
this.dictTypeName = 'Dict'
|
||||
this.filePath = filePath // 文件路径
|
||||
this.fileName = fileName // 文件名字
|
||||
this.header = null // 文件头部内容
|
||||
this.wordsOrigin = [] // 文件词条数组
|
||||
this.lastIndex = 0 // 最后一个Word Index 的值,用于新添加词时,作为唯一的 id 传入
|
||||
this.lastGroupIndex = 0 // 最后一个WordGroup Index 的值,用于新添加词时,作为唯一的 id 传入
|
||||
this.isGroupMode = false // 识别码表是否为分组形式的
|
||||
|
||||
let indexEndOfHeader = fileContent.indexOf('...')
|
||||
if (indexEndOfHeader < 0){
|
||||
log('文件格式错误,没有 ... 这一行')
|
||||
} else {
|
||||
this.indexEndOfHeader = indexEndOfHeader + 3
|
||||
this.header = fileContent.substring(0, this.indexEndOfHeader)
|
||||
this.isGroupMode = this.header.includes('dict_grouped: true') // 根据有没有这一段文字进行判断,是否为分组形式的码表
|
||||
let body = fileContent.substring(this.indexEndOfHeader)
|
||||
this.wordsOrigin = this.isGroupMode? this.getDictWordsInGroupMode(body): this.getDictWordsInNormalMode(body)
|
||||
}
|
||||
}
|
||||
// 总的词条数量
|
||||
get countDictOrigin(){
|
||||
if (this.isGroupMode){
|
||||
let countOrigin = 0
|
||||
this.wordsOrigin.forEach(group => {
|
||||
countOrigin = countOrigin + group.dict.length
|
||||
})
|
||||
return countOrigin
|
||||
} else {
|
||||
return this.wordsOrigin.length
|
||||
}
|
||||
}
|
||||
|
||||
// 返回所有 word
|
||||
getDictWordsInNormalMode(fileContent){
|
||||
let startPoint = new Date().getTime()
|
||||
let EOL = this.getFileEOLFrom(fileContent)
|
||||
let lines = fileContent.split(EOL) // 拆分词条与编码成单行
|
||||
this.lastIndex = lines.length
|
||||
let linesValid = lines.filter(item => item.indexOf('\t') > -1) // 选取包含 \t 的行
|
||||
let words = []
|
||||
linesValid.forEach((item, index) => {
|
||||
let currentWord = getWordFromLine(index, item)
|
||||
words.push(currentWord) // 获取词条
|
||||
})
|
||||
log(`处理yaml码表文件:完成,共:${words.length } ${this.isGroupMode? '组': '条'},用时 ${new Date().getTime() - startPoint} ms`)
|
||||
return words
|
||||
}
|
||||
|
||||
// 返回 word 分组
|
||||
getDictWordsInGroupMode(fileContent){
|
||||
let startPoint = new Date().getTime()
|
||||
let EOL = this.getFileEOLFrom(fileContent)
|
||||
let lines = fileContent.split(EOL) // 拆分词条与编码成单行
|
||||
let wordsGroup = [] // 总分组
|
||||
let temp = null // 第一个分组
|
||||
let lastItemIsEmptyLine = false // 上一条是空,用于循环中判断是否需要新起一个 WordGroup
|
||||
this.lastIndex = lines.length
|
||||
lines.forEach((item, index) => {
|
||||
if (item.startsWith('##')) { // 注释分组
|
||||
if (temp && temp.groupName) { // 如果上一个已经有名字了,说明需要保存
|
||||
wordsGroup.push(temp)
|
||||
}
|
||||
temp = new WordGroup(this.lastGroupIndex++, item.substring(3).trim())
|
||||
lastItemIsEmptyLine = false
|
||||
} else if (item.indexOf('\t') > -1) { // 是词条
|
||||
if (!temp){ // 第一行是词条时,没有分组名时
|
||||
temp = new WordGroup(this.lastGroupIndex++)
|
||||
}
|
||||
temp.dict.push(getWordFromLine(index, item))
|
||||
lastItemIsEmptyLine = false
|
||||
} else if (item.startsWith('#')) { // 注释
|
||||
log(item)
|
||||
lastItemIsEmptyLine = false
|
||||
} else {
|
||||
// 为空行时
|
||||
if (lastItemIsEmptyLine){
|
||||
// 上行和本行都是空行
|
||||
} else {
|
||||
if (temp){
|
||||
temp.groupName = temp.groupName || '未命名'
|
||||
wordsGroup.push(temp)
|
||||
temp = new WordGroup(this.lastGroupIndex++)
|
||||
}
|
||||
}
|
||||
lastItemIsEmptyLine = true
|
||||
}
|
||||
})
|
||||
log(`处理yaml码表文件:完成,共:${wordsGroup.length } ${this.isGroupMode? '组': '条'},用时 ${new Date().getTime() - startPoint} ms`)
|
||||
if (temp){
|
||||
if (temp.dict.length > 0){
|
||||
wordsGroup.push(temp) // 加上最后一个
|
||||
}
|
||||
return wordsGroup
|
||||
} else {
|
||||
return [] // 文件内容为空时
|
||||
}
|
||||
}
|
||||
// 判断码表文件的换行符是 \r\n 还是 \n
|
||||
getFileEOLFrom(fileContent){
|
||||
if(fileContent.indexOf('\r\n') > 0){
|
||||
return '\r\n'
|
||||
} else {
|
||||
return '\n'
|
||||
}
|
||||
}
|
||||
|
||||
// 排序
|
||||
sort(groupIndex){
|
||||
let startPoint = new Date().getTime()
|
||||
if (this.isGroupMode){ // group mode
|
||||
if (groupIndex !== -1){ // -1 代表全部
|
||||
this.wordsOrigin[groupIndex].dict.sort((a,b) => a.code < b.code ? -1: 1)
|
||||
} else {
|
||||
this.wordsOrigin.forEach(group => {
|
||||
group.dict.sort((a,b) => a.code < b.code ? -1: 1)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
this.wordsOrigin.sort((a,b) => a.code < b.code ? -1: 1)
|
||||
}
|
||||
log(`Sort 用时 ${new Date().getTime() - startPoint} ms`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加新 Word
|
||||
* @param word Word
|
||||
* @param groupIndex Number
|
||||
*/
|
||||
addNewWord(word, groupIndex){
|
||||
if(this.isGroupMode){
|
||||
if (groupIndex !== -1){
|
||||
this.wordsOrigin[groupIndex].dict.push(word)
|
||||
} else {
|
||||
let newWordGroup = new WordGroup(this.lastGroupIndex++,'- 未命名 -',[word])
|
||||
this.wordsOrigin.unshift(newWordGroup) // 添加到第一组
|
||||
}
|
||||
} else {
|
||||
this.addWordToDictInOrder(word)
|
||||
}
|
||||
this.lastIndex = this.lastIndex + 1 // 新加的词添加后, lastIndex + 1
|
||||
}
|
||||
|
||||
// 依次序添加 words
|
||||
addWordsInOrder(words, groupIndex){
|
||||
let startPoint = new Date().getTime()
|
||||
if (this.isGroupMode && groupIndex !== -1){
|
||||
this.addWordToDictInOrderWithGroup(words, groupIndex)
|
||||
} else {
|
||||
words.forEach(word => {
|
||||
this.addWordToDictInOrder(word)
|
||||
})
|
||||
}
|
||||
log(`添加 ${words.length } 条词条到指定码表, 用时 ${new Date().getTime() - startPoint} ms`)
|
||||
}
|
||||
|
||||
// 依次序添加 word
|
||||
addWordToDictInOrder(word){
|
||||
let insetPosition = null // 插入位置 index
|
||||
for (let i=0; i<this.wordsOrigin.length-1; i++){ // -1 为了避免下面 i+1 为 undefined
|
||||
if (word.code >= this.wordsOrigin[i] && word.code <= this.wordsOrigin[i+1].code){
|
||||
insetPosition = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!insetPosition){ // 没有匹配到任何位置,添加到结尾
|
||||
insetPosition = this.wordsOrigin.length
|
||||
}
|
||||
let wordInsert = word.clone() // 断开与别一个 dict 的引用链接,新建一个 word 对象,不然两个 dict 引用同一个 word
|
||||
wordInsert.setId(this.lastIndex++) // 给新的 words 一个新的唯一 id
|
||||
this.wordsOrigin.splice(insetPosition, 0, wordInsert)
|
||||
}
|
||||
|
||||
|
||||
// 依次序添加 word groupMode
|
||||
addWordToDictInOrderWithGroup(words, groupIndex){
|
||||
let dictWords = this.wordsOrigin[groupIndex].dict
|
||||
log('TODO: add to group')
|
||||
words.forEach(word => {
|
||||
let insetPosition = null // 插入位置 index
|
||||
for (let i=0; i<dictWords.length-1; i++){ // -1 为了避免下面 i+1 为 undefined
|
||||
if (word.code >= dictWords[i] && word.code <= dictWords[i+1].code){
|
||||
insetPosition = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!insetPosition){ // 没有匹配到任何位置,添加到结尾
|
||||
insetPosition = dictWords.length
|
||||
}
|
||||
let wordInsert = word.clone() // 断开与别一个 dict 的引用链接,新建一个 word 对象,不然两个 dict 引用同一个 word
|
||||
wordInsert.id = dictWords.length + 1 // 给新的 words 一个新的唯一 id
|
||||
dictWords.splice(insetPosition, 0, wordInsert)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 删除词条
|
||||
deleteWords(wordIdSet, isDeleteInSelf){ // isDeleteInSelf 在移动词条到自己分组时使用,不删除空的分组
|
||||
if (this.isGroupMode){
|
||||
let deleteGroupIds = [] // 记录 words 为 0 的 group,最后删除分组
|
||||
this.wordsOrigin.forEach((group, index) => {
|
||||
group.dict = group.dict.filter(item => !wordIdSet.has(item.id))
|
||||
if (group.dict.length === 0){
|
||||
deleteGroupIds.push(index)
|
||||
}
|
||||
})
|
||||
// config: 是否删除空的分组
|
||||
if (!isDeleteInSelf){
|
||||
this.wordsOrigin = this.wordsOrigin.filter((group, index) => !deleteGroupIds.includes(index))
|
||||
}
|
||||
} else {
|
||||
this.wordsOrigin = this.wordsOrigin.filter(item => !wordIdSet.has(item.id))
|
||||
}
|
||||
}
|
||||
|
||||
addGroupBeforeId(groupIndex){
|
||||
this.wordsOrigin.splice(groupIndex,0,new WordGroup(this.lastGroupIndex++,'',[],true))
|
||||
}
|
||||
|
||||
// 分组模式:删除分组
|
||||
deleteGroup(groupId){
|
||||
log('要删除的分组 id: ',groupId)
|
||||
this.wordsOrigin = this.wordsOrigin.filter(group => group.id !== groupId)
|
||||
}
|
||||
// 转为 yaml String
|
||||
toYamlString(){
|
||||
let yamlBody = ''
|
||||
if (this.isGroupMode){
|
||||
this.wordsOrigin.forEach(group => {
|
||||
let tempGroupString = ''
|
||||
tempGroupString = tempGroupString + `## ${group.groupName}${os.EOL}` // + groupName
|
||||
group.dict.forEach(item =>{
|
||||
tempGroupString = tempGroupString + item.toYamlString() + os.EOL
|
||||
})
|
||||
yamlBody = yamlBody + tempGroupString + os.EOL // 每组的末尾加个空行
|
||||
})
|
||||
return this.header + os.EOL + yamlBody
|
||||
} else {
|
||||
let yamlBody = ''
|
||||
this.wordsOrigin.forEach(item =>{
|
||||
yamlBody = yamlBody + item.toYamlString() + os.EOL
|
||||
})
|
||||
return this.header + os.EOL + yamlBody
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 在 origin 中调换两个词条的位置
|
||||
exchangePositionInOrigin(word1, word2){
|
||||
// 确保 word1 在前
|
||||
if (parseInt(word1.id) > parseInt(word2.id)){
|
||||
let temp = word1
|
||||
word1 = word2
|
||||
word2 = temp
|
||||
}
|
||||
for(let i=0; i<this.wordsOrigin.length; i++){
|
||||
let tempWord = this.wordsOrigin[i]
|
||||
if (tempWord.isEqualTo(word1)){
|
||||
this.wordsOrigin[i] = word2
|
||||
}
|
||||
if (tempWord.isEqualTo(word2)){
|
||||
this.wordsOrigin[i] = word1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 从一条词条字符串中获取 word 对象
|
||||
function getWordFromLine(index, lineStr){
|
||||
let wordArray = lineStr.split('\t')
|
||||
let code = wordArray[1]
|
||||
let word = wordArray[0]
|
||||
let priority = wordArray.length > 2 ? wordArray[2] : null
|
||||
return new Word(index, code, word, priority)
|
||||
}
|
||||
module.exports = Dict
|
|
@ -0,0 +1,121 @@
|
|||
// 单字字典
|
||||
const Word = require("./Word")
|
||||
const {log} = require('./Utility')
|
||||
const os = require('os')
|
||||
|
||||
// 只接受 一词一码 的码表文件
|
||||
class DictMap {
|
||||
constructor(fileContent, filename, filePath) {
|
||||
this.dictTypeName = 'DictMap'
|
||||
this.filePath = filePath // 文件路径
|
||||
this.filename = filename // 文件路径
|
||||
this.lastIndex = 0 // 最后一个 Index 的值,用于新添加词时,作为唯一的 id 传入
|
||||
this.seperator = '\t' // 间隔符为 tab
|
||||
this.characterMap = new Map() // 单字码表,用于根据此生成词语码表
|
||||
this.wordsOrigin = this.getDictWordsInNormalMode(fileContent)
|
||||
}
|
||||
// 总的词条数量
|
||||
get countDictOrigin(){
|
||||
return this.wordsOrigin.length
|
||||
}
|
||||
|
||||
// 返回所有 word
|
||||
getDictWordsInNormalMode(fileContent){
|
||||
this.characterMap = new Map() // 单字码表,用于根据此生成词语码表
|
||||
|
||||
// 处理 rime 码表
|
||||
let indexEndOfHeader = fileContent.indexOf('...')
|
||||
let bodyString = ''
|
||||
if (indexEndOfHeader > 0){
|
||||
bodyString = fileContent.substring(this.indexEndOfHeader)
|
||||
} else {
|
||||
bodyString = fileContent
|
||||
}
|
||||
// 处理词条
|
||||
let startPoint = new Date().getTime()
|
||||
let lines = bodyString.split(os.EOL) // 拆分词条与编码成单行
|
||||
this.lastIndex = lines.length + 1
|
||||
let linesValid = lines.filter(item => item.indexOf(this.seperator) > -1) // 选取包含分隔符的行
|
||||
let words = []
|
||||
log('正常词条的行数:',linesValid.length)
|
||||
linesValid.forEach(item => {
|
||||
let currentWords = this.getWordsFromLine(item)
|
||||
words.push(...currentWords) // 拼接词组
|
||||
currentWords.forEach(currentWord => {
|
||||
if (currentWord.word.length === 1
|
||||
&& currentWord.code.length >=2
|
||||
&& !this.characterMap.has(currentWord.word)) // map里不存在这个字
|
||||
{ // 编码长度为 4 的单字
|
||||
this.characterMap.set(currentWord.word, currentWord.code)
|
||||
}
|
||||
})
|
||||
})
|
||||
log(`处理文件完成,共:${words.length } 条,用时 ${new Date().getTime() - startPoint} ms`)
|
||||
return words
|
||||
}
|
||||
|
||||
decodeWord(word){
|
||||
try{
|
||||
let decodeArray = [] // 每个字解码后的数组表
|
||||
let letterArray = word.split('')
|
||||
if (letterArray.length > 4){ // 只截取前三和后一
|
||||
letterArray.splice(3,letterArray.length - 4)
|
||||
}
|
||||
letterArray.forEach(ch => {
|
||||
decodeArray.push(this.characterMap.get(ch) || '')
|
||||
})
|
||||
let phraseCode = ''
|
||||
switch (decodeArray.length){
|
||||
case 0:
|
||||
case 1:
|
||||
break
|
||||
case 2: // 取一的前二码,二的前二码
|
||||
phraseCode =
|
||||
decodeArray[0].substring(0,2) +
|
||||
decodeArray[1].substring(0,2)
|
||||
break
|
||||
case 3: // 取一二前一码,三前二码
|
||||
phraseCode =
|
||||
decodeArray[0].substring(0,1) +
|
||||
decodeArray[1].substring(0,1) +
|
||||
decodeArray[2].substring(0,2)
|
||||
break
|
||||
default: // 取一二三前一码,最后的一码
|
||||
phraseCode =
|
||||
decodeArray[0].substring(0,1) +
|
||||
decodeArray[1].substring(0,1) +
|
||||
decodeArray[2].substring(0,1) +
|
||||
decodeArray[decodeArray.length - 1].substring(0,1)
|
||||
}
|
||||
log(phraseCode, decodeArray)
|
||||
return phraseCode
|
||||
} catch(err){
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
toExportString(){
|
||||
let startPoint = new Date().getTime()
|
||||
let fileContentString = ''
|
||||
this.characterMap.forEach((code, word) => {
|
||||
fileContentString = fileContentString.concat(word, this.seperator, code, os.EOL)
|
||||
})
|
||||
log(`字典词条文本已生成,用时 ${new Date().getTime() - startPoint} ms`)
|
||||
return fileContentString
|
||||
}
|
||||
|
||||
// 从一条词条字符串中获取 word 对象,只取单字的
|
||||
// 单字时返回,多字时返回空
|
||||
getWordsFromLine(lineStr){
|
||||
let wordArray = lineStr.split(this.seperator)
|
||||
let word = wordArray[0]
|
||||
let code = wordArray[1]
|
||||
if (word.length > 1){
|
||||
return []
|
||||
} else {
|
||||
return [new Word(this.lastIndex++, code, word)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DictMap
|
|
@ -0,0 +1,269 @@
|
|||
// 其它字典对象
|
||||
const Word = require("./Word")
|
||||
const {shakeDom, log, shakeDomFocus} = require('./Utility')
|
||||
|
||||
const os = require('os')
|
||||
|
||||
class DictOther {
|
||||
constructor(fileContent, filename, filePath, seperator, dictFormat) {
|
||||
this.dictTypeName = 'DictOther'
|
||||
this.filePath = filePath // 文件路径
|
||||
this.filename = filename // 文件路径
|
||||
this.lastIndex = 0 // 最后一个 Index 的值,用于新添加词时,作为唯一的 id 传入
|
||||
this.seperator = seperator ||' ' // 默认间隔符为空格
|
||||
this.dictFormat = dictFormat || 'cww' // 码表格式: 一码多词什么的 cww: 一码多词 | wc: 一词一码 | cw: 一码一词
|
||||
this.characterMap = new Map() // 单字码表,用于根据此生成词语码表
|
||||
this.wordsOrigin = this.getDictWordsInNormalMode(fileContent)
|
||||
}
|
||||
// 总的词条数量
|
||||
get countDictOrigin(){
|
||||
return this.wordsOrigin.length
|
||||
}
|
||||
|
||||
// 设置 seperator
|
||||
setSeperator(seperator){
|
||||
this.seperator = seperator
|
||||
}
|
||||
// 设置 dictFormat
|
||||
setDictFormat(dictFormat){
|
||||
this.dictFormat = dictFormat
|
||||
}
|
||||
|
||||
// 获取指定字数的词条组
|
||||
getWordsLengthOf(length){
|
||||
switch (length){
|
||||
case 0:
|
||||
return this.wordsOrigin
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
case 4:
|
||||
return this.wordsOrigin.filter(word => word.word.length === length)
|
||||
default:
|
||||
return this.wordsOrigin.filter(word => word.word.length > 4)
|
||||
}
|
||||
}
|
||||
|
||||
// 查重,返回重复定义的字词
|
||||
// includeCharacter 当包含单字时
|
||||
getRepetitionWords(filterSingleCharacter){
|
||||
let startPoint = new Date().getTime()
|
||||
let wordMap = new Map()
|
||||
let repetitionWords = []
|
||||
this.wordsOrigin.forEach(word => {
|
||||
if (filterSingleCharacter){
|
||||
if (wordMap.has(word.word) && word.word.length === 1){
|
||||
repetitionWords.push(word)
|
||||
let matchedWord = wordMap.get(word.word)
|
||||
if (matchedWord) repetitionWords.push(matchedWord)
|
||||
} else { // 如果 map 中没有这个词的记录,添加这个记录
|
||||
wordMap.set(word.word, word)
|
||||
}
|
||||
} else {
|
||||
if (wordMap.has(word.word) && word.word.length > 1){ // 单字没必要查重,所以这里只搜索 2 个字以上的词
|
||||
repetitionWords.push(word)
|
||||
let matchedWord = wordMap.get(word.word)
|
||||
if (matchedWord) repetitionWords.push(matchedWord)
|
||||
} else { // 如果 map 中没有这个词的记录,添加这个记录
|
||||
wordMap.set(word.word, word)
|
||||
}
|
||||
}
|
||||
})
|
||||
// 排序后再去除重复项
|
||||
repetitionWords.sort((a, b) => {
|
||||
// log(a.word + a.code, b.word + b.code)
|
||||
return (a.word + a.code) > (b.word + b.code) ? 1 : -1
|
||||
})
|
||||
log('重复词条数量:未去重之前 ', repetitionWords.length)
|
||||
|
||||
for (let i = 0; i < repetitionWords.length - 1; i++) {
|
||||
if (repetitionWords[i].id === repetitionWords[i + 1].id ) {
|
||||
repetitionWords.splice(i,1)
|
||||
i = i - 1
|
||||
}
|
||||
}
|
||||
log(`查重完成,用时 ${new Date().getTime() - startPoint} ms`)
|
||||
log('词条字典数量: ', wordMap.size)
|
||||
log('重复词条数量: ', repetitionWords.length)
|
||||
log('重复 + 词条字典 = ', repetitionWords.length + wordMap.size)
|
||||
return repetitionWords
|
||||
}
|
||||
|
||||
// 返回所有 word
|
||||
getDictWordsInNormalMode(fileContent){
|
||||
let startPoint = new Date().getTime()
|
||||
let EOL = this.getFileEOLFrom(fileContent)
|
||||
let lines = fileContent.split(EOL) // 拆分词条与编码成单行
|
||||
this.lastIndex = lines.length + 1
|
||||
// 如果为纯词模式,就使用所有的行,否则就根据分隔符进行筛选
|
||||
let linesValid = this.dictFormat === 'w'? lines: lines.filter(item => item.indexOf(this.seperator) > -1)
|
||||
let words = []
|
||||
log('正常词条的行数:',linesValid.length)
|
||||
linesValid.forEach(item => {
|
||||
let currentWords = this.getWordsFromLine(item)
|
||||
words.push(...currentWords) // 拼接词组
|
||||
currentWords.forEach(currentWord => {
|
||||
if (currentWord.word.length === 1
|
||||
&& currentWord.code.length >=2
|
||||
&& !this.characterMap.has(currentWord.word)) // map里不存在这个字
|
||||
{ // 编码长度为 4 的单字
|
||||
this.characterMap.set(currentWord.word, currentWord.code)
|
||||
}
|
||||
})
|
||||
})
|
||||
log(`处理文件完成,共:${words.length } 条,用时 ${new Date().getTime() - startPoint} ms`)
|
||||
return words
|
||||
}
|
||||
|
||||
// 排序
|
||||
sort(){
|
||||
let startPoint = new Date().getTime()
|
||||
this.wordsOrigin.sort((a,b) => a.code < b.code ? -1: 1)
|
||||
log(`排序用时 ${new Date().getTime() - startPoint} ms`)
|
||||
}
|
||||
|
||||
|
||||
// 依次序添加 words
|
||||
addWordsInOrder(words){
|
||||
let startPoint = new Date().getTime()
|
||||
words.forEach(word => {
|
||||
this.addWordToDictInOrder(word)
|
||||
})
|
||||
log(`添加 ${words.length } 条词条到指定码表, 用时 ${new Date().getTime() - startPoint} ms`)
|
||||
}
|
||||
|
||||
// 依次序添加 word
|
||||
addWordToDictInOrder(word){
|
||||
let insetPosition = null // 插入位置 index
|
||||
this.sort() // 插入之前排序码表
|
||||
for (let i=0; i<this.wordsOrigin.length-1; i++){ // -1 为了避免下面 i+1 为 undefined
|
||||
if (word.code >= this.wordsOrigin[i] && word.code <= this.wordsOrigin[i+1].code){
|
||||
insetPosition = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!insetPosition){ // 没有匹配到任何位置,添加到结尾
|
||||
insetPosition = this.wordsOrigin.length
|
||||
}
|
||||
let wordInsert = word.clone() // 断开与别一个 dict 的引用链接,新建一个 word 对象,不然两个 dict 引用同一个 word
|
||||
wordInsert.setId(this.lastIndex++) // 给新的 words 一个新的唯一 id
|
||||
this.wordsOrigin.splice(insetPosition, 0, wordInsert)
|
||||
}
|
||||
|
||||
// 判断码表文件的换行符是 \r\n 还是 \n
|
||||
getFileEOLFrom(fileContent){
|
||||
if(fileContent.indexOf('\r\n') > 0){
|
||||
return '\r\n'
|
||||
} else {
|
||||
return '\n'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 删除词条
|
||||
deleteWords(wordIdSet){
|
||||
this.wordsOrigin = this.wordsOrigin.filter(item => !wordIdSet.has(item.id))
|
||||
}
|
||||
|
||||
// 转为 String
|
||||
toYamlString(){
|
||||
let fileContentString = ''
|
||||
this.wordsOrigin.forEach(item =>{
|
||||
fileContentString = fileContentString + item.toYamlString() + os.EOL
|
||||
})
|
||||
return fileContentString
|
||||
}
|
||||
|
||||
toExportString(seperator, dictFormat){
|
||||
let startPoint = new Date().getTime()
|
||||
let fileContentString = ''
|
||||
switch (dictFormat){
|
||||
case 'cww':
|
||||
let codeMap = new Map() // code: [word, word, word]
|
||||
this.wordsOrigin.forEach((word, index) => {
|
||||
let code = word.code
|
||||
if (codeMap.has(code)){ // 用 map 记录所有 code, 如果有就添加到对应的 value 中,没有就新增 map item
|
||||
codeMap.set(code, codeMap.get(code).concat(word))
|
||||
} else {
|
||||
codeMap.set(code, [word])
|
||||
}
|
||||
})
|
||||
codeMap.forEach((wordArray, code) => {
|
||||
let oneCodewordsString = ''
|
||||
wordArray.forEach(item => {oneCodewordsString = oneCodewordsString.concat(seperator + item.word)}) // seperater + wordsString
|
||||
fileContentString = fileContentString.concat(code, oneCodewordsString, os.EOL)
|
||||
})
|
||||
log(`词条文本已生成,用时 ${new Date().getTime() - startPoint} ms`)
|
||||
return fileContentString
|
||||
case 'cw':
|
||||
this.wordsOrigin.forEach(word => {
|
||||
fileContentString = fileContentString.concat(word.toFileString(seperator, true), os.EOL)
|
||||
})
|
||||
log(`词条文本已生成,用时 ${new Date().getTime() - startPoint} ms`)
|
||||
return fileContentString
|
||||
case 'wc':
|
||||
this.wordsOrigin.forEach(word => {
|
||||
fileContentString = fileContentString.concat(word.toFileString(seperator, false), os.EOL)
|
||||
})
|
||||
log(`词条文本已生成,用时 ${new Date().getTime() - startPoint} ms`)
|
||||
return fileContentString
|
||||
case 'w':
|
||||
this.wordsOrigin.forEach(word => {
|
||||
fileContentString = fileContentString.concat(word.word, os.EOL)
|
||||
})
|
||||
log(`词条文本已生成,用时 ${new Date().getTime() - startPoint} ms`)
|
||||
return fileContentString
|
||||
}
|
||||
}
|
||||
|
||||
// 在 origin 中调换两个词条的位置
|
||||
exchangePositionInOrigin(word1, word2){
|
||||
// 确保 word1 在前
|
||||
if (parseInt(word1.id) > parseInt(word2.id)){
|
||||
let temp = word1
|
||||
word1 = word2
|
||||
word2 = temp
|
||||
}
|
||||
for(let i=0; i<this.wordsOrigin.length; i++){
|
||||
let tempWord = this.wordsOrigin[i]
|
||||
if (tempWord.isEqualTo(word1)){
|
||||
this.wordsOrigin[i] = word2
|
||||
}
|
||||
if (tempWord.isEqualTo(word2)){
|
||||
this.wordsOrigin[i] = word1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从一条词条字符串中获取 word 对象
|
||||
// 一编码对应多词
|
||||
getWordsFromLine(lineStr){
|
||||
let wordArray = lineStr.split(this.seperator)
|
||||
let words = []
|
||||
let code, word
|
||||
switch (this.dictFormat){
|
||||
case 'cww':
|
||||
code = wordArray[0]
|
||||
for(let i=1; i<wordArray.length;i++){
|
||||
words.push(new Word(this.lastIndex, code, wordArray[i]))
|
||||
this.lastIndex = this.lastIndex + 1
|
||||
}
|
||||
return words
|
||||
case 'cw':
|
||||
code = wordArray[0]
|
||||
word = wordArray[1]
|
||||
return [new Word(this.lastIndex++, code, word)]
|
||||
case 'wc':
|
||||
word = wordArray[0]
|
||||
code = wordArray[1]
|
||||
return [new Word(this.lastIndex++, code, word)]
|
||||
case 'w':
|
||||
word = wordArray[0]
|
||||
// code = getCodeFromWord(word)
|
||||
return [new Word(this.lastIndex++, '', word)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = DictOther
|
|
@ -0,0 +1,23 @@
|
|||
const IS_IN_DEVELOP = false // 生产
|
||||
// const IS_IN_DEVELOP = true // 开发
|
||||
const CONFIG_FILE_NAME = 'config.json' // 配置文件 文件名
|
||||
const CONFIG_DICT_MAP_FILE_NAME = 'dict_map.txt' // 编码生成用的字典码表文件
|
||||
const CONFIG_FILE_PATH = 'wubi-dict-editor' // 配置文件存放的目录
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
initFileName: 'wubi86_jidian_user.dict.yaml', // 初始文件信息
|
||||
autoDeployOnAdd: false, // 添词后 是否自动布署
|
||||
autoDeployOnDelete: false, // 删词后 是否自动布署
|
||||
autoDeployOnEdit: false, // 编辑词条后 是否自动布署
|
||||
enterKeyBehavior: 'add', // add | search
|
||||
rimeHomeDir: '', // 配置文件主目录
|
||||
searchMethod: 'both', // 搜索匹配的内容 code | phrase | both | any
|
||||
chosenGroupIndex: -1, // 列表中选定的分组 id
|
||||
theme: 'auto', // auto 跟随系统 | black
|
||||
hasSetDictMap: false, // 是否已经设置字典码表文件
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
IS_IN_DEVELOP, CONFIG_FILE_NAME,CONFIG_FILE_PATH, DEFAULT_CONFIG, CONFIG_DICT_MAP_FILE_NAME
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
function $(selector){
|
||||
return document.querySelector(selector)
|
||||
}
|
||||
|
||||
// 抖动 dom 元素
|
||||
function shakeDom(dom){
|
||||
let animateClass = 'shake';
|
||||
dom.classList.add('animated');
|
||||
dom.classList.add(animateClass);
|
||||
setTimeout(()=>{
|
||||
dom.classList.remove('animated')
|
||||
dom.classList.remove(animateClass)
|
||||
}, 250)
|
||||
}
|
||||
|
||||
// 抖动 dom 元素 并 聚焦
|
||||
function shakeDomFocus(dom){
|
||||
let animateClass = 'shake';
|
||||
dom.classList.add('animated');
|
||||
dom.classList.add(animateClass);
|
||||
setTimeout(()=>{
|
||||
dom.classList.remove('animated')
|
||||
dom.classList.remove(animateClass)
|
||||
}, 250)
|
||||
dom.focus()
|
||||
}
|
||||
|
||||
function log(...obj){
|
||||
console.log(...obj)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
shakeDom, shakeDomFocus, log
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
// 词条对象
|
||||
class Word{
|
||||
/**
|
||||
*
|
||||
* @param id Number
|
||||
* @param code String
|
||||
* @param word String
|
||||
* @param priority String
|
||||
*/
|
||||
constructor(id, code, word, priority) {
|
||||
this.id = id
|
||||
this.code = code
|
||||
this.word = word
|
||||
this.priority = priority || null
|
||||
}
|
||||
toString(){
|
||||
return this.id + '\t' + this.word + '\t' + this.code + '\t' + this.priority
|
||||
}
|
||||
toYamlString(){
|
||||
return this.word + '\t' + this.code
|
||||
}
|
||||
toFileString(seperator, codeFirst){
|
||||
if (codeFirst){
|
||||
return this.code + seperator + this.word
|
||||
} else {
|
||||
return this.word + seperator + this.code
|
||||
}
|
||||
}
|
||||
setCode(code){
|
||||
this.code = code
|
||||
}
|
||||
setId(id){
|
||||
this.id = id
|
||||
}
|
||||
// 复制一个对象
|
||||
clone(){
|
||||
return new Word(this.id, this.code, this.word, this.priority)
|
||||
}
|
||||
isEqualTo(word){
|
||||
return this.id === word.id
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Word
|
|
@ -0,0 +1,15 @@
|
|||
// 词条对象 分组
|
||||
class WordGroup{
|
||||
constructor(id, groupName, words, editing) {
|
||||
this.id = id
|
||||
this.groupName = groupName || ''
|
||||
this.dict = words || []
|
||||
this.isEditingTitle = editing || false // 标题是否在编辑
|
||||
}
|
||||
// 复制一个对象
|
||||
clone(){
|
||||
return new WordGroup(this.id, this.groupName, [...this.dict])
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WordGroup
|
|
@ -0,0 +1,664 @@
|
|||
const {app, BrowserWindow, Menu, ipcMain, shell, dialog} = require('electron')
|
||||
const { exec } = require('child_process')
|
||||
const fs = require('fs')
|
||||
const os = require('os')
|
||||
const url = require("url")
|
||||
const path = require("path")
|
||||
const {shakeDom, log, shakeDomFocus} = require('./js/Utility')
|
||||
const { IS_IN_DEVELOP, CONFIG_FILE_PATH, CONFIG_FILE_NAME, DEFAULT_CONFIG, CONFIG_DICT_MAP_FILE_NAME } = require('./js/Global')
|
||||
|
||||
let mainWindow // 主窗口
|
||||
let fileList = [] // 文件目录列表,用于移动词条
|
||||
|
||||
function createMainWindow() {
|
||||
let width = IS_IN_DEVELOP ? 1500: 1000
|
||||
let height = 700
|
||||
mainWindow = new BrowserWindow({
|
||||
width,
|
||||
height,
|
||||
icon: __dirname + '/assets/appIcon/appicon.ico', // windows icon
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false
|
||||
}
|
||||
})
|
||||
|
||||
if (IS_IN_DEVELOP){
|
||||
mainWindow.webContents.openDevTools() // 打开调试窗口
|
||||
}
|
||||
|
||||
mainWindow.loadURL(
|
||||
url.format({
|
||||
pathname: path.join(__dirname, './view/index/index.html'),
|
||||
protocol: "file:",
|
||||
slashes: true
|
||||
})
|
||||
)
|
||||
mainWindow.on('closed', function () {
|
||||
mainWindow = null
|
||||
if (configWindow) configWindow.close()
|
||||
if (toolWindow) toolWindow.close()
|
||||
})
|
||||
|
||||
|
||||
// 保存词库到文件
|
||||
ipcMain.on('saveFile', (event, filename, yamlString) => {
|
||||
fs.writeFile(path.join(getRimeConfigDir(), filename), yamlString, {encoding: "utf8"}, err => {
|
||||
if (!err){
|
||||
log('saveFileSuccess')
|
||||
applyRime() // 布署
|
||||
mainWindow.webContents.send('saveFileSuccess')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 监听 window 的文件载入请求
|
||||
ipcMain.on('loadInitDictFile', event => {
|
||||
let config = readConfigFile()
|
||||
readFileFromConfigDir(config.initFileName)
|
||||
})
|
||||
|
||||
// 监听载入主文件内容的请求
|
||||
ipcMain.on('loadDictFile', (event,filename) => {
|
||||
readFileFromConfigDir(filename)
|
||||
})
|
||||
|
||||
// 监听载入次文件内容的请求
|
||||
ipcMain.on('MainWindow:LoadSecondDict', (event, filename) => {
|
||||
let filePath = path.join(getRimeConfigDir(), filename)
|
||||
fs.readFile(filePath, {encoding: 'utf-8'}, (err, res) => {
|
||||
if(err){
|
||||
log(err)
|
||||
} else {
|
||||
mainWindow.webContents.send('setTargetDict', filename, filePath, res)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 监听载入主文件内容的请求
|
||||
ipcMain.on('loadMainDict', event => {
|
||||
let mainDictFileName = 'wubi86_jidian.dict.yaml'
|
||||
fs.readFile(path.join(getRimeConfigDir(), mainDictFileName), {encoding: 'utf-8'}, (err, res) => {
|
||||
if(err){
|
||||
log(err)
|
||||
} else {
|
||||
mainWindow.webContents.send('setMainDict', path.join(getRimeConfigDir(), mainDictFileName) ,res)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 外部打开当前码表文件
|
||||
ipcMain.on('openFileOutside', (event, filename) => {
|
||||
shell.openPath(path.join(getRimeConfigDir(), filename)).then(res => {
|
||||
log(res)
|
||||
}).catch(err => {
|
||||
log(err)
|
||||
})
|
||||
})
|
||||
ipcMain.on('GetFileList', event => {
|
||||
mainWindow.send('FileList', fileList)
|
||||
})
|
||||
|
||||
// config 相关,载入配置文件内容
|
||||
ipcMain.on('MainWindow:RequestConfigFile', event => {
|
||||
let config = readConfigFile() // 没有配置文件时,返回 false
|
||||
if (config){ // 如果有配置文件
|
||||
mainWindow.send('MainWindow:ResponseConfigFile', config) // 向窗口发送 config 内容
|
||||
}
|
||||
})
|
||||
// 保存配置文件内容
|
||||
ipcMain.on('saveConfigFileFromMainWindow', (event, configString) => {
|
||||
writeConfigFile(configString, mainWindow)
|
||||
})
|
||||
|
||||
// 响应所有请求 dictMap 的请求
|
||||
ipcMain.on('getDictMap', event => {
|
||||
let dictMapFilePath = path.join(getAppConfigDir(), CONFIG_DICT_MAP_FILE_NAME)
|
||||
let dictMapFileContent = readFileFromDisk(dictMapFilePath)
|
||||
if (dictMapFileContent){
|
||||
if (mainWindow) mainWindow.send('setDictMap', dictMapFileContent, CONFIG_DICT_MAP_FILE_NAME, dictMapFilePath)
|
||||
if (toolWindow) toolWindow.send('setDictMap', dictMapFileContent, CONFIG_DICT_MAP_FILE_NAME, dictMapFilePath)
|
||||
} else {
|
||||
// 如果没有设置码表字典文件,使用默认配置目录中的码表文件作为字典文件
|
||||
let rimeWubiDefaultDictFilePath = path.join(getRimeConfigDir(), 'wubi86_jidian.dict.yaml')
|
||||
let originalDictFileContent = readFileFromDisk(rimeWubiDefaultDictFilePath)
|
||||
if (originalDictFileContent){
|
||||
if (mainWindow) mainWindow.send('setDictMap', originalDictFileContent, CONFIG_DICT_MAP_FILE_NAME, dictMapFilePath)
|
||||
if (toolWindow) toolWindow.send('setDictMap', originalDictFileContent, CONFIG_DICT_MAP_FILE_NAME, dictMapFilePath)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 载入文件内容
|
||||
ipcMain.on('MainWindow:LoadFile', (event, fileName) => {
|
||||
readFileFromConfigDir(fileName, mainWindow)
|
||||
})
|
||||
}
|
||||
|
||||
let toolWindow
|
||||
function showToolWindow (){
|
||||
let width = IS_IN_DEVELOP ? 1400: 1000
|
||||
let height = IS_IN_DEVELOP ? 600: 600
|
||||
toolWindow = new BrowserWindow({
|
||||
width,
|
||||
height,
|
||||
icon: __dirname + '/assets/appIcon/appicon.ico', // windows icon
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false
|
||||
}
|
||||
})
|
||||
|
||||
if (IS_IN_DEVELOP){
|
||||
toolWindow.webContents.openDevTools() // 打开调试窗口
|
||||
}
|
||||
|
||||
toolWindow.loadURL(
|
||||
url.format({
|
||||
pathname: path.join(__dirname, 'view/tool/tool.html'),
|
||||
protocol: "file:",
|
||||
slashes: true
|
||||
})
|
||||
)
|
||||
toolWindow.on('closed', function () {
|
||||
let listeners = [
|
||||
'ToolWindow:RequestConfigFile',
|
||||
'ToolWindow:chooseDictFile',
|
||||
'ToolWindow:SaveFile',
|
||||
'ToolWindow:loadFileContent',
|
||||
'ToolWindow:openFileOutside',
|
||||
'ToolWindow:GetFileList',
|
||||
'ToolWindow:LoadTargetDict'
|
||||
]
|
||||
listeners.forEach(item => {
|
||||
ipcMain.removeAllListeners(item)
|
||||
})
|
||||
toolWindow = null
|
||||
if(mainWindow) mainWindow.show()
|
||||
})
|
||||
|
||||
|
||||
|
||||
// 选取码表文件目录
|
||||
ipcMain.on('ToolWindow:chooseDictFile', event => {
|
||||
let dictFilePath = dialog.showOpenDialogSync(toolWindow,{
|
||||
filters: [
|
||||
{ name: 'Text', extensions: ['text', 'txt', 'yaml'] },
|
||||
],
|
||||
properties: ['openFile'] // 选择文件
|
||||
})
|
||||
console.log(dictFilePath)
|
||||
if (dictFilePath){
|
||||
readFileFromDiskAndResponse(dictFilePath[0], toolWindow)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// 保存词库到文件
|
||||
ipcMain.on('ToolWindow:SaveFile', (event, filePath, fileConentString) => {
|
||||
fs.writeFile(filePath, fileConentString, {encoding: "utf8"}, err => {
|
||||
if (!err){
|
||||
log('saveFileSuccess')
|
||||
// applyRime() // 布署
|
||||
toolWindow.webContents.send('saveFileSuccess')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 监听 window 的文件载入请求
|
||||
ipcMain.on('ToolWindow:loadFileContent', (event, filePath) => {
|
||||
readFileFromDiskAndResponse( filePath, toolWindow)
|
||||
})
|
||||
|
||||
// 外部打开当前码表文件
|
||||
ipcMain.on('ToolWindow:openFileOutside', (event, filename) => {
|
||||
shell.openPath(path.join(getRimeConfigDir(), filename)).then(res => {
|
||||
log(res)
|
||||
}).catch(err => {
|
||||
log(err)
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.on('ToolWindow:GetFileList', event => {
|
||||
toolWindow.send('ToolWindow:FileList', fileList)
|
||||
})
|
||||
|
||||
// 监听载入次文件内容的请求
|
||||
ipcMain.on('ToolWindow:LoadTargetDict', (event, filename) => {
|
||||
let filePath = path.join(getRimeConfigDir(), filename)
|
||||
fs.readFile(filePath, {encoding: 'utf-8'}, (err, res) => {
|
||||
if(err){
|
||||
log(err)
|
||||
} else {
|
||||
toolWindow.webContents.send('ToolWindow:SetTargetDict',filename, filePath ,res)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// config 相关
|
||||
ipcMain.on('ToolWindow:RequestConfigFile', event => {
|
||||
let config = readConfigFile() // 没有配置文件时,返回 false
|
||||
|
||||
if (config){ // 如果有配置文件
|
||||
if (toolWindow){ // 如果有配置文件
|
||||
toolWindow.send('ToolWindow:ResponseConfigFile', config) // 向窗口发送 config 内容
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 读取文件 从硬盘
|
||||
function readFileFromDisk(filePath){
|
||||
try {
|
||||
return fs.readFileSync(filePath, {encoding: 'utf-8'})
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 读取文件并回馈给指定窗口
|
||||
function readFileFromDiskAndResponse(filePath, responseWindow){
|
||||
let fileName = path.basename(filePath) // 获取文件名
|
||||
let fileContent = readFileFromDisk(filePath)
|
||||
if (fileContent){
|
||||
responseWindow.send('showFileContent', fileName, filePath, fileContent)
|
||||
} else {
|
||||
log('读取文件错误')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
let configWindow
|
||||
function createConfigWindow() {
|
||||
let width = IS_IN_DEVELOP ? 1400 : 600
|
||||
let height = IS_IN_DEVELOP ? 600 : 600
|
||||
// TODO:打开配置窗口的时候,先创建配置文件夹,供后面保存配置文件和字典文件使用
|
||||
|
||||
// 判断 config 文件夹是否存在
|
||||
let configDir = getAppConfigDir()
|
||||
console.log(configDir)
|
||||
if (!fs.existsSync(configDir)){
|
||||
console.log('create config dir', configDir)
|
||||
fs.mkdirSync(configDir) // 创建目录
|
||||
|
||||
}
|
||||
|
||||
configWindow = new BrowserWindow({
|
||||
width,
|
||||
height,
|
||||
icon: __dirname + '/assets/appIcon/appicon.ico', // windows icon
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false
|
||||
}
|
||||
})
|
||||
|
||||
if (IS_IN_DEVELOP) {
|
||||
configWindow.webContents.openDevTools() // 打开调试窗口
|
||||
}
|
||||
|
||||
|
||||
configWindow.loadURL(
|
||||
url.format({
|
||||
pathname: path.join(__dirname, 'view/config/config.html'),
|
||||
protocol: "file:",
|
||||
slashes: true
|
||||
})
|
||||
)
|
||||
configWindow.on('closed', function () {
|
||||
let listeners = [
|
||||
'requestFileList',
|
||||
'ConfigWindow:RequestSaveConfig',
|
||||
'ConfigWindow:ChooseRimeHomeDir',
|
||||
'ConfigWindow:SetDictMapFile',
|
||||
]
|
||||
listeners.forEach(item => {
|
||||
ipcMain.removeAllListeners(item)
|
||||
})
|
||||
configWindow = null
|
||||
if(toolWindow) toolWindow.show()
|
||||
if(mainWindow) mainWindow.show()
|
||||
})
|
||||
|
||||
// 载入文件列表
|
||||
ipcMain.on('requestFileList', event => {
|
||||
configWindow.send('responseFileList', fileList)
|
||||
})
|
||||
|
||||
// config 相关
|
||||
ipcMain.on('ConfigWindow:RequestConfigFile', event => {
|
||||
let config = readConfigFile() // 没有配置文件时,返回 false
|
||||
if (config){ // 如果有配置文件
|
||||
if (configWindow){ // 如果有配置文件
|
||||
configWindow.send('ConfigWindow:ResponseConfigFile', config) // 向窗口发送 config 内容
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 保存配置文件内容
|
||||
ipcMain.on('ConfigWindow:RequestSaveConfig', (event, configString) => {
|
||||
writeConfigFile(configString)
|
||||
})
|
||||
|
||||
// 选取配置文件目录
|
||||
ipcMain.on('ConfigWindow:ChooseRimeHomeDir', event => {
|
||||
let rimeHomeDir = dialog.showOpenDialogSync(configWindow,{
|
||||
properties: ['openDirectory'] // 选择文件夹
|
||||
})
|
||||
if (rimeHomeDir){
|
||||
configWindow.send('ConfigWindow:ChoosenRimeHomeDir', rimeHomeDir)
|
||||
}
|
||||
})
|
||||
|
||||
// 选取编码字典文件
|
||||
ipcMain.on('ConfigWindow:SetDictMapFile', event => {
|
||||
// 获取文件码表文件路径,返回值为路径数组
|
||||
let dictMapPathArray = dialog.showOpenDialogSync(configWindow,{
|
||||
defaultPath: getRimeConfigDir(), // 默认为 Rime 配置文件目录
|
||||
filters: [
|
||||
{ name: '码表文件', extensions: ['text', 'txt', 'yaml'] },
|
||||
],
|
||||
properties: ['openFile'] // 选择文件夹
|
||||
})
|
||||
if (dictMapPathArray.length > 0){
|
||||
let filePath = dictMapPathArray[0]
|
||||
let fileName = path.basename(filePath) // 获取文件名
|
||||
let fileContent = readFileFromDisk(filePath)
|
||||
if (fileContent){
|
||||
configWindow.send('ConfigWindow:ShowDictMapContent', fileName, filePath, fileContent)
|
||||
} else {
|
||||
log('读取码表字典文件错误')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 保存 DictMap 文件
|
||||
ipcMain.on('ConfigWindow:SaveDictMapFile', ( event, fileContentString) => {
|
||||
let configPath = getAppConfigDir()
|
||||
console.log(configPath)
|
||||
fs.writeFile(
|
||||
path.join(configPath, CONFIG_DICT_MAP_FILE_NAME),
|
||||
fileContentString,
|
||||
{encoding: 'utf-8'},
|
||||
err => {
|
||||
if (err) {
|
||||
log(err)
|
||||
} else {
|
||||
configWindow.send('ConfigWindow:SaveDictMapSuccess')
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// config 文件保存在 用户文件夹下 / CONFIG_FILE_PATH/CONFIG_FILE_NAME 文件中
|
||||
function writeConfigFile(contentString){
|
||||
let configPath = getAppConfigDir()
|
||||
fs.writeFile(
|
||||
path.join(configPath, CONFIG_FILE_NAME),
|
||||
contentString, {encoding: 'utf-8'},
|
||||
err => {
|
||||
if (err) {
|
||||
log(err)
|
||||
} else {
|
||||
// 配置保存成功后,向主窗口发送配置文件内容
|
||||
if (toolWindow) toolWindow.send('ToolWindow:ResponseConfigFile', JSON.parse(contentString)) // 向窗口发送 config 内容
|
||||
if (mainWindow) mainWindow.send('MainWindow:ResponseConfigFile', JSON.parse(contentString)) // 向窗口发送 config 内容
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function readConfigFile(){
|
||||
let configPath = path.join(os.homedir(), CONFIG_FILE_PATH)
|
||||
try{ // 捕获读取文件时的错误,如果有配置文件 返回其内容,如果没有,返回 false
|
||||
let result = fs.readFileSync(path.join(configPath, CONFIG_FILE_NAME), {encoding: 'utf-8'})
|
||||
return JSON.parse(result)
|
||||
} catch (err){
|
||||
return DEFAULT_CONFIG
|
||||
}
|
||||
}
|
||||
|
||||
app.on('ready', ()=>{
|
||||
createMainWindow()
|
||||
getDictFileList() // 读取目录中的所有码表文件
|
||||
createMenu() // 创建菜单
|
||||
})
|
||||
|
||||
app.on('window-all-closed', function () {
|
||||
// if (process.platform !== 'darwin') app.quit()
|
||||
app.quit()
|
||||
})
|
||||
|
||||
app.on('activate', function () {
|
||||
if (mainWindow === null) {
|
||||
createMainWindow()
|
||||
}
|
||||
})
|
||||
|
||||
// 读取文件 从配置文件目录
|
||||
function readFileFromConfigDir(fileName, responseWindow){
|
||||
let rimeHomeDir = getRimeConfigDir()
|
||||
let filePath = path.join(rimeHomeDir, fileName)
|
||||
fs.readFile(filePath, {encoding: 'utf-8'}, (err, res) => {
|
||||
if(err){
|
||||
log(err)
|
||||
} else {
|
||||
if(responseWindow){
|
||||
responseWindow.send('showFileContent', fileName, filePath, res)
|
||||
} else {
|
||||
mainWindow.webContents.send('showFileContent', fileName, filePath, res)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 匹配文件名,返回对应文件的名字
|
||||
function getLabelNameFromFileName(fileName){
|
||||
let map = [
|
||||
{name: '拼音词库', path: 'pinyin_simp.dict.yaml'},
|
||||
{name: '⒈ 五笔极点 - 主表', path: 'wubi86_jidian.dict.yaml'},
|
||||
{name: '⒉ 五笔极点 - 临时', path: 'wubi86_jidian_addition.dict.yaml'},
|
||||
{name: '⒊ 五笔极点 - 附加', path: 'wubi86_jidian_extra.dict.yaml'},
|
||||
{name: '⒋ 五笔极点 - 用户', path: 'wubi86_jidian_user.dict.yaml'},
|
||||
{name: '⒌ 五笔极点 - 英文', path: 'wubi86_jidian_english.dict.yaml'},
|
||||
|
||||
// 测试词库
|
||||
{name: '测试 - 主表 ⛳', path: 'test_main.dict.yaml'},
|
||||
{name: '测试 - 分组 ⛳', path: 'test_group.dict.yaml'},
|
||||
{name: '测试 - 普通 ⛳', path: 'test.dict.yaml'},
|
||||
]
|
||||
let matchedPath = map.filter(item => item.path === fileName)
|
||||
// 返回匹配的名字,或者返回原文件名
|
||||
return matchedPath.length > 0 ? matchedPath[0].name: fileName.substring(0, fileName.indexOf('.dict.yaml'))
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 创建 menu
|
||||
function createMenu() {
|
||||
let menuStructure = [
|
||||
{
|
||||
label: '词库工具',
|
||||
submenu: [
|
||||
{
|
||||
label: '配置',
|
||||
click() {
|
||||
createConfigWindow()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '刷新', // 刷新页面
|
||||
click() {
|
||||
refreshWindows()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '打开调试窗口',
|
||||
click(menuItem, targetWindow) {
|
||||
targetWindow.openDevTools()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '关闭调试窗口',
|
||||
click(menuItem, targetWindow) {
|
||||
targetWindow.closeDevTools()
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '编辑',
|
||||
role: 'editMenu'
|
||||
},
|
||||
{
|
||||
label: '布署',
|
||||
submenu: [
|
||||
{
|
||||
label: '重新布署',
|
||||
click() {
|
||||
applyRime()
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '文件夹',
|
||||
submenu: [
|
||||
{label: '打开 Rime 配置文件夹', click() {shell.openPath(getRimeConfigDir())}},
|
||||
{label: '打开 Rime 程序文件夹', click() {shell.openPath(getRimeExecDir())}},
|
||||
{
|
||||
label: '打开工具配置文件夹', click() {
|
||||
let configDir = path.join(os.homedir(), CONFIG_FILE_PATH)
|
||||
shell.openPath(configDir)
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '码表处理',
|
||||
submenu: [
|
||||
{
|
||||
label: '码表处理工具',
|
||||
click() {
|
||||
showToolWindow()
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '关于',
|
||||
submenu: [
|
||||
{label: '最小化', role: 'minimize'},
|
||||
{label: '关于', role: 'about'},
|
||||
{type: 'separator'},
|
||||
{label: '退出', role: 'quit'},
|
||||
]
|
||||
},
|
||||
]
|
||||
if(IS_IN_DEVELOP){
|
||||
/* menuStructure.push(
|
||||
|
||||
)*/
|
||||
}
|
||||
let menu = Menu.buildFromTemplate(menuStructure)
|
||||
Menu.setApplicationMenu(menu)
|
||||
}
|
||||
|
||||
// 刷新所有窗口内容
|
||||
function refreshWindows(){
|
||||
if(mainWindow) mainWindow.reload()
|
||||
if(configWindow) configWindow.reload()
|
||||
if(toolWindow) toolWindow.reload()
|
||||
}
|
||||
|
||||
// 读取配置目录中的所有码表文件
|
||||
function getDictFileList(){
|
||||
let rimeFolderPath = getRimeConfigDir()
|
||||
fs.readdir(rimeFolderPath,(err, filePaths) => {
|
||||
if (err) {
|
||||
log(err)
|
||||
} else {
|
||||
let filesMenu = []
|
||||
// 筛选 .yaml 文件
|
||||
let yamlFileList = filePaths.filter(item => item.indexOf('.dict.yaml') > 0)
|
||||
// 匹配获取上面提前定义的文件名
|
||||
fileList = yamlFileList.map(item => {
|
||||
return {
|
||||
name: getLabelNameFromFileName(item),
|
||||
path: item
|
||||
}
|
||||
})
|
||||
// 排序路径
|
||||
fileList.sort((a,b) => a.name > b.name ? 1: -1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 布署 Rime
|
||||
function applyRime(){
|
||||
let rimeBinDir = getRimeExecDir()
|
||||
log(path.join(rimeBinDir,'WeaselDeployer.exe'))
|
||||
switch (os.platform()){
|
||||
case 'darwin':
|
||||
// macOS
|
||||
exec(`"${rimeBinDir}/Squirrel" --reload`, error => {
|
||||
log(error)
|
||||
})
|
||||
break
|
||||
case 'win32':
|
||||
// windows
|
||||
let execFilePath = path.join(rimeBinDir,'WeaselDeployer.exe')
|
||||
exec(`"${execFilePath}" /deploy`, err => {
|
||||
if (err){
|
||||
log(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 根据系统返回 rime 配置路径
|
||||
function getRimeConfigDir(){
|
||||
let userHome = os.homedir()
|
||||
let config = readConfigFile()
|
||||
if (!config.rimeHomeDir){ // 没有设置配置文件目录时
|
||||
switch (os.platform()){
|
||||
case 'aix': break
|
||||
case 'darwin': return path.join(userHome + '/Library/Rime') // macOS
|
||||
case 'freebsd': break
|
||||
case 'linux': break
|
||||
case 'openbsd': break
|
||||
case 'sunos': break
|
||||
case 'win32': return path.join(userHome + '/AppData/Roaming/Rime') // windows
|
||||
}
|
||||
} else {
|
||||
return config.rimeHomeDir
|
||||
}
|
||||
}
|
||||
|
||||
function getAppConfigDir(){
|
||||
return path.join(os.homedir(), CONFIG_FILE_PATH)
|
||||
}
|
||||
|
||||
// 返回 Rime 可执行文件夹
|
||||
function getRimeExecDir(){
|
||||
switch (os.platform()){
|
||||
case 'aix': break
|
||||
case 'darwin': // macOS
|
||||
return path.join('/Library/Input Methods/Squirrel.app', 'Contents/MacOS')
|
||||
case 'freebsd': break
|
||||
case 'linux': break
|
||||
case 'openbsd': break
|
||||
case 'sunos': break
|
||||
case 'win32': // windows
|
||||
const PATH_RIME_BIN_WIN = 'C:/Program Files (x86)/Rime'
|
||||
let execDirEntes = fs.readdirSync(PATH_RIME_BIN_WIN, {withFileTypes: true})
|
||||
// 获取路径中 weasel 版本文件夹
|
||||
// TODO:后续可能需要处理多版本的时候获取最新版本
|
||||
let rimeDirEntes = execDirEntes.filter(item => item.name.includes('weasel'))
|
||||
return path.join(PATH_RIME_BIN_WIN, rimeDirEntes[0].name)
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,67 @@
|
|||
{
|
||||
"name": "wubi-dict-editor",
|
||||
"version": "1.0.7",
|
||||
"private": true,
|
||||
"author": {
|
||||
"name": "KyleBing",
|
||||
"email": "kylebing@163.com"
|
||||
},
|
||||
"date": "2021-11-22",
|
||||
"dateInit": "2021-07-24",
|
||||
"description": "五笔码表管理工具",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "electron-forge start",
|
||||
"package": "electron-forge package"
|
||||
},
|
||||
"dependencies": {
|
||||
"electron-squirrel-startup": "^1.0.0",
|
||||
"vue": "^2.6.14",
|
||||
"vue-virtual-scroller": "^1.0.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "^6.0.0-beta.58",
|
||||
"@electron-forge/maker-deb": "^6.0.0-beta.58",
|
||||
"@electron-forge/maker-rpm": "^6.0.0-beta.58",
|
||||
"@electron-forge/maker-squirrel": "^6.0.0-beta.58",
|
||||
"@electron-forge/maker-zip": "^6.0.0-beta.58",
|
||||
"electron": "^13.1.7"
|
||||
},
|
||||
"config": {
|
||||
"forge": {
|
||||
"packagerConfig": {
|
||||
"appVersion": "1.0.7",
|
||||
"name": "五笔助手",
|
||||
"appCopyright": "KyleBing(kylebing@163.com)",
|
||||
"icon": "./assets/img/appIcon/appicon",
|
||||
"win32metadata": {
|
||||
"ProductName": "五笔助手 Windows",
|
||||
"CompanyName": "kylebing.cn",
|
||||
"FileDescription": "五笔助手 for 小狼毫"
|
||||
}
|
||||
},
|
||||
"makers": [
|
||||
{
|
||||
"name": "@electron-forge/maker-squirrel",
|
||||
"config": {
|
||||
"name": "WubiDictEditor"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@electron-forge/maker-zip",
|
||||
"platforms": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@electron-forge/maker-deb",
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"name": "@electron-forge/maker-rpm",
|
||||
"config": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
const Vue = require('../../node_modules/vue/dist/vue.common.prod')
|
||||
const {ipcRenderer} = require('electron')
|
||||
const {log} = require('../../js/Utility')
|
||||
const { IS_IN_DEVELOP, CONFIG_FILE_PATH, CONFIG_FILE_NAME, DEFAULT_CONFIG } = require('../../js/Global')
|
||||
const os = require('os')
|
||||
const DictMap = require('../../js/DictMap')
|
||||
|
||||
|
||||
// Vue 2
|
||||
const app = {
|
||||
el: '#app',
|
||||
data() {
|
||||
return {
|
||||
fileList: null, // 展示用的配置文件夹内的文件列表
|
||||
// [{ "name": "luna_pinyin.sogou", "path": "luna_pinyin.sogou.dict.yaml" }]
|
||||
config: DEFAULT_CONFIG,
|
||||
dictMapContent: '' // 字典文件内容
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.heightContent = innerHeight - 47 - 20 - 10 + 3
|
||||
|
||||
// load file list
|
||||
ipcRenderer.on('responseFileList', (event, fileList) => {
|
||||
fileList.sort((a,b) => a.name > b.name ? 1: -1)
|
||||
this.fileList = fileList
|
||||
})
|
||||
ipcRenderer.send('requestFileList')
|
||||
|
||||
// config
|
||||
ipcRenderer.on('ConfigWindow:ResponseConfigFile', (event, config) => {
|
||||
this.config = config
|
||||
})
|
||||
ipcRenderer.send('ConfigWindow:RequestConfigFile')
|
||||
|
||||
// 选取配置目录后保存
|
||||
ipcRenderer.on('ConfigWindow:ChoosenRimeHomeDir', (event, dir) => {
|
||||
this.config.rimeHomeDir = dir[0]
|
||||
})
|
||||
|
||||
// 字典文件保存后
|
||||
ipcRenderer.on('ConfigWindow:SaveDictMapSuccess', event => {
|
||||
this.config.hasSetDictMap = true
|
||||
})
|
||||
|
||||
// 读取字典文件的内容
|
||||
ipcRenderer.on('ConfigWindow:ShowDictMapContent', (event, fileName, filePath, fileContent) => {
|
||||
let dictMap = new DictMap(fileContent, fileName, filePath)
|
||||
let dictMapFileContent = dictMap.toExportString()
|
||||
ipcRenderer.send('ConfigWindow:SaveDictMapFile', dictMapFileContent) // 保存取到的单字字典文本
|
||||
})
|
||||
|
||||
|
||||
onresize = ()=>{
|
||||
this.heightContent = innerHeight - 47 - 20 - 10 + 3
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setInitFile(file){
|
||||
this.config.initFileName = file.path
|
||||
},
|
||||
setDictMap(){
|
||||
ipcRenderer.send('ConfigWindow:SetDictMapFile')
|
||||
},
|
||||
chooseRimeHomeDir(){
|
||||
ipcRenderer.send('ConfigWindow:ChooseRimeHomeDir')
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
config: {
|
||||
handler(newValue) {
|
||||
switch (newValue.theme){
|
||||
case "auto":
|
||||
document.documentElement.classList.add('auto-mode');
|
||||
document.documentElement.classList.remove('dark-mode');
|
||||
break;
|
||||
case "black":
|
||||
document.documentElement.classList.add('dark-mode');
|
||||
document.documentElement.classList.remove('auto-mode');
|
||||
break;
|
||||
}
|
||||
log(JSON.stringify(newValue))
|
||||
ipcRenderer.send('ConfigWindow:RequestSaveConfig', JSON.stringify(this.config))
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
new Vue(app)
|
|
@ -0,0 +1,199 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>五笔助手配置</title>
|
||||
<link rel="stylesheet" href="../../assets/scss/wubi.css">
|
||||
</head>
|
||||
<body class="config">
|
||||
|
||||
<div v-cloak id="app" class="container">
|
||||
|
||||
<section>
|
||||
<div class="section-title">
|
||||
<h3>选择初始文件</h3>
|
||||
<div class="desc">程序打开时,自动载入的码表文件</div>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="config-file-list">
|
||||
<div @click="setInitFile(file)"
|
||||
:class="['config-file-list-item', {active: file.path === config.initFileName}]"
|
||||
v-for="(file, index) in fileList"
|
||||
:key="index"
|
||||
v-if="fileList"
|
||||
>
|
||||
<div class="name">{{file.name}}</div>
|
||||
<div class="path">{{file.path}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="section-title">
|
||||
<h3>自动布署</h3>
|
||||
<div class="desc">添加新用户词之后,是否保存当前自动布署</div>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="check-item">
|
||||
<div class="label">加词后,自动保存并布署输入法</div>
|
||||
<div class="audio-switch">
|
||||
<input v-model="config.autoDeployOnAdd" checked type="checkbox" id="autoDeployAdd">
|
||||
<label for="autoDeployAdd"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="check-item">
|
||||
<div class="label">删词后,自动保存并布署输入法</div>
|
||||
<div class="audio-switch">
|
||||
<input v-model="config.autoDeployOnDelete" checked type="checkbox" id="autoDeployDelete">
|
||||
<label for="autoDeployDelete"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="check-item">
|
||||
<div class="label">编辑词后,自动保存并布署输入法</div>
|
||||
<div class="audio-switch">
|
||||
<input v-model="config.autoDeployOnEdit" checked type="checkbox" id="autoDeployEdit">
|
||||
<label for="autoDeployEdit"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="section-title">
|
||||
<h3>回车动作</h3>
|
||||
<div class="desc">定义搜索框中按回车键的动作</div>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="check-list">
|
||||
<div class="check-item">
|
||||
<div class="checkbox-cell">
|
||||
<input value="add"
|
||||
v-model="config.enterKeyBehavior"
|
||||
name="enterKey" id="radioAddNew" type="radio">
|
||||
<label class="radio" for="radioAddNew"></label>
|
||||
</div>
|
||||
<div class="label">添加新词</div>
|
||||
</div>
|
||||
<div class="check-item">
|
||||
<div class="checkbox-cell">
|
||||
<input value="search"
|
||||
v-model="config.enterKeyBehavior"
|
||||
name="enterKey" id="radioSearch" type="radio">
|
||||
<label class="radio" for="radioSearch"></label>
|
||||
</div>
|
||||
<div class="label">搜索</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="section-title">
|
||||
<h3>搜索</h3>
|
||||
<div class="desc">搜索匹配的内容</div>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="check-list">
|
||||
<div class="check-item">
|
||||
<div class="checkbox-cell">
|
||||
<input value="code"
|
||||
v-model="config.searchMethod"
|
||||
name="searchMethod" id="radioSearchCode" type="radio">
|
||||
<label class="radio" for="radioSearchCode"></label>
|
||||
</div>
|
||||
<div class="label">编码</div>
|
||||
</div>
|
||||
<div class="check-item">
|
||||
<div class="checkbox-cell">
|
||||
<input value="phrase"
|
||||
v-model="config.searchMethod"
|
||||
name="searchMethod" id="radioSearchPhrase" type="radio">
|
||||
<label class="radio" for="radioSearchPhrase"></label>
|
||||
</div>
|
||||
<div class="label">词条</div>
|
||||
</div>
|
||||
<div class="check-item">
|
||||
<div class="checkbox-cell">
|
||||
<input value="both"
|
||||
v-model="config.searchMethod"
|
||||
name="searchMethod" id="radioSearchBoth" type="radio">
|
||||
<label class="radio" for="radioSearchBoth"></label>
|
||||
</div>
|
||||
<div class="label">编码 和 词条</div>
|
||||
</div>
|
||||
<div class="check-item">
|
||||
<div class="checkbox-cell">
|
||||
<input value="any"
|
||||
v-model="config.searchMethod"
|
||||
name="searchMethod" id="radioSearchAny" type="radio">
|
||||
<label class="radio" for="radioSearchAny"></label>
|
||||
</div>
|
||||
<div class="label">编码 或 词条</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="section-title">
|
||||
<h3>配置文件目录</h3>
|
||||
<div class="desc">工具自动读取的输入法配置文件目录</div>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="check-item">
|
||||
<p>{{config.rimeHomeDir || '默认目录'}}</p>
|
||||
<div class="btn btn-primary" @click="chooseRimeHomeDir">选择目录</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="section-title">
|
||||
<h3>选择参考码表</h3>
|
||||
<div class="desc">添加新词时、批量生成编码时,使用的码表,格式:前词后码,tab 分隔。</div>
|
||||
<div class="desc">正常的 Rime 码表文件都能正常使用,有多字也无所谓,程序会自动只选取单字部分使用。</div>
|
||||
<div class="desc">默认情况下,程序使用 wubi86_jidian.dict.yaml 这个文件作为参考码表。</div>
|
||||
<div class="desc">可以载入任意版本的五笔码表文件,这样就能生成对应版本的五笔编码了。</div>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="check-item">
|
||||
<p>{{config.hasSetDictMap? '已添加码表文件': '请选择码表文件'}}</p>
|
||||
<div class="btn btn-primary" @click="setDictMap">选择文件</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="section-title">
|
||||
<h3>主题</h3>
|
||||
<div class="desc">切换主题</div>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="check-list">
|
||||
<div class="check-item">
|
||||
<div class="checkbox-cell">
|
||||
<input value="black"
|
||||
v-model="config.theme"
|
||||
name="theme" id="themeBlack" type="radio">
|
||||
<label class="radio" for="themeBlack"></label>
|
||||
</div>
|
||||
<div class="label">暗黑</div>
|
||||
</div>
|
||||
<div class="check-item">
|
||||
<div class="checkbox-cell">
|
||||
<input value="auto"
|
||||
v-model="config.theme"
|
||||
name="theme" id="themeAuto" type="radio">
|
||||
<label class="radio" for="themeAuto"></label>
|
||||
</div>
|
||||
<div class="label">跟随系统</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script src="Config.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,606 @@
|
|||
const {shakeDom, shakeDomFocus, log} = require('../../js/Utility')
|
||||
const {IS_IN_DEVELOP} = require('../../js/Global')
|
||||
|
||||
const Dict = require('../../js/Dict')
|
||||
const DictMap = require('../../js/DictMap')
|
||||
const Word = require('../../js/Word')
|
||||
const Vue = require('../../node_modules/vue/dist/vue.common.prod')
|
||||
|
||||
const {ipcRenderer} = require('electron')
|
||||
const VirtualScroller = require('vue-virtual-scroller')
|
||||
|
||||
|
||||
// Vue 2
|
||||
const app = {
|
||||
el: '#app',
|
||||
components: {RecycleScroller: VirtualScroller.RecycleScroller},
|
||||
data() {
|
||||
return {
|
||||
IS_IN_DEVELOP: IS_IN_DEVELOP, // 是否为开发模式,html 使用
|
||||
tip: '', // 提示信息
|
||||
dict: {
|
||||
deep: true
|
||||
}, // 当前词库对象 Dict
|
||||
dictMain: {}, // 主码表 Dict
|
||||
keyword: '', // 搜索关键字
|
||||
code: '',
|
||||
word: '',
|
||||
activeGroupId: -1, // 组 index
|
||||
keywordUnwatch: null, // keyword watch 方法的撤消方法
|
||||
labelOfSaveBtn: '保存', // 保存按钮的文本
|
||||
heightContent: 0, // content 高度
|
||||
words: [], // 显示的 words
|
||||
|
||||
chosenWordIds: new Set(),
|
||||
chosenWordIdArray: [], // 对应上面的 set 内容
|
||||
lastChosenWordIndex: null, // 最后一次选中的 index
|
||||
|
||||
|
||||
targetDict: {}, // 要移动到的码表
|
||||
showDropdown: false, // 显示移动词条窗口
|
||||
dropdownFileList: [
|
||||
// {name: '拼音词库', path: 'pinyin_simp.dict.yaml'}
|
||||
],
|
||||
dropdownActiveFileIndex: -1, // 选中的
|
||||
dropdownActiveGroupIndex: -1, // 选中的分组 ID
|
||||
|
||||
config: {}, // 全局配置
|
||||
|
||||
dictMap: null, // main 返回的 dictMap,用于解码词条
|
||||
|
||||
wordEditing: null, // 正在编辑的词条
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.heightContent = innerHeight - 47 - 20 - 10 + 3
|
||||
// 载入主要操作码表文件
|
||||
ipcRenderer.on('showFileContent', (event, fileName, filePath, res) => {
|
||||
// 过滤移动到的文件列表,不显示正在显示的这个码表
|
||||
// this.dropdownFileList = this.dropdownFileList.filter(item => item.path !== fileName)
|
||||
this.dict = new Dict(res, fileName, filePath)
|
||||
// 载入新码表时,清除 word 保存 code
|
||||
this.word = ''
|
||||
this.refreshShowingWords()
|
||||
// this.search() // 配置项:切换码表是否自动搜索
|
||||
ipcRenderer.send('loadMainDict') // 请求主码表文件
|
||||
})
|
||||
ipcRenderer.on('saveFileSuccess', () => {
|
||||
this.labelOfSaveBtn = '保存成功'
|
||||
this.$refs.domBtnSave.classList.add('btn-green')
|
||||
setTimeout(()=>{
|
||||
this.$refs.domBtnSave.classList.remove('btn-green')
|
||||
this.labelOfSaveBtn = '保存'
|
||||
}, 2000)
|
||||
})
|
||||
|
||||
// 由 window 触发获取文件目录的请求,不然无法实现适时的获取到 主进程返回的数据
|
||||
ipcRenderer.send('GetFileList')
|
||||
ipcRenderer.on('FileList', (event, fileList) => {
|
||||
log(fileList)
|
||||
this.dropdownFileList = fileList
|
||||
})
|
||||
ipcRenderer.send('loadInitDictFile')
|
||||
|
||||
// 载入目标码表
|
||||
ipcRenderer.on('setTargetDict', (event, fileName, filePath, res) => {
|
||||
this.targetDict = new Dict(res, fileName, filePath)
|
||||
})
|
||||
|
||||
// 载入主码表
|
||||
ipcRenderer.on('setMainDict', (event, filename, res) => {
|
||||
this.dictMain = new Dict(res, filename)
|
||||
})
|
||||
|
||||
// 配置相关
|
||||
ipcRenderer.on('MainWindow:ResponseConfigFile', (event, config) => {
|
||||
this.config = config
|
||||
this.activeGroupId = config.chosenGroupIndex // 首次载入时,定位到上次选中的分组
|
||||
log('窗口载入时获取到的 config 文件:', config)
|
||||
})
|
||||
ipcRenderer.send('MainWindow:RequestConfigFile')
|
||||
|
||||
// 配置文件保存后,向主窗口更新配置文件内容
|
||||
ipcRenderer.on('updateConfigFile', (event, config) => {
|
||||
this.config = config
|
||||
})
|
||||
|
||||
// 获取并设置字典文件
|
||||
ipcRenderer.on('setDictMap', (event, fileContent, fileName, filePath) => {
|
||||
this.dictMap = new DictMap(fileContent, fileName, filePath)
|
||||
})
|
||||
ipcRenderer.send('getDictMap')
|
||||
|
||||
this.addKeyboardListener()
|
||||
onresize = ()=>{
|
||||
this.heightContent = innerHeight - 47 - 20 - 10 + 3
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 当前显示的 words 数量
|
||||
wordsCount(){
|
||||
if (this.dict.isGroupMode){
|
||||
let countCurrent = 0
|
||||
this.words.forEach(group => {
|
||||
countCurrent = countCurrent + group.dict.length
|
||||
})
|
||||
return countCurrent
|
||||
} else {
|
||||
return this.words.length
|
||||
}
|
||||
},
|
||||
// 当前载入的是否为 主 码表
|
||||
isInMainDict(){
|
||||
return this.dict.fileName === 'wubi86_jidian.dict.yaml'
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 切换码表文件
|
||||
switchToFile(file){
|
||||
ipcRenderer.send('MainWindow:LoadFile', file.path)
|
||||
},
|
||||
tipNotice(msg){
|
||||
this.tip = msg
|
||||
setTimeout(()=>{this.tip = ''}, 3000)
|
||||
},
|
||||
// 确定编辑词条
|
||||
confirmEditWord(){
|
||||
this.wordEditing = null
|
||||
if(this.config.autoDeployOnEdit) this.saveToFile(this.dict) // 根据配置,是否在编辑后保存码表文件
|
||||
},
|
||||
// 生成编辑词条的编码
|
||||
generateCodeForWordEdit(){
|
||||
if (this.wordEditing){
|
||||
this.wordEditing.code = this.dictMap.decodeWord(this.wordEditing.word)
|
||||
} else {
|
||||
shakeDomFocus(this.$refs.editInputWord)
|
||||
}
|
||||
},
|
||||
// 编辑词条
|
||||
editWord(word){
|
||||
this.wordEditing = word
|
||||
},
|
||||
|
||||
// 选择操作
|
||||
select(index, wordId, event){
|
||||
if (event.shiftKey){
|
||||
if (this.lastChosenWordIndex !== null){
|
||||
let a,b // 判断大小,调整大小顺序
|
||||
if (index > this.lastChosenWordIndex){
|
||||
a = this.lastChosenWordIndex
|
||||
b = index
|
||||
} else {
|
||||
b = this.lastChosenWordIndex
|
||||
a = index
|
||||
}
|
||||
|
||||
if (this.dict.isGroupMode){
|
||||
// TODO: select batch words cross group
|
||||
if (this.activeGroupId !== -1){
|
||||
for (let i=a; i<=b; i++){
|
||||
this.chosenWordIds.add(this.dict.wordsOrigin[this.activeGroupId].dict[i].id)
|
||||
}
|
||||
} else {
|
||||
this.tipNotice('只能在单组内进行批量选择')
|
||||
}
|
||||
} else {
|
||||
for (let i=a; i<=b; i++){
|
||||
this.chosenWordIds.add(this.words[i].id)
|
||||
}
|
||||
}
|
||||
}
|
||||
this.lastChosenWordIndex = null // shift 选择后,最后一个id定义为没有
|
||||
|
||||
} else {
|
||||
if (this.chosenWordIds.has(wordId)){
|
||||
this.chosenWordIds.delete(wordId)
|
||||
this.lastChosenWordIndex = null
|
||||
} else {
|
||||
this.chosenWordIds.add(wordId)
|
||||
this.lastChosenWordIndex = index
|
||||
}
|
||||
}
|
||||
this.chosenWordIdArray = [...this.chosenWordIds.values()]
|
||||
},
|
||||
// 选择移动到的分组 index
|
||||
setDropdownActiveGroupIndex(index){
|
||||
this.dropdownActiveGroupIndex = index
|
||||
},
|
||||
// 选择移动到的文件 index
|
||||
setDropdownActiveIndex(fileIndex){
|
||||
this.dropdownActiveFileIndex = fileIndex
|
||||
this.dropdownActiveGroupIndex = -1 // 切换文件列表时,复位分组 fileIndex
|
||||
// this.dictSecond = {} // 立即清空次码表,分组列表也会立即消失,不会等下面的码表加载完成再清空
|
||||
ipcRenderer.send('MainWindow:LoadSecondDict', this.dropdownFileList[fileIndex].path) // 载入当前 index 的文件内容
|
||||
},
|
||||
sort(){
|
||||
this.dict.sort(this.activeGroupId)
|
||||
this.refreshShowingWords()
|
||||
},
|
||||
enterKeyPressed(){
|
||||
switch (this.config.enterKeyBehavior){
|
||||
case "add":this.addNewWord(); break;
|
||||
case "search": this.search(); break;
|
||||
default: break;
|
||||
}
|
||||
},
|
||||
// 通过 code, word 筛选词条
|
||||
search(){
|
||||
this.chosenWordIds.clear()
|
||||
this.chosenWordIdArray = []
|
||||
this.activeGroupId = -1 // 切到【全部】标签页,展示所有搜索结果
|
||||
let startPoint = new Date().getTime()
|
||||
if (this.code || this.word){
|
||||
if (this.dict.isGroupMode){
|
||||
this.words = []
|
||||
this.dict.wordsOrigin.forEach(groupItem => {
|
||||
let tempGroupItem = groupItem.clone() // 不能直接使用原 groupItem,不然会改变 wordsOrigin 的数据
|
||||
tempGroupItem.dict = tempGroupItem.dict.filter(item => {
|
||||
switch (this.config.searchMethod){
|
||||
case "code": return item.code.includes(this.code);
|
||||
case "phrase": return item.word.includes(this.word);
|
||||
case "both": return item.code.includes(this.code) && item.word.includes(this.word)
|
||||
case "any": return item.code.includes(this.code) || item.word.includes(this.word)
|
||||
}
|
||||
})
|
||||
if (tempGroupItem.dict.length > 0){ // 当前分组中有元素,添加到结果中
|
||||
this.words.push(tempGroupItem)
|
||||
}
|
||||
})
|
||||
log('用时: ', new Date().getTime() - startPoint, 'ms')
|
||||
} else {
|
||||
this.words = this.dict.wordsOrigin.filter(item => { // 获取包含 code 的记录
|
||||
switch (this.config.searchMethod){
|
||||
case "code": return item.code.includes(this.code);
|
||||
case "phrase": return item.word.includes(this.word);
|
||||
case "both": return item.code.includes(this.code) && item.word.includes(this.word)
|
||||
case "any": return item.code.includes(this.code) || item.word.includes(this.word)
|
||||
}
|
||||
})
|
||||
log(`${this.code} ${this.word}: ` ,'搜索出', this.words.length, '条,', '用时: ', new Date().getTime() - startPoint, 'ms')
|
||||
}
|
||||
|
||||
} else { // 如果 code, word 为空,恢复原有数据
|
||||
this.refreshShowingWords()
|
||||
}
|
||||
},
|
||||
|
||||
// GROUP OPERATION
|
||||
// 添加新组
|
||||
addGroupBeforeId(groupIndex){
|
||||
this.dict.addGroupBeforeId(groupIndex)
|
||||
this.refreshShowingWords()
|
||||
},
|
||||
deleteGroup(groupId){
|
||||
this.dict.deleteGroup(groupId)
|
||||
this.activeGroupId = - 1 // 不管删除哪个分组,之后都指向全部
|
||||
this.refreshShowingWords()
|
||||
},
|
||||
// 设置当前显示的 分组
|
||||
setGroupId(groupId){ // groupId 全部的 id 是 -1
|
||||
this.activeGroupId = groupId
|
||||
this.refreshShowingWords()
|
||||
this.config.chosenGroupIndex = groupId
|
||||
ipcRenderer.send('saveConfigFileFromMainWindow', JSON.stringify(this.config))
|
||||
},
|
||||
// 刷新 this.words
|
||||
refreshShowingWords(){
|
||||
this.chosenWordIds.clear()
|
||||
this.chosenWordIdArray = []
|
||||
log('已选中的 groupIndex: ',this.activeGroupId, typeof this.activeGroupId)
|
||||
if (this.dict.isGroupMode){
|
||||
if (this.activeGroupId === -1){
|
||||
this.words = [...this.dict.wordsOrigin]
|
||||
} else {
|
||||
if (this.activeGroupId > this.dict.wordsOrigin.length - 1) {
|
||||
this.activeGroupId = this.dict.wordsOrigin.length - 1
|
||||
}
|
||||
this.words = new Array(this.dict.wordsOrigin[this.activeGroupId])
|
||||
}
|
||||
} else {
|
||||
this.words = [...this.dict.wordsOrigin]
|
||||
}
|
||||
},
|
||||
addNewWord(){
|
||||
if (!this.word){
|
||||
shakeDomFocus(this.$refs.domInputWord)
|
||||
} else if (!this.code){
|
||||
shakeDomFocus(this.$refs.domInputCode)
|
||||
} else {
|
||||
this.dict.addNewWord(new Word(this.dict.lastIndex, this.code, this.word) ,this.activeGroupId)
|
||||
this.refreshShowingWords()
|
||||
log(this.code, this.word, this.activeGroupId)
|
||||
if (this.config.autoDeployOnAdd){
|
||||
this.saveToFile(this.dict)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// 保存内容到文件
|
||||
saveToFile(dict){
|
||||
log(dict.fileName)
|
||||
ipcRenderer.send('saveFile', dict.fileName, dict.toYamlString())
|
||||
},
|
||||
// 选中全部展示的词条
|
||||
selectAll(){
|
||||
if(this.wordsCount < 1000){
|
||||
if (this.dict.isGroupMode){
|
||||
this.chosenWordIds.clear()
|
||||
this.chosenWordIdArray = []
|
||||
this.words.forEach(group => { // group 是 dictGroup
|
||||
group.dict.forEach( item => {
|
||||
this.chosenWordIds.add(item.id)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
this.words.forEach(item => {this.chosenWordIds.add(item.id)})
|
||||
}
|
||||
this.chosenWordIdArray = [...this.chosenWordIds.values()]
|
||||
} else {
|
||||
// 提示不能同时选择太多内容
|
||||
this.tip = '不能同时选择大于 1000条 的词条内容'
|
||||
shakeDom(this.$refs.domBtnSelectAll)
|
||||
}
|
||||
},
|
||||
// 清除内容
|
||||
resetInputs(){
|
||||
this.chosenWordIds.clear()
|
||||
this.chosenWordIdArray = []
|
||||
this.code = ''
|
||||
this.word = ''
|
||||
this.search()
|
||||
this.tip = ''
|
||||
},
|
||||
// 删除词条:单
|
||||
deleteWord(wordId){
|
||||
this.chosenWordIds.delete(wordId)
|
||||
this.chosenWordIdArray = [...this.chosenWordIds.values()]
|
||||
this.dict.deleteWords(new Set([wordId]))
|
||||
this.refreshShowingWords()
|
||||
if(this.config.autoDeployOnDelete){ this.saveToFile(this.dict) }
|
||||
},
|
||||
// 删除词条:多
|
||||
deleteWords(){
|
||||
this.dict.deleteWords(this.chosenWordIds)
|
||||
this.refreshShowingWords()
|
||||
this.chosenWordIds.clear() // 清空选中 wordID
|
||||
this.chosenWordIdArray = []
|
||||
if(this.config.autoDeployOnDelete){ this.saveToFile(this.dict) }
|
||||
},
|
||||
|
||||
// 词条位置移动
|
||||
move(wordId, direction){
|
||||
if (this.dict.isGroupMode){
|
||||
// group 时,移动 调换 word 位置,是直接调动的 wordsOrigin 中的word
|
||||
// 因为 group 时数据为: [{word, word},{word,word}],是 wordGroup 的索引
|
||||
for(let i=0; i<this.words.length; i++){
|
||||
let group = this.words[i]
|
||||
for(let j=0; j<group.dict.length; j++){
|
||||
if (wordId === group.dict[j].id){
|
||||
let tempItem = group.dict[j]
|
||||
if (direction === 'up'){
|
||||
if (j !==0){
|
||||
group.dict[j] = group.dict[j - 1]
|
||||
group.dict[j - 1] = tempItem
|
||||
return ''
|
||||
} else {
|
||||
log('已到顶')
|
||||
return '已到顶'
|
||||
}
|
||||
} else if (direction === 'down'){
|
||||
if (j+1 !== group.dict.length){
|
||||
group.dict[j] = group.dict[j + 1]
|
||||
group.dict[j + 1] = tempItem
|
||||
return ''
|
||||
} else {
|
||||
log('已到底')
|
||||
return '已到底'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 非分组模式时,调换位置并不能直接改变 wordsOrigin 因为 与 words 已经断开连接
|
||||
// [word, word]
|
||||
for(let i=0; i<this.words.length; i++){
|
||||
if (wordId === this.words[i].id){
|
||||
let tempItem = this.words[i]
|
||||
if (direction === 'up'){
|
||||
if (i !==0) {
|
||||
this.dict.exchangePositionInOrigin(tempItem, this.words[i-1]) // 调换 wordsOrigin 中的词条位置
|
||||
this.words[i] = this.words[i - 1]
|
||||
this.words[i - 1] = tempItem
|
||||
return ''
|
||||
} else {
|
||||
log('已到顶')
|
||||
return '已到顶'
|
||||
}
|
||||
} else if (direction === 'down'){
|
||||
if (i+1 !== this.words.length) {
|
||||
this.dict.exchangePositionInOrigin(tempItem, this.words[i+1]) // 调换 wordsOrigin 中的词条位置
|
||||
this.words[i] = this.words[i + 1]
|
||||
this.words[i + 1] = tempItem
|
||||
return ''
|
||||
} else {
|
||||
log('已到底')
|
||||
return '已到底'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 上移词条
|
||||
moveUp(id){
|
||||
this.tip = this.move(id, 'up')
|
||||
let temp = this.words.pop()
|
||||
this.words.push(temp)
|
||||
},
|
||||
// 下移词条
|
||||
moveDown(id){
|
||||
this.tip = this.move(id, 'down')
|
||||
let temp = this.words.pop()
|
||||
this.words.push(temp)
|
||||
},
|
||||
// 判断是否为第一个元素
|
||||
isFirstItem(id){
|
||||
if (this.dict.isGroupMode){ // 分组时的第一个元素
|
||||
for (let i=0; i<this.words.length; i++) {
|
||||
for (let j = 0; j < this.words[i].dict.length; j++) {
|
||||
if (this.words[i].dict[j].id === id){
|
||||
return j === 0 // 使用 array.forEach() 无法跳出循环
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
} else {
|
||||
for (let i = 0; i < this.words.length; i++) {
|
||||
if (this.words[i].id === id){
|
||||
return i === 0 // 使用 array.forEach() 无法跳出循环
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
// 判断是否为最后一个元素
|
||||
isLastItem(id){
|
||||
if (this.dict.isGroupMode){ // 分组时的最后一个元素
|
||||
for (let i=0; i<this.words.length; i++) {
|
||||
for (let j = 0; j < this.words[i].dict.length; j++) {
|
||||
if (this.words[i].id === id){
|
||||
return j + 1 === this.words.length
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
} else {
|
||||
for (let i = 0; i < this.words.length; i++) {
|
||||
if (this.words[i].id === id){
|
||||
return i + 1 === this.words.length
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
// 绑定键盘事件: 键盘上下控制词条上下移动
|
||||
addKeyboardListener(){
|
||||
window.addEventListener('keydown', event => {
|
||||
// log(event)
|
||||
switch( event.key) {
|
||||
case 's':
|
||||
if (event.ctrlKey || event.metaKey){ // metaKey 是 macOS 的 Ctrl
|
||||
this.saveToFile(this.dict)
|
||||
event.preventDefault()
|
||||
} else {
|
||||
|
||||
}
|
||||
break
|
||||
case 'ArrowDown':
|
||||
if(this.chosenWordIds.size === 1) { // 只有一个元素时,键盘才起作用
|
||||
let id = [...this.chosenWordIds.values()][0]
|
||||
this.moveDown(id)
|
||||
}
|
||||
event.preventDefault()
|
||||
break
|
||||
case 'ArrowUp':
|
||||
if(this.chosenWordIds.size === 1) { // 只有一个元素时,键盘才起作用
|
||||
let id = [...this.chosenWordIds.values()][0]
|
||||
this.moveUp(id)
|
||||
}
|
||||
event.preventDefault()
|
||||
break
|
||||
}
|
||||
})
|
||||
},
|
||||
// 将选中的词条移动到指定码表
|
||||
moveWordsToTargetDict(){
|
||||
let wordsTransferring = [] // 被转移的 [Word]
|
||||
if (this.dict.isGroupMode){
|
||||
this.dict.wordsOrigin.forEach((group, index) => {
|
||||
let matchedWords = group.dict.filter(item => this.chosenWordIds.has(item.id))
|
||||
wordsTransferring = wordsTransferring.concat(matchedWords)
|
||||
})
|
||||
} else {
|
||||
wordsTransferring = this.dict.wordsOrigin.filter(item => this.chosenWordIds.has(item.id))
|
||||
}
|
||||
log('words transferring:', JSON.stringify(wordsTransferring))
|
||||
|
||||
if (this.dict.fileName === this.targetDict.fileName){ // 如果是同词库移动
|
||||
this.targetDict.deleteWords(this.chosenWordIds, true) // 删除移动的词条
|
||||
this.targetDict.addWordsInOrder(wordsTransferring, this.dropdownActiveGroupIndex)
|
||||
log('after insert:( main:wordOrigin ):\n ', JSON.stringify(this.targetDict.wordsOrigin))
|
||||
// 如果在同码表中移动:如,从一个分组移到别一个分组
|
||||
// 只保存 dictSecond 内容,重新载入 dict 内容
|
||||
this.saveToFile(this.targetDict)
|
||||
this.reloadCurrentDict()
|
||||
} else {
|
||||
this.targetDict.addWordsInOrder(wordsTransferring, this.dropdownActiveGroupIndex)
|
||||
this.words = [...this.dict.wordsOrigin]
|
||||
log('after insert:( main:wordOrigin ):\n ', JSON.stringify(this.targetDict.wordsOrigin))
|
||||
this.deleteWords() // 删除当前词库已移动的词条
|
||||
this.saveToFile(this.targetDict)
|
||||
this.saveToFile(this.dict)
|
||||
}
|
||||
this.tip = '移动成功'
|
||||
setTimeout(()=>{this.tip = ''}, 3000)
|
||||
this.resetDropList()
|
||||
},
|
||||
// 复制 dropdown
|
||||
resetDropList(){
|
||||
this.showDropdown = false
|
||||
this.dropdownActiveFileIndex = -1
|
||||
this.dropdownActiveGroupIndex = -1
|
||||
this.targetDict = {} // 清空次码表
|
||||
},
|
||||
// 打开当前码表源文件
|
||||
openCurrentYaml(){
|
||||
ipcRenderer.send('openFileOutside', this.dict.fileName)
|
||||
},
|
||||
// 重新载入当前码表
|
||||
reloadCurrentDict(){
|
||||
ipcRenderer.send('loadDictFile', this.dict.fileName)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
code(newValue){
|
||||
this.code = newValue.replaceAll(/[^A-Za-z ]/g, '') // input.code 只允许输入字母
|
||||
},
|
||||
word(newValue, oldValue){
|
||||
if (newValue.length < oldValue.length){
|
||||
// 删除或清空时,不清空编码
|
||||
} else {
|
||||
if (this.dictMap){
|
||||
this.code = this.dictMap.decodeWord(newValue)
|
||||
}
|
||||
}
|
||||
},
|
||||
chosenWordIdArray(newValue){
|
||||
if (newValue.length === 0){
|
||||
this.showDropdown = false
|
||||
}
|
||||
log('已选词条id: ', JSON.stringify(newValue))
|
||||
},
|
||||
showDropdown(newValue){
|
||||
if (!newValue){ // 窗口关闭时,重置 index
|
||||
this.resetDropList()
|
||||
}
|
||||
},
|
||||
config: (newValue) => {
|
||||
switch (newValue.theme){
|
||||
case "auto":
|
||||
document.documentElement.classList.add('auto-mode');
|
||||
document.documentElement.classList.remove('dark-mode');
|
||||
break;
|
||||
case "black":
|
||||
document.documentElement.classList.add('dark-mode');
|
||||
document.documentElement.classList.remove('auto-mode');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new Vue(app)
|
|
@ -0,0 +1,234 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>五笔助手</title>
|
||||
<link rel="stylesheet" href="../../assets/scss/wubi.css">
|
||||
<link rel="stylesheet" href="../../node_modules/vue-virtual-scroller/dist/vue-virtual-scroller.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div v-cloak id="app">
|
||||
<!--词条编辑窗口-->
|
||||
<div class="edit-modal modal" v-if="wordEditing">
|
||||
<div class="modal-panel">
|
||||
<div class="modal-header">
|
||||
<div class="id">{{wordEditing.id}}</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input ref="editInputWord" class="mb-1" type="text" v-model="wordEditing.word" >
|
||||
<input ref="editInputCode" type="text" v-model="wordEditing.code" >
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="btn btn-cyan mr-2" @click="generateCodeForWordEdit">生成编码</div>
|
||||
<div class="btn btn-roseo" @click="confirmEditWord">确定</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--搜索框-->
|
||||
<div class="search-bar" v-if="dict">
|
||||
<div
|
||||
:class="[
|
||||
'btn',
|
||||
{'btn-green': labelOfSaveBtn === '保存成功'},
|
||||
{'btn-primary': labelOfSaveBtn !== '保存成功'} ]"
|
||||
ref="domBtnSave"
|
||||
@click="saveToFile(dict)">{{ labelOfSaveBtn }}
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<div class="dropdown-link btn btn-blue"
|
||||
v-show="chosenWordIdArray.length > 0"
|
||||
@click.capture="showDropdown = !showDropdown">移动到... </div>
|
||||
<div class="dropdown-body" v-show="showDropdown">
|
||||
<div class="file-list shadow" v-if="dropdownFileList.length > 0">
|
||||
<div :class="['file-list-item', {active: fileIndex === dropdownActiveFileIndex}]"
|
||||
@click="setDropdownActiveIndex(fileIndex)"
|
||||
v-for="(file, fileIndex) in dropdownFileList"
|
||||
:key="fileIndex">
|
||||
<div>{{ file.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-list shadow" v-if="targetDict.isGroupMode">
|
||||
<div :class="['file-list-item', {active: index === dropdownActiveGroupIndex}]"
|
||||
v-if="targetDict.isGroupMode"
|
||||
@click="setDropdownActiveGroupIndex(index)"
|
||||
v-for="(group, index) in targetDict.wordsOrigin"
|
||||
:key="index">
|
||||
<div>{{ group.groupName }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn btn-blue shadow ml-1"
|
||||
@click="moveWordsToTargetDict"
|
||||
v-if="targetDict.isGroupMode && dropdownActiveGroupIndex !== -1 || !targetDict.isGroupMode && dropdownActiveFileIndex !== -1"
|
||||
>确定</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-item">
|
||||
<input class="word" ref="domInputWord" @keyup.enter="enterKeyPressed" v-model="word" type="text" placeholder="词条">
|
||||
<div @click="word = ''" v-show="word" class="btn-clear">
|
||||
<img src="../../assets/img/delete_white.svg" alt="clear">
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-item">
|
||||
<input class="code" ref="domInputCode" @keyup.enter="enterKeyPressed" v-model="code" type="text" placeholder="编码">
|
||||
<div @click="code = ''" v-show="code" class="btn-clear">
|
||||
<img src="../../assets/img/delete_white.svg" alt="clear">
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn btn-primary" @click="addNewWord">添加</div>
|
||||
<div class="btn btn-primary" @click="search">搜索</div>
|
||||
<div class="btn btn-roseo" v-show="chosenWordIdArray.length > 0" @click="deleteWords">删除</div>
|
||||
<p class="notice">显示格式:<b>编码</b> - 词条 - 序号 - id</p>
|
||||
|
||||
<template v-if="IS_IN_DEVELOP">
|
||||
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="app-container">
|
||||
<!--文件目录列表-->
|
||||
<div class="schema-list" :style="`height: ${heightContent}px`">
|
||||
<div @click="switchToFile(file)"
|
||||
:class="['schema-list-item', {active: dict.fileName === file.path}]"
|
||||
v-for="file in dropdownFileList" :key="file.path">{{file.name}}</div>
|
||||
</div>
|
||||
<!-- 普通模式列表 -->
|
||||
<div ref="container"
|
||||
v-if="!dict.isGroupMode"
|
||||
:style="`height: ${heightContent}px`"
|
||||
class="content">
|
||||
<div class="list-container">
|
||||
<div class="group">
|
||||
<recycle-scroller
|
||||
:buffer="1000"
|
||||
:prerender="200"
|
||||
:style="`height: ${heightContent}px`"
|
||||
v-if="words.length > 0"
|
||||
:min-item-size="24"
|
||||
class="virtual-list"
|
||||
:items="words">
|
||||
<template v-slot="{ item, index }">
|
||||
<div class="word-item" @click="select(index, item.id, $event)">
|
||||
<div class="checkbox-cell">
|
||||
<!-- <div :class="['checkbox-item', {active: chosenWordIdArray.some(i => i === item.id)}]"></div>-->
|
||||
<div :class="['checkbox-item', {active: chosenWordIds.has(item.id)}]"></div>
|
||||
</div>
|
||||
<div class="code">{{ item.code }}</div>
|
||||
<div class="word">{{ item.word }}</div>
|
||||
<div v-if="item.priority" title="权重" class="priority">{{ item.priority }}</div>
|
||||
<div class="id">{{ index }}</div>
|
||||
<div class="id">{{ item.id }}</div>
|
||||
<div class="operation">
|
||||
<div class="up" @click.stop="moveUp(item.id)"><img src="../../assets/img/up.svg" alt="move up"></div>
|
||||
<div class="down" @click.stop="moveDown(item.id)"><img src="../../assets/img/down.svg" alt="move down"></div>
|
||||
<div class="up" @click.stop="editWord(item)"><img src="../../assets/img/edit.svg" alt="edit"></div>
|
||||
<div class="down" @click.stop="deleteWord(item.id)"><img src="../../assets/img/delete.svg" alt="delete"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</recycle-scroller>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分组模式列表 -->
|
||||
<div v-else
|
||||
:style="`height: ${heightContent}px`"
|
||||
class="content">
|
||||
<div class="group-container"
|
||||
:style="`height: ${heightContent}px`">
|
||||
<!--分组目录-->
|
||||
<div class="catalog-list"
|
||||
:style="`height: ${heightContent}px`">
|
||||
<div :class="['catalog-item', {active: -1 === activeGroupId}]"
|
||||
@click="setGroupId(-1)"><div>全部</div></div>
|
||||
<div :class="['catalog-item', {active: groupIndex === activeGroupId}]"
|
||||
@click="setGroupId(groupIndex)"
|
||||
v-for="(group, groupIndex) in dict.wordsOrigin"
|
||||
:key="groupIndex">
|
||||
<div>{{group.groupName || '- 未命名 -'}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 分组词条组列表-->
|
||||
<div class="group-list"
|
||||
:style="`height: ${heightContent}px`">
|
||||
<div class="group" v-for="(group, groupIndex) in words" :key="groupIndex">
|
||||
<!--- GROUP HEADER -->
|
||||
<div class="group-header"
|
||||
@click.right="group.isEditingTitle = !group.isEditingTitle">
|
||||
<template v-if="group.isEditingTitle">
|
||||
<input :key="groupIndex"
|
||||
v-model="group.groupName"
|
||||
@keydown.enter="group.isEditingTitle = false" />
|
||||
<div class="operation">
|
||||
<div class="btn btn-sm btn-alt-primary" @click="group.isEditingTitle = false">保存</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<h3 :key="groupIndex"
|
||||
>{{ group.groupName }}</h3>
|
||||
<div class="operation" >
|
||||
<div class="btn btn-sm btn-alt-primary" @click="group.isEditingTitle = true">编辑</div>
|
||||
<div v-if="activeGroupId === -1" class="btn btn-sm btn-alt-primary" @click="addGroupBeforeId(groupIndex)">添加分组</div>
|
||||
<div class="btn btn-sm btn-alt-roseo" @click="deleteGroup(group.id)">删除</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- <p class="tip" v-if="activeGroupId === groupIndex">+ 新词将添加到该分组</p>-->
|
||||
</div>
|
||||
|
||||
<div class="word-list">
|
||||
<div class="word-item"
|
||||
@click="select(index, item.id, $event)"
|
||||
v-for="(item, index) in group.dict"
|
||||
:key="item.id">
|
||||
<div class="checkbox-cell">
|
||||
<!-- <div :class="['checkbox-item', {active: chosenWordIdArray.some(i => i === item.id)}]"></div>-->
|
||||
<div :class="['checkbox-item', {active: chosenWordIds.has(item.id)}]"></div>
|
||||
</div>
|
||||
<div class="code">{{ item.code }}</div>
|
||||
<div class="word">{{ item.word }}</div>
|
||||
<div v-if="item.priority" title="权重" class="priority">{{ item.priority }}</div>
|
||||
<div class="id">{{ index }}</div>
|
||||
<div class="id">{{ item.id }}</div>
|
||||
<div class="operation">
|
||||
<div class="up" @click.stop="moveUp(item.id)"><img src="../../assets/img/up.svg" alt="move up"></div>
|
||||
<div class="down" @click.stop="moveDown(item.id)"><img src="../../assets/img/down.svg" alt="move down"></div>
|
||||
<div class="up" @click.stop="editWord(item)"><img src="../../assets/img/edit.svg" alt="edit"></div>
|
||||
<div class="down" @click.stop="deleteWord(item.id)"><img src="../../assets/img/delete.svg" alt="delete"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!--FOOTER-->
|
||||
<div class="footer">
|
||||
<div class="footer-toolbar">
|
||||
<div class="link-list">
|
||||
<div class="link" ref="domBtnSelectAll" @click="selectAll">全选</div>
|
||||
<div class="link" @click="resetInputs">清选</div>
|
||||
<div class="link" @click="reloadCurrentDict">重载码表</div>
|
||||
<div class="link" @click="sort">排序</div>
|
||||
<div class="link origin" @click="openCurrentYaml">{{ dict.fileName }}</div>
|
||||
</div>
|
||||
<div class="info-list">
|
||||
<div class="count">总<span class="number">{{dict.countDictOrigin}}</span></div>
|
||||
<div class="count"
|
||||
v-if="dict.countDictOrigin !== dict.countDict">显<span class="number">{{wordsCount}}</span></div>
|
||||
<div class="count">选<span class="number">{{chosenWordIds.size}}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tip">{{tip}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="App.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,622 @@
|
|||
const {shakeDom, shakeDomFocus, log} = require('../../js/Utility')
|
||||
const {IS_IN_DEVELOP} = require('../../js/Global')
|
||||
const path = require('path')
|
||||
|
||||
const Dict = require('../../js/Dict')
|
||||
const DictOther = require('../../js/DictOther')
|
||||
const DictMap = require('../../js/DictMap')
|
||||
const Word = require('../../js/Word')
|
||||
const Vue = require('../../node_modules/vue/dist/vue.common.prod')
|
||||
|
||||
const {ipcRenderer} = require('electron')
|
||||
const VirtualScroller = require('vue-virtual-scroller')
|
||||
|
||||
|
||||
// Vue 2
|
||||
const app = {
|
||||
el: '#app',
|
||||
components: {RecycleScroller: VirtualScroller.RecycleScroller},
|
||||
data() {
|
||||
return {
|
||||
IS_IN_DEVELOP: IS_IN_DEVELOP, // 是否为开发模式,html 使用
|
||||
tip: '', // 提示信息
|
||||
dict: {
|
||||
deep: true
|
||||
}, // 当前词库对象 Dict
|
||||
dictMain: {}, // 主码表 Dict
|
||||
keyword: '', // 搜索关键字
|
||||
code: '',
|
||||
word: '',
|
||||
activeGroupId: -1, // 组 index
|
||||
keywordUnwatch: null, // keyword watch 方法的撤消方法
|
||||
labelOfSaveBtn: '保存', // 保存按钮的文本
|
||||
heightContent: 0, // content 高度
|
||||
words: [], // 显示的 words
|
||||
|
||||
chosenWordIds: new Set(),
|
||||
chosenWordIdArray: [], // 对应上面的 set 内容
|
||||
lastChosenWordIndex: null, // 最后一次选中的 index
|
||||
|
||||
filePath: '', // 选择的文件路径
|
||||
fileName: '', // 选择的文件名
|
||||
|
||||
|
||||
targetDict: {}, // 要移动到的码表
|
||||
showDropdown: false, // 显示移动词条窗口
|
||||
dropdownFileList: [
|
||||
// {name: '拼音词库', path: 'pinyin_simp.dict.yaml'}
|
||||
],
|
||||
dropdownActiveFileIndex: -1, // 选中的
|
||||
dropdownActiveGroupIndex: -1, // 选中的分组 ID
|
||||
|
||||
config: {}, // 全局配置
|
||||
|
||||
// 码表配置
|
||||
seperatorRead: '\t', // 分隔符
|
||||
seperatorSave: '\t', // 分隔符
|
||||
seperatorArray: [
|
||||
{name: '空格', value: ' ',},
|
||||
{name: 'Tab', value: '\t',}
|
||||
], // 分隔符 数组
|
||||
dictFormatRead: 'wc', // 码表格式默认值
|
||||
dictFormatSave: 'wc', // 码表格式默认值
|
||||
dictFormatArray: [
|
||||
{name: '一码多词', value: 'cww',},
|
||||
{name: '一码一词', value: 'cw',},
|
||||
{name: '一词一码', value: 'wc',},
|
||||
{name: '纯词', value: 'w',}
|
||||
], // 码表格式数组
|
||||
filterCharacterLength: 0, // 筛选词条字数默认值
|
||||
filterCharacterLengthArray: [
|
||||
{name: '无', value: 0,},
|
||||
{name: '一', value: 1,},
|
||||
{name: '二', value: 2,},
|
||||
{name: '三', value: 3,},
|
||||
{name: '四', value: 4,},
|
||||
{name: '五+', value: 5,}
|
||||
], // 筛选词条字数数组
|
||||
fileNameSave: '', // 显示的保存文件名
|
||||
dictMap: null, // main 返回的 dictMap,用于解码词条
|
||||
|
||||
wordEditing: null, // 正在编辑的词条
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if(IS_IN_DEVELOP) this.filePath = 'C:\\Users\\Administrator\\AppData\\Roaming\\Rime\\origin.txt'
|
||||
|
||||
this.heightContent = innerHeight - 47 - 20 - 10 + 3
|
||||
// 载入主要操作码表文件
|
||||
ipcRenderer.on('showFileContent', (event, fileName, filePath, fileContent) => {
|
||||
// 过滤移动到的文件列表,不显示正在显示的这个码表
|
||||
this.dropdownFileList = this.dropdownFileList.filter(item => item.path !== fileName)
|
||||
|
||||
this.filePath = filePath
|
||||
this.fileName = fileName
|
||||
this.dict = new DictOther(fileContent, fileName, filePath, this.seperatorRead, this.dictFormatRead)
|
||||
this.fileNameSave = this.filePathSave()
|
||||
this.tipNotice('载入完成')
|
||||
// 载入新码表时,清除 word 保存 code
|
||||
this.word = ''
|
||||
this.refreshShowingWords()
|
||||
// this.search() // 配置项:切换码表是否自动搜索
|
||||
ipcRenderer.send('loadMainDict') // 请求主码表文件
|
||||
})
|
||||
ipcRenderer.on('saveFileSuccess', () => {
|
||||
this.labelOfSaveBtn = '保存成功'
|
||||
this.tipNotice('保存成功')
|
||||
this.$refs.domBtnSave.classList.add('btn-green')
|
||||
setTimeout(()=>{
|
||||
this.$refs.domBtnSave.classList.remove('btn-green')
|
||||
this.labelOfSaveBtn = '保存'
|
||||
}, 2000)
|
||||
})
|
||||
|
||||
// 由 window 触发获取文件目录的请求,不然无法实现适时的获取到 主进程返回的数据
|
||||
ipcRenderer.on('ToolWindow:FileList', (event, fileList) => {
|
||||
log(fileList)
|
||||
this.dropdownFileList = fileList
|
||||
})
|
||||
ipcRenderer.send('ToolWindow:GetFileList')
|
||||
|
||||
// 载入目标码表
|
||||
ipcRenderer.on('ToolWindow:SetTargetDict', (event, filename, filePath, res) => {
|
||||
this.targetDict = new Dict(res, filename, filePath)
|
||||
})
|
||||
|
||||
// 载入主码表
|
||||
ipcRenderer.on('setMainDict', (event, filename, res) => {
|
||||
this.dictMain = new Dict(res, filename)
|
||||
})
|
||||
|
||||
// 配置相关
|
||||
ipcRenderer.on('ToolWindow:ResponseConfigFile', (event, config) => {
|
||||
this.config = config
|
||||
log('窗口载入时获取到的 config 文件:', config)
|
||||
})
|
||||
ipcRenderer.send('ToolWindow:RequestConfigFile')
|
||||
|
||||
// 配置文件保存后,向主窗口更新配置文件内容
|
||||
ipcRenderer.on('updateConfigFile', (event, config) => {
|
||||
this.config = config
|
||||
})
|
||||
|
||||
// 获取并设置字典文件
|
||||
ipcRenderer.on('setDictMap', (event, fileContent, fileName, filePath) => {
|
||||
this.dictMap = new DictMap(fileContent, fileName, filePath)
|
||||
})
|
||||
ipcRenderer.send('getDictMap')
|
||||
|
||||
this.addKeyboardListener()
|
||||
onresize = ()=>{
|
||||
this.heightContent = innerHeight - 47 - 20 - 10 + 3
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 当前显示的 words 数量
|
||||
wordsCount(){
|
||||
if (this.dict.isGroupMode){
|
||||
let countCurrent = 0
|
||||
this.words.forEach(group => {
|
||||
countCurrent = countCurrent + group.dict.length
|
||||
})
|
||||
return countCurrent
|
||||
} else {
|
||||
return this.words.length
|
||||
}
|
||||
},
|
||||
// 当前载入的是否为 主 码表
|
||||
isInMainDict(){
|
||||
return this.dict.fileName === 'wubi86_jidian.dict.yaml'
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
tipNotice(msg){
|
||||
this.tip = msg
|
||||
setTimeout(()=>{this.tip = ''}, 3000)
|
||||
},
|
||||
// 确定编辑词条
|
||||
confirmEditWord(){
|
||||
this.wordEditing = null
|
||||
},
|
||||
// 生成编辑词条的编码
|
||||
generateCodeForWordEdit(){
|
||||
if (this.wordEditing){
|
||||
this.wordEditing.code = this.dictMap.decodeWord(this.wordEditing.word)
|
||||
} else {
|
||||
shakeDomFocus(this.$refs.editInputWord)
|
||||
}
|
||||
},
|
||||
// 编辑词条
|
||||
editWord(word){
|
||||
this.wordEditing = word
|
||||
},
|
||||
|
||||
generateCodeForAllWords(){
|
||||
this.dict.wordsOrigin.forEach(word => {
|
||||
word.setCode(this.dictMap.decodeWord(word.word))
|
||||
})
|
||||
this.refreshShowingWords()
|
||||
this.tipNotice('编码生成完成')
|
||||
},
|
||||
// 生成保存文件的文件名
|
||||
filePathSave(withFullPath){
|
||||
let filePathObject = path.parse(this.filePath)
|
||||
let type = ''
|
||||
switch (this.dictFormatSave){
|
||||
case 'cww': type = '一码多词';break;
|
||||
case 'wc': type = '一词一码';break;
|
||||
case 'cw': type = '一码一词';break;
|
||||
case 'w': type = '纯词';break;
|
||||
}
|
||||
let seperater = ''
|
||||
switch (this.seperatorSave){
|
||||
case ' ': seperater = '空格分隔';break;
|
||||
case '\t': seperater = 'Tab分隔';break;
|
||||
}
|
||||
if (withFullPath){
|
||||
return path.join(
|
||||
filePathObject.dir,
|
||||
filePathObject.name + '_' + type + '_' + seperater + filePathObject.ext
|
||||
)
|
||||
} else {
|
||||
return filePathObject.name + '_' + type + '_' + seperater + filePathObject.ext
|
||||
}
|
||||
},
|
||||
|
||||
checkRepetitionInOrder(characterMode){
|
||||
this.words = this.dict.getRepetition(characterMode)
|
||||
},
|
||||
// 根据码表的一些参数,重新载入当前文件
|
||||
reloadCurrentFile(){
|
||||
ipcRenderer.send('ToolWindow:loadFileContent', this.filePath)
|
||||
},
|
||||
// 筛选词条字数
|
||||
changeFilterWordLength(length){
|
||||
this.filterCharacterLength = parseInt(length)
|
||||
this.words = this.dict.getWordsLengthOf(length)
|
||||
},
|
||||
checkRepetition(includeCharacter){
|
||||
this.words = this.dict.getRepetitionWords(includeCharacter)
|
||||
},
|
||||
|
||||
// 载入码表文件
|
||||
loadDictFile(){
|
||||
ipcRenderer.send('ToolWindow:chooseDictFile')
|
||||
},
|
||||
select(index, wordId, event){
|
||||
if (event.shiftKey){
|
||||
if (this.lastChosenWordIndex !== null){
|
||||
let a,b // 判断大小,调整大小顺序
|
||||
if (index > this.lastChosenWordIndex){
|
||||
a = this.lastChosenWordIndex
|
||||
b = index
|
||||
} else {
|
||||
b = this.lastChosenWordIndex
|
||||
a = index
|
||||
}
|
||||
for (let i=a; i<=b; i++){
|
||||
this.chosenWordIds.add(this.words[i].id)
|
||||
}
|
||||
}
|
||||
this.lastChosenWordIndex = null // shift 选择后,最后一个id定义为没有
|
||||
|
||||
} else {
|
||||
if (this.chosenWordIds.has(wordId)){
|
||||
this.chosenWordIds.delete(wordId)
|
||||
this.lastChosenWordIndex = null
|
||||
} else {
|
||||
this.chosenWordIds.add(wordId)
|
||||
this.lastChosenWordIndex = index
|
||||
}
|
||||
}
|
||||
this.chosenWordIdArray = [...this.chosenWordIds.values()]
|
||||
},
|
||||
// 选择移动到的分组 index
|
||||
setDropdownActiveGroupIndex(index){
|
||||
this.dropdownActiveGroupIndex = index
|
||||
},
|
||||
// 选择移动到的文件 index
|
||||
setDropdownActiveIndex(fileIndex){
|
||||
this.dropdownActiveFileIndex = fileIndex
|
||||
this.dropdownActiveGroupIndex = -1 // 切换文件列表时,复位分组 fileIndex
|
||||
// this.dictSecond = {} // 立即清空次码表,分组列表也会立即消失,不会等下面的码表加载完成再清空
|
||||
ipcRenderer.send('ToolWindow:LoadTargetDict', this.dropdownFileList[fileIndex].path) // 载入当前 index 的文件内容
|
||||
},
|
||||
sort(){
|
||||
let startPoint = new Date().getTime()
|
||||
this.words.sort((a,b) => a.code < b.code ? -1: 1)
|
||||
this.tipNotice('排序完成')
|
||||
log(`排序用时 ${new Date().getTime() - startPoint} ms`)
|
||||
},
|
||||
enterKeyPressed(){
|
||||
switch (this.config.enterKeyBehavior){
|
||||
case "add":this.addNewWord(); break;
|
||||
case "search": this.search(); break;
|
||||
default: break;
|
||||
}
|
||||
},
|
||||
|
||||
// 通过 code, word 筛选词条
|
||||
search(){
|
||||
this.chosenWordIds.clear()
|
||||
this.chosenWordIdArray = []
|
||||
this.activeGroupId = -1 // 切到【全部】标签页,展示所有搜索结果
|
||||
let startPoint = new Date().getTime()
|
||||
if (this.code || this.word){
|
||||
this.words = this.dict.wordsOrigin.filter(item => { // 获取包含 code 的记录
|
||||
switch (this.config.searchMethod){
|
||||
case "code": return item.code.includes(this.code);
|
||||
case "phrase": return item.word.includes(this.word);
|
||||
case "both": return item.code.includes(this.code) && item.word.includes(this.word)
|
||||
case "any": return item.code.includes(this.code) || item.word.includes(this.word)
|
||||
}
|
||||
})
|
||||
log(`${this.code} ${this.word}: ` ,'搜索出', this.words.length, '条,', '用时: ', new Date().getTime() - startPoint, 'ms')
|
||||
} else { // 如果 code, word 为空,恢复原有数据
|
||||
this.refreshShowingWords()
|
||||
}
|
||||
},
|
||||
|
||||
// 刷新 this.words
|
||||
refreshShowingWords(){
|
||||
this.chosenWordIds.clear()
|
||||
this.chosenWordIdArray = []
|
||||
this.words = [...this.dict.wordsOrigin]
|
||||
},
|
||||
addNewWord(){
|
||||
if (!this.word){
|
||||
shakeDomFocus(this.$refs.domInputWord)
|
||||
} else if (!this.code){
|
||||
shakeDomFocus(this.$refs.domInputCode)
|
||||
} else {
|
||||
this.dict.addWordToDictInOrder(new Word(this.dict.lastIndex++, this.code, this.word))
|
||||
this.refreshShowingWords()
|
||||
log(this.code, this.word)
|
||||
}
|
||||
},
|
||||
// 保存内容到文件
|
||||
saveToFile(dict, isSaveToOriginalFilePath){
|
||||
if (this.dict.lastIndex >= 1){ // 以 dict 的 lastIndex 作为判断有没有加载码表的依据
|
||||
if (isSaveToOriginalFilePath){ // 保存到原来文件,针对工具里打开的文件,和词条移动的目标文件
|
||||
log('保存文件路径: ', dict.filePath)
|
||||
ipcRenderer.send(
|
||||
'ToolWindow:SaveFile',
|
||||
dict.filePath,
|
||||
dict.toYamlString())
|
||||
} else { // 保存成新文件,新文件名,只针对工具里打开的码表
|
||||
log('保存文件路径: ', this.filePathSave(true))
|
||||
ipcRenderer.send(
|
||||
'ToolWindow:SaveFile',
|
||||
this.filePathSave(true),
|
||||
this.dict.toExportString(this.seperatorSave, this.dictFormatSave))
|
||||
}
|
||||
} else {
|
||||
log('未加载任何码表文件')
|
||||
}
|
||||
},
|
||||
// 选中全部展示的词条
|
||||
selectAll(){
|
||||
if(this.wordsCount < 100000){ // 最多同时选择 10w 条数据
|
||||
if (this.dict.isGroupMode){
|
||||
this.chosenWordIds.clear()
|
||||
this.chosenWordIdArray = []
|
||||
this.words.forEach(group => {
|
||||
group.forEach( item => {
|
||||
this.chosenWordIds.add(item.id)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
this.words.forEach(item => {this.chosenWordIds.add(item.id)})
|
||||
}
|
||||
this.chosenWordIdArray = [...this.chosenWordIds.values()]
|
||||
} else {
|
||||
// 提示不能同时选择太多内容
|
||||
this.tip = '不能同时选择大于 1000条 的词条内容'
|
||||
shakeDom(this.$refs.domBtnSelectAll)
|
||||
}
|
||||
},
|
||||
// 清除内容
|
||||
resetInputs(){
|
||||
this.chosenWordIds.clear()
|
||||
this.chosenWordIdArray = []
|
||||
this.code = ''
|
||||
this.word = ''
|
||||
this.search()
|
||||
this.tip = ''
|
||||
},
|
||||
// 删除词条:单
|
||||
deleteWord(wordId){
|
||||
this.chosenWordIds.delete(wordId)
|
||||
this.chosenWordIdArray = [...this.chosenWordIds.values()]
|
||||
this.dict.deleteWords(new Set([wordId]))
|
||||
this.refreshShowingWords()
|
||||
},
|
||||
// 删除词条:多
|
||||
deleteWords(){
|
||||
this.dict.deleteWords(this.chosenWordIds)
|
||||
this.refreshShowingWords()
|
||||
this.chosenWordIds.clear() // 清空选中 wordID
|
||||
this.chosenWordIdArray = []
|
||||
},
|
||||
|
||||
// 词条位置移动
|
||||
move(wordId, direction){
|
||||
if (this.dict.isGroupMode){
|
||||
// group 时,移动 调换 word 位置,是直接调动的 wordsOrigin 中的word
|
||||
// 因为 group 时数据为: [{word, word},{word,word}],是 wordGroup 的索引
|
||||
for(let i=0; i<this.words.length; i++){
|
||||
let group = this.words[i]
|
||||
for(let j=0; j<group.dict.length; j++){
|
||||
if (wordId === group.dict[j].id){
|
||||
let tempItem = group.dict[j]
|
||||
if (direction === 'up'){
|
||||
if (j !==0){
|
||||
group.dict[j] = group.dict[j - 1]
|
||||
group.dict[j - 1] = tempItem
|
||||
return ''
|
||||
} else {
|
||||
log('已到顶')
|
||||
return '已到顶'
|
||||
}
|
||||
} else if (direction === 'down'){
|
||||
if (j+1 !== group.dict.length){
|
||||
group.dict[j] = group.dict[j + 1]
|
||||
group.dict[j + 1] = tempItem
|
||||
return ''
|
||||
} else {
|
||||
log('已到底')
|
||||
return '已到底'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 非分组模式时,调换位置并不能直接改变 wordsOrigin 因为 与 words 已经断开连接
|
||||
// [word, word]
|
||||
for(let i=0; i<this.words.length; i++){
|
||||
if (wordId === this.words[i].id){
|
||||
let tempItem = this.words[i]
|
||||
if (direction === 'up'){
|
||||
if (i !==0) {
|
||||
this.dict.exchangePositionInOrigin(tempItem, this.words[i-1]) // 调换 wordsOrigin 中的词条位置
|
||||
this.words[i] = this.words[i - 1]
|
||||
this.words[i - 1] = tempItem
|
||||
return ''
|
||||
} else {
|
||||
log('已到顶')
|
||||
return '已到顶'
|
||||
}
|
||||
} else if (direction === 'down'){
|
||||
if (i+1 !== this.words.length) {
|
||||
this.dict.exchangePositionInOrigin(tempItem, this.words[i+1]) // 调换 wordsOrigin 中的词条位置
|
||||
this.words[i] = this.words[i + 1]
|
||||
this.words[i + 1] = tempItem
|
||||
return ''
|
||||
} else {
|
||||
log('已到底')
|
||||
return '已到底'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 上移词条
|
||||
moveUp(id){
|
||||
this.tip = this.move(id, 'up')
|
||||
let temp = this.words.pop()
|
||||
this.words.push(temp)
|
||||
},
|
||||
// 下移词条
|
||||
moveDown(id){
|
||||
this.tip = this.move(id, 'down')
|
||||
let temp = this.words.pop()
|
||||
this.words.push(temp)
|
||||
},
|
||||
// 判断是否为第一个元素
|
||||
isFirstItem(id){
|
||||
if (this.dict.isGroupMode){ // 分组时的第一个元素
|
||||
for (let i=0; i<this.words.length; i++) {
|
||||
for (let j = 0; j < this.words[i].dict.length; j++) {
|
||||
if (this.words[i].dict[j].id === id){
|
||||
return j === 0 // 使用 array.forEach() 无法跳出循环
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
} else {
|
||||
for (let i = 0; i < this.words.length; i++) {
|
||||
if (this.words[i].id === id){
|
||||
return i === 0 // 使用 array.forEach() 无法跳出循环
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
// 判断是否为最后一个元素
|
||||
isLastItem(id){
|
||||
if (this.dict.isGroupMode){ // 分组时的最后一个元素
|
||||
for (let i=0; i<this.words.length; i++) {
|
||||
for (let j = 0; j < this.words[i].dict.length; j++) {
|
||||
if (this.words[i].id === id){
|
||||
return j + 1 === this.words.length
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
} else {
|
||||
for (let i = 0; i < this.words.length; i++) {
|
||||
if (this.words[i].id === id){
|
||||
return i + 1 === this.words.length
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
// 绑定键盘事件: 键盘上下控制词条上下移动
|
||||
addKeyboardListener(){
|
||||
window.addEventListener('keydown', event => {
|
||||
// log(event)
|
||||
switch( event.key) {
|
||||
case 's':
|
||||
if (event.ctrlKey || event.metaKey){ // metaKey 是 macOS 的 Ctrl
|
||||
this.saveToFile(this.dict)
|
||||
event.preventDefault()
|
||||
} else {
|
||||
|
||||
}
|
||||
break
|
||||
case 'ArrowDown':
|
||||
if(this.chosenWordIds.size === 1) { // 只有一个元素时,键盘才起作用
|
||||
let id = [...this.chosenWordIds.values()][0]
|
||||
this.moveDown(id)
|
||||
}
|
||||
event.preventDefault()
|
||||
break
|
||||
case 'ArrowUp':
|
||||
if(this.chosenWordIds.size === 1) { // 只有一个元素时,键盘才起作用
|
||||
let id = [...this.chosenWordIds.values()][0]
|
||||
this.moveUp(id)
|
||||
}
|
||||
event.preventDefault()
|
||||
break
|
||||
}
|
||||
})
|
||||
},
|
||||
// 将选中的词条移动到指定码表
|
||||
moveWordsToTargetDict(){
|
||||
let wordsTransferring = this.dict.wordsOrigin.filter(item => this.chosenWordIds.has(item.id)) // 被转移的 [Word]
|
||||
log('words transferring:', JSON.stringify(wordsTransferring))
|
||||
|
||||
this.targetDict.addWordsInOrder(wordsTransferring, this.dropdownActiveGroupIndex)
|
||||
|
||||
this.words = [...this.dict.wordsOrigin]
|
||||
log('after insert:( main:wordOrigin ):\n ', JSON.stringify(this.targetDict.wordsOrigin))
|
||||
|
||||
this.deleteWords() // 删除当前词库已移动的词条
|
||||
this.saveToFile(this.targetDict, true)
|
||||
this.saveToFile(this.dict, true)
|
||||
this.tipNotice('移动成功')
|
||||
this.resetDropList()
|
||||
},
|
||||
// 复制 dropdown
|
||||
resetDropList(){
|
||||
this.showDropdown = false
|
||||
this.dropdownActiveFileIndex = -1
|
||||
this.dropdownActiveGroupIndex = -1
|
||||
this.targetDict = {} // 清空次码表
|
||||
},
|
||||
// 打开当前码表源文件
|
||||
openCurrentYaml(){
|
||||
ipcRenderer.send('openFileOutside', this.dict.fileName)
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
code(newValue){
|
||||
this.code = newValue.replaceAll(/[^A-Za-z ]/g, '') // input.code 只允许输入字母
|
||||
},
|
||||
word(newValue, oldValue){
|
||||
if (newValue.length < oldValue.length){
|
||||
// 删除或清空时,不清空编码
|
||||
} else {
|
||||
if (this.dictMap){
|
||||
this.code = this.dictMap.decodeWord(newValue)
|
||||
}
|
||||
}
|
||||
},
|
||||
seperatorSave(){
|
||||
this.fileNameSave = this.filePathSave()
|
||||
},
|
||||
dictFormatSave(){
|
||||
this.fileNameSave = this.filePathSave()
|
||||
},
|
||||
chosenWordIdArray(newValue){
|
||||
if (newValue.length === 0){
|
||||
this.showDropdown = false
|
||||
}
|
||||
log('已选词条id: ', JSON.stringify(newValue))
|
||||
},
|
||||
showDropdown(newValue){
|
||||
if (!newValue){ // 窗口关闭时,重置 index
|
||||
this.resetDropList()
|
||||
}
|
||||
},
|
||||
config: (newValue) => {
|
||||
switch (newValue.theme){
|
||||
case "auto":
|
||||
document.documentElement.classList.add('auto-mode');
|
||||
document.documentElement.classList.remove('dark-mode');
|
||||
break;
|
||||
case "black":
|
||||
document.documentElement.classList.add('dark-mode');
|
||||
document.documentElement.classList.remove('auto-mode');
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
new Vue(app)
|
|
@ -0,0 +1,307 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>码表处理工具</title>
|
||||
<link rel="stylesheet" href="../../assets/scss/wubi.css">
|
||||
<link rel="stylesheet" href="../../node_modules/vue-virtual-scroller/dist/vue-virtual-scroller.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div v-cloak id="app" class="tool">
|
||||
<!--词条编辑窗口-->
|
||||
<div class="edit-modal modal" v-if="wordEditing">
|
||||
<div class="modal-panel">
|
||||
<div class="modal-header">
|
||||
<div class="id">{{wordEditing.id}}</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input ref="editInputWord" class="mb-1" type="text" v-model="wordEditing.word" >
|
||||
<input ref="editInputCode" type="text" v-model="wordEditing.code" >
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="btn btn-cyan mr-2" @click="generateCodeForWordEdit">生成编码</div>
|
||||
<div class="btn btn-roseo" @click="confirmEditWord">确定</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--搜索框-->
|
||||
<div class="search-bar" v-if="dict">
|
||||
<div class="dropdown">
|
||||
<div class="dropdown-link btn btn-blue" v-show="chosenWordIdArray.length > 0" @click.capture="showDropdown = !showDropdown">移动到... </div>
|
||||
<div class="dropdown-body" v-show="showDropdown">
|
||||
<div class="file-list shadow" v-if="dropdownFileList.length > 0">
|
||||
<div :class="['file-list-item', {active: fileIndex === dropdownActiveFileIndex}]"
|
||||
@click="setDropdownActiveIndex(fileIndex)"
|
||||
v-for="(file, fileIndex) in dropdownFileList"
|
||||
:key="fileIndex">
|
||||
<div>{{ file.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-list shadow" v-if="targetDict.isGroupMode">
|
||||
<div :class="['file-list-item', {active: index === dropdownActiveGroupIndex}]"
|
||||
v-if="targetDict.isGroupMode"
|
||||
@click="setDropdownActiveGroupIndex(index)"
|
||||
v-for="(group, index) in targetDict.wordsOrigin"
|
||||
:key="index">
|
||||
<div>{{ group.groupName }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn btn-blue shadow ml-1"
|
||||
@click="moveWordsToTargetDict"
|
||||
v-if="targetDict.isGroupMode && dropdownActiveGroupIndex !== -1 || !targetDict.isGroupMode && dropdownActiveFileIndex !== -1"
|
||||
>确定</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-item">
|
||||
<input class="word" ref="domInputWord" @keyup.enter="enterKeyPressed" v-model="word" type="text" placeholder="词条">
|
||||
<div @click="word = ''" v-show="word" class="btn-clear">
|
||||
<img src="../../assets/img/delete_white.svg" alt="clear">
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-item">
|
||||
<input class="code" ref="domInputCode" @keyup.enter="enterKeyPressed" v-model="code" type="text" placeholder="编码">
|
||||
<div @click="code = ''" v-show="code" class="btn-clear">
|
||||
<img src="../../assets/img/delete_white.svg" alt="clear">
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn btn-primary" @click="addNewWord">添加</div>
|
||||
<div class="btn btn-primary" @click="search">搜索</div>
|
||||
<div class="btn btn-roseo" v-show="chosenWordIdArray.length > 0" @click="deleteWords">删除</div>
|
||||
<p class="notice">显示格式:<b>编码</b> - 词条 - 序号 - id</p>
|
||||
|
||||
<template v-if="IS_IN_DEVELOP">
|
||||
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- normal-mode-->
|
||||
<div ref="container"
|
||||
v-if="!dict.isGroupMode"
|
||||
:style="`height: ${heightContent}px`"
|
||||
class="container tool-container">
|
||||
<div class="tool-panel"
|
||||
:style="`height: ${heightContent}px`"
|
||||
>
|
||||
|
||||
<section>
|
||||
<div class="title">码表读取</div>
|
||||
<div class="content">
|
||||
<div class="load-list">
|
||||
<div class="btn btn-ellipsis btn-orange btn-load" v-if="dict.fileName" @click="loadDictFile" >{{ dict.fileName}}</div>
|
||||
<div class="btn btn-ellipsis btn-primary btn-load" v-else @click="loadDictFile" >选择码表文件</div>
|
||||
<div class="btn btn-primary center btn-reload" @click="reloadCurrentFile">重新载入</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="title">码表格式</div>
|
||||
<div class="content">
|
||||
<div class="btn-list">
|
||||
<div class="btn-item" v-for="item in dictFormatArray" :key="item.name">
|
||||
<div :class="['btn btn-primary', {'btn-blue': dictFormatRead === item.value}]" @click="dictFormatRead = item.value">{{item.name}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="title">字数筛选</div>
|
||||
<div class="content btn-list">
|
||||
<div class="btn-item" v-for="item in filterCharacterLengthArray" :key="item.name">
|
||||
<div @click="changeFilterWordLength(item.value)" :class="['btn btn-primary', {'btn-blue': filterCharacterLength === item.value}]">{{ item.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="title">码表分隔符</div>
|
||||
<div class="content">
|
||||
<div class="btn-list">
|
||||
<div class="btn-item" v-for="item in seperatorArray" :key="item.name">
|
||||
<div :class="['btn btn-primary', {'btn-blue': seperatorRead === item.value}]" @click="seperatorRead = item.value">{{item.name}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="title">查重</div>
|
||||
<div class="content">
|
||||
<div class="btn-list">
|
||||
<div class="btn-item">
|
||||
<div class="btn btn-primary" @click="checkRepetition(true)">单字</div>
|
||||
</div>
|
||||
<div class="btn-item">
|
||||
<div class="btn btn-primary" @click="checkRepetition(false)">词组</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr>
|
||||
|
||||
<!--保存-->
|
||||
<section>
|
||||
<div class="title">码表保存</div>
|
||||
<!-- 分隔符 -->
|
||||
<div class="content">
|
||||
<div class="btn-list">
|
||||
<div class="btn-item" v-for="item in seperatorArray" :key="item.name">
|
||||
<div :class="['btn btn-primary', {'btn-green': seperatorSave === item.value}]" @click="seperatorSave = item.value">{{item.name}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 码表格式 -->
|
||||
<div class="btn-list">
|
||||
<div class="btn-item" v-for="item in dictFormatArray" :key="item.name">
|
||||
<div :class="['btn btn-primary', {'btn-green': dictFormatSave === item.value}]" @click="dictFormatSave = item.value">{{item.name}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 保存-->
|
||||
<div class="load-list mb-2">
|
||||
<div v-if="fileNameSave" class="btn btn-orange btn-load btn-ellipsis center">{{ fileNameSave }}</div>
|
||||
<div
|
||||
:class="[
|
||||
'btn', 'center', 'btn-reload',
|
||||
{'btn-green': labelOfSaveBtn === '保存成功'},
|
||||
{'btn-primary': labelOfSaveBtn !== '保存成功'}]"
|
||||
ref="domBtnSave"
|
||||
@click="saveToFile(dict)"> {{ labelOfSaveBtn }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
:class="[
|
||||
'btn', 'center',
|
||||
{'btn-green': labelOfSaveBtn === '保存成功'},
|
||||
{'btn-primary': labelOfSaveBtn !== '保存成功'}]"
|
||||
ref="domBtnSave"
|
||||
@click="saveToFile(dict, true)"> 保存到源文件
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- 编码处理-->
|
||||
<section>
|
||||
<div class="title">编码生成</div>
|
||||
<div class="content">
|
||||
<div class="btn-list">
|
||||
<div class="btn btn-primary center" @click="generateCodeForAllWords">生成所有词条的编码</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- <section>
|
||||
<div class="title">编码查错</div>
|
||||
<div class="content">
|
||||
<div class="btn-list">
|
||||
<div class="btn btn-primary center" @click="">据此码表生成编码</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>-->
|
||||
|
||||
<hr>
|
||||
<section>
|
||||
<div class="title">使用说明</div>
|
||||
<div class="content">
|
||||
<div class="readme">
|
||||
<div class="readme-item">
|
||||
<h3>载入码表</h3>
|
||||
<p>载入码表的时候,需要保证码表文件是 <i>UTF8</i> 编码的,如果不是,可以使用 <i>VSCode</i>(支持所有平台) 进行编码转换,具体百度即可。</p>
|
||||
<p>然后选择码表文件中词条与编码的排列方式,以什么分隔符分隔的。</p>
|
||||
<p>点击 <i>重新载入</i>重新识别码表内容即可,注意列表中呈现的内容顺序是 <i>编码</i> - <i>词条</i> - <i>序号</i> - <i>id</i> 注意编码与词条位置调换的情况。</p>
|
||||
</div>
|
||||
<div class="readme-item">
|
||||
<h3>保存码表</h3>
|
||||
<p>保存码表时,可以选择保存词条的编码和内容的顺序: <i>前词后码</i>、<i>前码后词</i>、<i>一码多词</i>。</p>
|
||||
<p>还可以选择编码与词条的分隔符: <i>空格</i> 或 <i>tab</i>。</p>
|
||||
<p>保存文件时,可以选择保存到源文件,或者保存成下方显示的文件名中。</p>
|
||||
</div>
|
||||
<div class="readme-item">
|
||||
<h3>移动词条到现有词库</h3>
|
||||
<p>选择当前窗口中的词条,移动到 Rime 码表中的时候,会删除该码表文件中的对应词条,并实时保存到原码表文件。</p>
|
||||
<p>保险起见,最好在使用工具前备份一下你要操作的码表文件。</p>
|
||||
</div>
|
||||
<div class="readme-item">
|
||||
<h3>关于批量选择</h3>
|
||||
<p>选择词条时,不再需要点击前方的小方块,点击整条也是同样的效果。</p>
|
||||
<p>按 <i>shift键</i> 即可批量选择</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 主词条列表 -->
|
||||
|
||||
<div class="list-container">
|
||||
<div class="group">
|
||||
<recycle-scroller
|
||||
:buffer="1000"
|
||||
:prerender="200"
|
||||
:style="`height: ${heightContent}px`"
|
||||
v-if="words"
|
||||
:min-item-size="24"
|
||||
class="virtual-list"
|
||||
:items="words">
|
||||
<template v-slot="{ item, index }">
|
||||
<div class="word-item" @click="select(index, item.id, $event)">
|
||||
<div class="checkbox-cell">
|
||||
<!-- <div :class="['checkbox-item', {active: chosenWordIdArray.some(i => i === item.id)}]"></div>-->
|
||||
<div :class="['checkbox-item', {active: chosenWordIds.has(item.id)}]"></div>
|
||||
</div>
|
||||
<div class="code">{{ item.code }}</div>
|
||||
<div class="word">{{ item.word }}</div>
|
||||
<div v-if="item.priority" title="权重" class="priority">{{ item.priority }}</div>
|
||||
<div class="id">{{ index + 1 }}</div>
|
||||
<div class="id">{{ item.id }}</div>
|
||||
<div class="operation">
|
||||
<div class="up" @click.stop="moveUp(item.id)">
|
||||
<img src="../../assets/img/up.svg" alt="move up">
|
||||
</div>
|
||||
<div class="down" @click.stop="moveDown(item.id)">
|
||||
<img src="../../assets/img/down.svg" alt="move down">
|
||||
</div>
|
||||
<div class="down" @click.stop="editWord(item)">
|
||||
<img src="../../assets/img/edit.svg" alt="edit">
|
||||
</div>
|
||||
<div class="down" @click.stop="deleteWord(item.id)">
|
||||
<img src="../../assets/img/delete.svg" alt="move down">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</recycle-scroller>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--FOOTER-->
|
||||
<div class="footer">
|
||||
<div class="footer-toolbar">
|
||||
<div class="link-list">
|
||||
<div class="link" ref="domBtnSelectAll" @click="selectAll">全选</div>
|
||||
<div class="link" @click="resetInputs">清选</div>
|
||||
<div class="link" @click="reloadCurrentFile">重载码表</div>
|
||||
<div class="link" @click="sort">排序</div>
|
||||
<div class="link origin" @click="openCurrentYaml">{{ dict.fileName }}</div>
|
||||
</div>
|
||||
<div class="info-list">
|
||||
<div class="count">总<span class="number">{{dict.countDictOrigin}}</span></div>
|
||||
<div class="count"
|
||||
v-if="dict.countDictOrigin !== dict.countDict">显<span class="number">{{wordsCount}}</span></div>
|
||||
<div class="count">选<span class="number">{{chosenWordIds.size}}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tip">{{tip}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="Tool.js"></script>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue