Refactor resource categories in doc.vue

Removed duplicate resource entries and updated links for categories.
This commit is contained in:
Zyronon 2026-01-15 14:31:59 +08:00 committed by GitHub
commit 2aabd42bcd
237 changed files with 68302 additions and 0 deletions

4
.cursorignore Normal file
View File

@ -0,0 +1,4 @@
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
public/
js_node/

12
.editorconfig Normal file
View File

@ -0,0 +1,12 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false

2
.env Normal file
View File

@ -0,0 +1,2 @@
VITE_ROUTE_BASE=/

46
.github/workflows/deploy-aliyun-oss.yml vendored Normal file
View File

@ -0,0 +1,46 @@
name: Deploy to Aliyun OSS
on:
push:
branches: [ 'master' ]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
jobs:
build:
runs-on: ubuntu-latest
outputs:
build-path: dist
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 8
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 24
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm run build-oss
- name: Deploy to OSS + Refresh CDN
run: pnpm run deploy-oss
env:
OSS_KEY_ID: ${{ secrets.OSS_KEY_ID }}
OSS_KEY_SECRET: ${{ secrets.OSS_KEY_SECRET }}
OSS_BUCKET: ${{ secrets.OSS_BUCKET }}
OSS_REGION: ${{ secrets.OSS_REGION }}
CDN_DOMAIN: ${{ secrets.CDN_DOMAIN }}

47
.github/workflows/deploy-pages.yml vendored Normal file
View File

@ -0,0 +1,47 @@
name: Deploy to GitHub Pages
on:
push:
branches: [ 'master' ]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
jobs:
build:
runs-on: ubuntu-latest
outputs:
build-path: dist
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 8
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 24
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm run build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
# Upload dist repository
path: './dist'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

20
.github/workflows/readme.yml vendored Normal file
View File

@ -0,0 +1,20 @@
name: Translate README
on:
# 这个选项可以使你手动在 Action tab 页面触发工作流
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: 12.x
# ISO Langusge Codes: https://cloud.google.com/translate/docs/languages
- name: Adding README - English
uses: dephraiim/translate-readme@main
with:
LANG: en

25
.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
report.html

3
.husky/post-checkout Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { printf >&2 "\n%s\n\n" "This repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'post-checkout' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks')."; exit 2; }
git lfs post-checkout "$@"

3
.husky/post-commit Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { printf >&2 "\n%s\n\n" "This repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'post-commit' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks')."; exit 2; }
git lfs post-commit "$@"

3
.husky/post-merge Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { printf >&2 "\n%s\n\n" "This repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'post-merge' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks')."; exit 2; }
git lfs post-merge "$@"

4
.husky/pre-commit Normal file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm test

3
.husky/pre-push Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { printf >&2 "\n%s\n\n" "This repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'pre-push' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks')."; exit 2; }
git lfs pre-push "$@"

2
.prettierignore Normal file
View File

@ -0,0 +1,2 @@
dist
node_modules

9
.prettierrc Normal file
View File

@ -0,0 +1,9 @@
{
"semi": false,
"singleQuote": true,
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5",
"arrowParens": "avoid",
"bracketSpacing": true
}

1
.vercel/project.json Normal file
View File

@ -0,0 +1 @@
{"projectName":"trae_typing-word_c29f"}

1
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1 @@
{"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]}

674
LICENSE Normal file
View File

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

74
Note.md Normal file
View File

@ -0,0 +1,74 @@
多语言地址https://www.yuque.com/zhangyurang/ghwvmn/idhcg59wc2204o5n#Ah4t
例句:On Jan. 20, former Sen. Barack Obama became the 44th President of the U.S. Millions attended the Inauguration.
sentence-tokenizer
可以正常断句,但无法识别例句
sentence-splitter
无法正常断句(在引号里面的问号和感叹号会被断句),但可以识别例句
https://github.com/Tessmore/sbd
无法识别Mr.James Scott has a garage in Silbury and now he has just bought another garage in Pinhurst.
会将Mr.James 断句
wink-nlp
'What a day!' I thought.
'What are you doing?' she asked.
会被断句
compromise
表现良好,另外自带分词功能
以上所有库都会将:'Do you always get up so late? It's one o'clock!'分成两句....
1 错题本,添加错误次数
bug
换段的时候没发音
打完一段的一最后一行的时候,没有自动换行,需要按下空格才能换段
打完了没检测到
所有的图标hover时有放大效果
各种声音可以单独调节音量大小
列表加搜索
BaseIcon 在选中模式下,应该显示白色
添加文章时正文输入123报错
没有内容时,要显示占位符
A cold welcome 有bug
[EditAbleText.vue](src%2Fcomponents%2FEditAbleText.vue) 不能自动聚焦
单词发音,点击第二遍时减速
http://enpuz.com/ 语法分析工具
键盘音效应该多放几遍
加载单词列表时需要loading
点击句子播放的音乐,需要可暂停
footer 的输入数统计有问题,当在列表点一个,然后输入错误之后,不会统计到输入数里面(单词和文章的都有问题)
nce1-16.A polite request.解析出来有问题
I found this note on my car: 'Sir, we welcome you to our city. This is a 'No Parking' area. You will enjoy your stay here if you pay attention to our street signs. This note is only a reminder.' If you receive a request like this, you cannot fail to obey it!
'No Parking' 会被截断
Food and talk
A new play is coming to "The Globe"soon, I said. Will you be seeing it?
26的 of curse
1、例句可以选中单词并添加到收藏
2、ABC页面太墨迹不简洁进度复杂本周学习记录改成日历有个标记+激励分享功能,满足炫耀欲望
25/11/18 新网站新增同步数据弹框
25/11/20 完善登录页面

99
README.md Normal file
View File

@ -0,0 +1,99 @@
<h1 align=center>
<img src="https://github.com/user-attachments/assets/9d626e0f-0601-4640-8981-ad66d8ac4853" alt="TypeWords" style="width: 500px;"/>
</h1>
<p align="center">
<a href="/docs/README.en.md">English</a> | <a href="/README.md">简体中文</a>
</p>
<p align="center">
<b>学习英语,一次敲击,一点进步;记忆不再盲目,学习更高效,开源单词与文章练习工具</b>
</p>
<p align="center">
<a href="https://github.com/zyronon/type-word/blob/master/LICENSE"><img src="https://img.shields.io/github/license/zyronon/type-word" alt="License"></a>
<a><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg"/></a>
<a><img src="https://img.shields.io/badge/Powered%20by-Vue-blue"/></a>
<a href="https://hellogithub.com/repository/eb70616d65604458908fc1736e7d41fc" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=eb70616d65604458908fc1736e7d41fc&claim_uid=k5e4ZAqRjJEGzCW&theme=small" alt="FeaturedHelloGitHub" /></a>
</p>
<div align=center>
<a href="https://trendshift.io/repositories/15226" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15226" alt="zyronon%2FTypeWords | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</div>
<img width="1920" height="1440" alt="295shots_so" src="https://github.com/user-attachments/assets/383ed437-856e-48fe-92b0-9619babb49be" />
<img width="1920" height="1440" alt="922shots_so" src="https://github.com/user-attachments/assets/5b5fa13f-747c-4368-ae21-3c9d7d30fbc7" />
## 在线访问
[https://typewords.cc](https://typewords.cc)
## 功能列表
### 单词练习
- 四种输入模式:跟打 / 辨认 / 复习 / 默写
- 智能模式:记忆曲线自动计算学习单词,并通过默写加深记忆
- 自由模式:不受限制,自行规划
- 提供音标、发音(美音、英音)、例句、短语、近义词、同根词、词源、错误统计等功能
### 背文章
- 内置经典教材书籍,也可自行添加、导入文章,提供一键翻译、译文对照功能
- 跟打 + 默写双模式,逐句输入,自动发音,让背诵更高效
- 支持边听边默写,强化记忆
### 收藏、错词本、已掌握
- 学习单词时输入错误自动添加到错词本,方便后续复习
- 可主动添加到已掌握,后续学习时自动跳过
- 可主动添加到收藏中,以便巩固复习
### 高度自由
- 丰富的键盘音效
- 可自定义快捷键
- 高度定制化的设置选项
### 简洁高效
- 简洁设计现代化UI无广告
- 界面清爽,操作简单
- 不强制关注任何平台
### 词库
内置了常用的 CET-4 、CET-6 、GMAT 、GRE 、IELTS 、SAT 、TOEFL 、考研英语、专业四级英语、专业八级英语等词库。
尽可能满足大部分用户对背单词的需求,也非常欢迎社区贡献更多的词库。
## 运行
#### 注:本项目可单独运行,数据保存在本地,换设备需手动备份数据,不影响正常使用;
本项目是基于`Vue`开发的,需要 node 环境来运行。
1. 安装 NodeJS参考[官方文档](https://nodejs.org/en/download)
2. 项目文件很大,推荐使用 `git clone --depth 1 https://github.com/zyronon/TypeWords.git` 命令只克隆最近一次提交。直接下载
Github 提供的 Download ZIP 功能是无法运行的
3. 在项目根目录下,打开命令行,运行`npm install`来下载依赖。
4. 执行`npm run dev`来启动项目,项目默认地址为[`http://localhost:3000`](http://localhost:3000)
5. 在浏览器中打开[`http://localhost:3000`](http://localhost:3000) 来访问项目。
6. 执行`npm run build`打包项目文件
## 功能与建议
目前项目处于开发初期,新功能正在持续添加中,如果你对软件有任何功能与建议,欢迎在 `Issues` 中提出
如果你也喜欢本软件的设计思想,欢迎提交 `pr`,非常感谢你对我们的支持!
## 贡献指南
[贡献准则](/docs//CONTRIBUTING.md)
如果您对本项目感兴趣,我们非常欢迎参与到项目的贡献中,我们会尽可能地提供帮助
在贡献前,希望您阅读 [Issue #57](https://github.com/zyronon/TypeWords/issues/57) 了解我们目前的开发计划,我们希望您能参与到"计划中"的工作亦或者 Issue 区 Label 为 "Help Wanted" 的工作,我们也非常欢迎您实现自己的想法。
如果您确定了想要参与的工作,希望在有基本进展后提交 draft pr我们可以在 draft pr 上进行讨论,也有利于听取其他 collaborator 的意见。
再次感谢您对项目的贡献!🎉

197
components.d.ts vendored Normal file
View File

@ -0,0 +1,197 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
About: typeof import('./src/components/About.vue')['default']
ArticleAudio: typeof import('./src/components/article/ArticleAudio.vue')['default']
ArticleList: typeof import('./src/components/list/ArticleList.vue')['default']
ArticleSetting: typeof import('./src/components/setting/ArticleSetting.vue')['default']
Audio: typeof import('./src/components/base/Audio.vue')['default']
BackIcon: typeof import('./src/components/BackIcon.vue')['default']
BaseButton: typeof import('./src/components/BaseButton.vue')['default']
BaseIcon: typeof import('./src/components/BaseIcon.vue')['default']
BaseInput: typeof import('./src/components/base/BaseInput.vue')['default']
BaseList: typeof import('./src/components/list/BaseList.vue')['default']
BasePage: typeof import('./src/components/BasePage.vue')['default']
BaseTable: typeof import('./src/components/BaseTable.vue')['default']
Book: typeof import('./src/components/Book.vue')['default']
ChangeLastPracticeIndexDialog: typeof import('./src/components/word/ChangeLastPracticeIndexDialog.vue')['default']
ChannelIcons: typeof import('./src/components/ChannelIcons/ChannelIcons.vue')['default']
Checkbox: typeof import('./src/components/base/checkbox/Checkbox.vue')['default']
Close: typeof import('./src/components/icon/Close.vue')['default']
Code: typeof import('./src/components/user/Code.vue')['default']
Collapse: typeof import('./src/components/base/Collapse.vue')['default']
CommonSetting: typeof import('./src/components/setting/CommonSetting.vue')['default']
ConflictNotice: typeof import('./src/components/ConflictNotice.vue')['default']
ConflictNoticeText: typeof import('./src/components/ConflictNoticeText.vue')['default']
DeleteIcon: typeof import('./src/components/icon/DeleteIcon.vue')['default']
Dialog: typeof import('./src/components/dialog/Dialog.vue')['default']
DictGroup: typeof import('./src/components/list/DictGroup.vue')['default']
DictList: typeof import('./src/components/list/DictList.vue')['default']
EditAbleText: typeof import('./src/components/EditAbleText.vue')['default']
EditArticle: typeof import('./src/components/article/EditArticle.vue')['default']
EditBook: typeof import('./src/components/article/EditBook.vue')['default']
EditSingleArticleModal: typeof import('./src/components/article/EditSingleArticleModal.vue')['default']
Empty: typeof import('./src/components/Empty.vue')['default']
Footer: typeof import('./src/components/word/Footer.vue')['default']
Form: typeof import('./src/components/base/form/Form.vue')['default']
FormItem: typeof import('./src/components/base/form/FormItem.vue')['default']
Github: typeof import('./src/components/ChannelIcons/Github.vue')['default']
GroupList: typeof import('./src/components/word/GroupList.vue')['default']
Header: typeof import('./src/components/Header.vue')['default']
IconBxVolume: typeof import('~icons/bx/volume')['default']
IconBxVolumeFull: typeof import('~icons/bx/volume-full')['default']
IconBxVolumeLow: typeof import('~icons/bx/volume-low')['default']
IconBxVolumeMute: typeof import('~icons/bx/volume-mute')['default']
IconClarityVolumeUpLine: typeof import('~icons/clarity/volume-up-line')['default']
IconEosIconsLoading: typeof import('~icons/eos-icons/loading')['default']
IconFluentAdd16Regular: typeof import('~icons/fluent/add16-regular')['default']
IconFluentAdd20Regular: typeof import('~icons/fluent/add20-regular')['default']
IconFluentAddSquare20Regular: typeof import('~icons/fluent/add-square20-regular')['default']
IconFluentAlignSpaceFitVertical20Regular: typeof import('~icons/fluent/align-space-fit-vertical20-regular')['default']
IconFluentArrowBounce20Regular: typeof import('~icons/fluent/arrow-bounce20-regular')['default']
IconFluentArrowCircleRight16Regular: typeof import('~icons/fluent/arrow-circle-right16-regular')['default']
IconFluentArrowClockwise20Regular: typeof import('~icons/fluent/arrow-clockwise20-regular')['default']
IconFluentArrowDownload20Regular: typeof import('~icons/fluent/arrow-download20-regular')['default']
IconFluentArrowLeft16Regular: typeof import('~icons/fluent/arrow-left16-regular')['default']
IconFluentArrowRepeatAll20Regular: typeof import('~icons/fluent/arrow-repeat-all20-regular')['default']
IconFluentArrowRight16Regular: typeof import('~icons/fluent/arrow-right16-regular')['default']
IconFluentArrowShuffle16Regular: typeof import('~icons/fluent/arrow-shuffle16-regular')['default']
IconFluentArrowSort20Regular: typeof import('~icons/fluent/arrow-sort20-regular')['default']
IconFluentArrowSwap20Regular: typeof import('~icons/fluent/arrow-swap20-regular')['default']
IconFluentBook20Regular: typeof import('~icons/fluent/book20-regular')['default']
IconFluentBookLetter20Regular: typeof import('~icons/fluent/book-letter20-regular')['default']
IconFluentBookNumber20Filled: typeof import('~icons/fluent/book-number20-filled')['default']
IconFluentCalendarDate20Regular: typeof import('~icons/fluent/calendar-date20-regular')['default']
IconFluentCheckmarkCircle16Filled: typeof import('~icons/fluent/checkmark-circle16-filled')['default']
IconFluentCheckmarkCircle16Regular: typeof import('~icons/fluent/checkmark-circle16-regular')['default']
IconFluentCheckmarkCircle20Filled: typeof import('~icons/fluent/checkmark-circle20-filled')['default']
IconFluentCheckmarkCircle20Regular: typeof import('~icons/fluent/checkmark-circle20-regular')['default']
IconFluentChevronDown20Filled: typeof import('~icons/fluent/chevron-down20-filled')['default']
IconFluentChevronDown20Regular: typeof import('~icons/fluent/chevron-down20-regular')['default']
IconFluentChevronLeft20Filled: typeof import('~icons/fluent/chevron-left20-filled')['default']
IconFluentChevronLeft28Filled: typeof import('~icons/fluent/chevron-left28-filled')['default']
IconFluentChevronUp20Filled: typeof import('~icons/fluent/chevron-up20-filled')['default']
IconFluentClock20Regular: typeof import('~icons/fluent/clock20-regular')['default']
IconFluentCommentEdit20Regular: typeof import('~icons/fluent/comment-edit20-regular')['default']
IconFluentCopy20Regular: typeof import('~icons/fluent/copy20-regular')['default']
IconFluentCrown20Regular: typeof import('~icons/fluent/crown20-regular')['default']
IconFluentDatabasePerson20Regular: typeof import('~icons/fluent/database-person20-regular')['default']
IconFluentDelete20Regular: typeof import('~icons/fluent/delete20-regular')['default']
IconFluentDismiss20Regular: typeof import('~icons/fluent/dismiss20-regular')['default']
IconFluentDismissCircle16Regular: typeof import('~icons/fluent/dismiss-circle16-regular')['default']
IconFluentDismissCircle20Filled: typeof import('~icons/fluent/dismiss-circle20-filled')['default']
IconFluentDocument20Regular: typeof import('~icons/fluent/document20-regular')['default']
IconFluentErrorCircle20Filled: typeof import('~icons/fluent/error-circle20-filled')['default']
IconFluentErrorCircle20Regular: typeof import('~icons/fluent/error-circle20-regular')['default']
IconFluentEye16Regular: typeof import('~icons/fluent/eye16-regular')['default']
IconFluentEyeOff16Regular: typeof import('~icons/fluent/eye-off16-regular')['default']
IconFluentHandWave20Regular: typeof import('~icons/fluent/hand-wave20-regular')['default']
IconFluentHome20Regular: typeof import('~icons/fluent/home20-regular')['default']
IconFluentKeyboardLayoutFloat20Regular: typeof import('~icons/fluent/keyboard-layout-float20-regular')['default']
IconFluentLockClosed20Regular: typeof import('~icons/fluent/lock-closed20-regular')['default']
IconFluentMail20Regular: typeof import('~icons/fluent/mail20-regular')['default']
IconFluentMyLocation20Regular: typeof import('~icons/fluent/my-location20-regular')['default']
IconFluentNumberSymbol20Regular: typeof import('~icons/fluent/number-symbol20-regular')['default']
IconFluentPaddingLeft20Regular: typeof import('~icons/fluent/padding-left20-regular')['default']
IconFluentPerson20Regular: typeof import('~icons/fluent/person20-regular')['default']
IconFluentPhone20Regular: typeof import('~icons/fluent/phone20-regular')['default']
IconFluentPlay20Regular: typeof import('~icons/fluent/play20-regular')['default']
IconFluentQuestionCircle20Regular: typeof import('~icons/fluent/question-circle20-regular')['default']
IconFluentReplay20Regular: typeof import('~icons/fluent/replay20-regular')['default']
IconFluentSearch20Regular: typeof import('~icons/fluent/search20-regular')['default']
IconFluentSearch24Regular: typeof import('~icons/fluent/search24-regular')['default']
IconFluentSettings20Regular: typeof import('~icons/fluent/settings20-regular')['default']
IconFluentShare20Regular: typeof import('~icons/fluent/share20-regular')['default']
IconFluentShieldQuestion20Regular: typeof import('~icons/fluent/shield-question20-regular')['default']
IconFluentSlideTextTitleEdit20Regular: typeof import('~icons/fluent/slide-text-title-edit20-regular')['default']
IconFluentSparkle20Regular: typeof import('~icons/fluent/sparkle20-regular')['default']
IconFluentSpeakerEdit20Regular: typeof import('~icons/fluent/speaker-edit20-regular')['default']
IconFluentStar16Filled: typeof import('~icons/fluent/star16-filled')['default']
IconFluentStar16Regular: typeof import('~icons/fluent/star16-regular')['default']
IconFluentStar20Filled: typeof import('~icons/fluent/star20-filled')['default']
IconFluentStarAdd16Regular: typeof import('~icons/fluent/star-add16-regular')['default']
IconFluentTarget20Regular: typeof import('~icons/fluent/target20-regular')['default']
IconFluentTextAlignLeft16Regular: typeof import('~icons/fluent/text-align-left16-regular')['default']
IconFluentTextBulletListSquare20Regular: typeof import('~icons/fluent/text-bullet-list-square20-regular')['default']
IconFluentTextEditStyle20Regular: typeof import('~icons/fluent/text-edit-style20-regular')['default']
IconFluentTextListAbcUppercaseLtr20Regular: typeof import('~icons/fluent/text-list-abc-uppercase-ltr20-regular')['default']
IconFluentTextPositionThrough20Regular: typeof import('~icons/fluent/text-position-through20-regular')['default']
IconFluentTextUnderlineDouble20Regular: typeof import('~icons/fluent/text-underline-double20-regular')['default']
IconFluentTranslate16Regular: typeof import('~icons/fluent/translate16-regular')['default']
IconFluentTranslateOff16Regular: typeof import('~icons/fluent/translate-off16-regular')['default']
IconFluentWeatherMoon16Regular: typeof import('~icons/fluent/weather-moon16-regular')['default']
IconFluentWeatherSunny16Regular: typeof import('~icons/fluent/weather-sunny16-regular')['default']
IconIconParkOutlineAddMusic: typeof import('~icons/icon-park-outline/add-music')['default']
IconIxWechatLogo: typeof import('~icons/ix/wechat-logo')['default']
IconMaterialSymbolsMail: typeof import('~icons/material-symbols/mail')['default']
IconMdiSparkles: typeof import('~icons/mdi/sparkles')['default']
IconPhExportLight: typeof import('~icons/ph/export-light')['default']
IconPhMicrosoftWordLogoLight: typeof import('~icons/ph/microsoft-word-logo-light')['default']
IconRiTwitterFill: typeof import('~icons/ri/twitter-fill')['default']
IconSimpleIconsGithub: typeof import('~icons/simple-icons/github')['default']
IconSimpleIconsWechat: typeof import('~icons/simple-icons/wechat')['default']
IconSimpleIconsXiaohongshu: typeof import('~icons/simple-icons/xiaohongshu')['default']
IconStreamlineColorPenDrawFlat: typeof import('~icons/streamline-color/pen-draw-flat')['default']
IconStreamlineDiscountPercentCoupon: typeof import('~icons/streamline/discount-percent-coupon')['default']
IconSystemUiconsImport: typeof import('~icons/system-uicons/import')['default']
IconUiwAlipay: typeof import('~icons/uiw/alipay')['default']
IconUiwQq: typeof import('~icons/uiw/qq')['default']
InputNumber: typeof import('./src/components/base/InputNumber.vue')['default']
List: typeof import('./src/components/list/List.vue')['default']
Log: typeof import('./src/components/setting/Log.vue')['default']
Logo: typeof import('./src/components/Logo.vue')['default']
MigrateDialog: typeof import('./src/components/MigrateDialog.vue')['default']
MiniDialog: typeof import('./src/components/dialog/MiniDialog.vue')['default']
Notice: typeof import('./src/components/user/Notice.vue')['default']
Option: typeof import('./src/components/base/select/Option.vue')['default']
OptionButton: typeof import('./src/components/base/OptionButton.vue')['default']
Pagination: typeof import('./src/components/base/Pagination.vue')['default']
Panel: typeof import('./src/components/Panel.vue')['default']
PopConfirm: typeof import('./src/components/PopConfirm.vue')['default']
PracticeLayout: typeof import('./src/components/PracticeLayout.vue')['default']
PracticeSettingDialog: typeof import('./src/components/word/PracticeSettingDialog.vue')['default']
PracticeWordListDialog: typeof import('./src/components/word/PracticeWordListDialog.vue')['default']
Progress: typeof import('./src/components/base/Progress.vue')['default']
QuestionForm: typeof import('./src/components/article/QuestionForm.vue')['default']
QuestionItem: typeof import('./src/components/article/QuestionItem.vue')['default']
Radio: typeof import('./src/components/base/radio/Radio.vue')['default']
RadioGroup: typeof import('./src/components/base/radio/RadioGroup.vue')['default']
ResourceCard: typeof import('./src/components/ResourceCard.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Select: typeof import('./src/components/base/select/Select.vue')['default']
SentenceHightLightWord: typeof import('./src/components/word/SentenceHightLightWord.vue')['default']
SettingDialog: typeof import('./src/components/setting/SettingDialog.vue')['default']
SettingItem: typeof import('./src/components/setting/SettingItem.vue')['default']
ShareIcon: typeof import('./src/components/ChannelIcons/ShareIcon.vue')['default']
ShufflePracticeSettingDialog: typeof import('./src/components/word/ShufflePracticeSettingDialog.vue')['default']
Slide: typeof import('./src/components/Slide.vue')['default']
SlideHorizontal: typeof import('./src/components/slide/SlideHorizontal.vue')['default']
SlideItem: typeof import('./src/components/slide/SlideItem.vue')['default']
Slider: typeof import('./src/components/base/Slider.vue')['default']
Space: typeof import('./src/components/article/Space.vue')['default']
StageProgress: typeof import('./src/components/StageProgress.vue')['default']
Statistics: typeof import('./src/components/word/Statistics.vue')['default']
Switch: typeof import('./src/components/base/Switch.vue')['default']
Textarea: typeof import('./src/components/base/Textarea.vue')['default']
Toast: typeof import('./src/components/base/toast/Toast.vue')['default']
ToastComponent: typeof import('./src/components/base/toast/ToastComponent.vue')['default']
Tooltip: typeof import('./src/components/base/Tooltip.vue')['default']
TypeWord: typeof import('./src/components/word/TypeWord.vue')['default']
TypingArticle: typeof import('./src/components/article/TypingArticle.vue')['default']
TypingWord: typeof import('./src/components/article/TypingWord.vue')['default']
VolumeIcon: typeof import('./src/components/icon/VolumeIcon.vue')['default']
VolumeSettingMiniDialog: typeof import('./src/components/word/VolumeSettingMiniDialog.vue')['default']
WeChat: typeof import('./src/components/ChannelIcons/WeChat.vue')['default']
WordItem: typeof import('./src/components/WordItem.vue')['default']
WordList: typeof import('./src/components/list/WordList.vue')['default']
WordSetting: typeof import('./src/components/setting/WordSetting.vue')['default']
}
}

35
docs/CONTRIBUTING.md Normal file
View File

@ -0,0 +1,35 @@
# 开源代码贡献准则
## PR 来源
- 我们会在 GitHub 上将需要完成的 feature 和修复的 bug 标记为 "Help Wanted"
- 用户在用户社群(在项目官方部署的 footer 中有二维码)提出的需求和 bug
- 根据大家对项目的理解和兴趣,认为需要做的 feature 和修复的 bug
## 在开始做 PR 之前
- 在 **开发者社群或 issue 区** 进行讨论,确认这个 PR 符合项目需求,并考虑对现有代码和未来计划的影响
在 GitHub 上创建相关的 issue已有 issue 则无须创建),并进行回复。尽可能在 issue 描述了问题、解决方案、相关细节和贡献者预期的工作,方便大家进行讨论
- 确认开始 issue 后,尽可能在一周内解决(相对复杂的 issue 除外),以免 PR 长期无法推进,其他开发者也无法参与
在 PR 过程中
- 在开始 coding 后,尽早提出一个 draft PR方便其他开发者参与讨论给出建议和帮助
遇到任何技术问题和实现路线问题,可以在 开发者社群或 issue 区 进行讨论。确保 PR 满足项目的开发规范和质量标准,经过充分测试和文档支持
- 在 PR 中友好协作,回应 Code Review。接受反馈并进行改进遵守代码风格和注释规范确保代码的可读性和可维护性
其他人也可以对 PR 进行 Review帮助发现代码中的问题提出自己的建议和想法
不需要担心自己的 pr 没有完整的覆盖掉所有 corner case 或者是否会与项目的其他功能产生冲突(对于 new contributor 来说,很难有时间完整理解项目所有代码和逻辑),只需要尽自己所能去实现,我们会在 review 的过程中一起讨论和完善。 我们都贡献自己擅长的部分就可以打造一个优秀 pr 和更好的 TypeWords!
## 完成 PR 后
- 标记相关 issue 为完成。确保代码被合并到主分支,并在生产环境中经过充分测试和部署
- 如果是用户社群的相关需求,可以在社群内对用户进行回复
## 行为准则
- 尊重所有贡献者,不论其技术水平、经验、性别、性取向、种族、宗教信仰或国籍
- 保持开放的心态,愿意接受其他人的批评和建议,并根据反馈进行改进
- 提交有价值的贡献,符合项目需求并遵守项目的开发规范和质量标准
- 不要在 PR 或讨论中使用不礼貌或侮辱性的语言,不要发布任何垃圾或攻击性的内容
- 遵守代码行为准则,不要进行不道德或不法的行为,如抄袭、篡改他人代码、恶意破坏等
- 避免进行灌水式的讨论或评论,尽量保持话题与项目相关,并尊重他人的意见和观点
- 遵守相关法律法规和 GitHub 平台的规定,不发布含有违法、政治或淫秽内容的 PR 或评论
## 反馈
如有任何问题或建议,请随时在 issue 区或者群内提出,我们将及时解决。同时,欢迎参与项目的讨论和开发,共同打造更加优秀的开源项目!

78
docs/README.en.md Normal file
View File

@ -0,0 +1,78 @@
<h1 align=center>
<img src="https://github.com/user-attachments/assets/9d626e0f-0601-4640-8981-ad66d8ac4853" alt="TypeWords" style="width: 500px;"/>
</h1>
<p align="center">
<a href="/docs/README.en.md">English</a> | <a href="/README.md">简体中文</a>
</p>
<div align="center">
Practice English, one strike, one step forward
</div>
<div align="center">
A website where one can memorize words and articles on web pages
</div>
<p align="center">
<a href="https://github.com/zyronon/type-word/blob/master/LICENSE"><img src="https://img.shields.io/github/license/zyronon/type-word" alt="License"></a>
<a><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg"/></a>
<a><img src="https://img.shields.io/badge/Powered%20by-Vue-blue"/></a>
<a href="https://hellogithub.com/repository/eb70616d65604458908fc1736e7d41fc" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=eb70616d65604458908fc1736e7d41fc&claim_uid=k5e4ZAqRjJEGzCW&theme=small" alt="FeaturedHelloGitHub" /></a>
</p>
<div align=center>
<a href="https://trendshift.io/repositories/14139" target="_blank" class="trendshift-badge"><img src="https://trendshift.io/api/badge/repositories/14139" alt="TypeWords | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</div>
<img width="1920" height="1440" alt="295shots_so" src="https://github.com/user-attachments/assets/383ed437-856e-48fe-92b0-9619babb49be" />
<img width="1920" height="1440" alt="922shots_so" src="https://github.com/user-attachments/assets/5b5fa13f-747c-4368-ae21-3c9d7d30fbc7" />
## Online visit
<https://typewords.cc>
## Feature list
### Memorize words
Automatically calculate the learning words based on the memory curve, and deepen memory through dictation; it provides
functions such as phonetic symbols, pronunciation (American, English), examples, phrases, synonyms, same root words,
etymology, error statistics, etc.
### Memorize the article
Built-in classic textbooks, practice and recite articles, enter sentence by sentence, and automatically pronounce it.
You can add and import articles by yourself, providing one-click translation and translation comparison functions
### Favorite, wrong word book, mastered
Incorrect input when learning words will be automatically added to the wrong word book for easier subsequent review. It
can also be added to mastered, and will automatically skip this word when you encounter it later. It can also be added
to your favorites to consolidate the review.
### Thesaurus
It has built-in commonly used CET-4, CET-6, GMAT, GRE, IELTS, SAT, TOEFL, postgraduate entrance examination English,
professional level 4 English, professional level 8 English and other thesis.
It meets the needs of most users for memorizing words as much as possible, and it is also very welcome to contribute
more vocabulary to the community.
## run
This project is based on`Vue`Developed, the node environment needs to be run.
1. Install NodeJS, refer to [Official Documentation](https://nodejs.org/en/download)
2. The project file is large, recommended`git clone --depth 1 https://github.com/zyronon/TypeWords.git` The command
clons only the last commit. Download directly
The Download ZIP function provided by Github cannot run
3. Open the command line, run it in the project root directory`npm install`Come download the dependency.
4. implement`npm start`To start the project, the default address of the project is[
`http://localhost:3000`](http://localhost:3000)
5. Open in a browser[`http://localhost:3000`](http://localhost:3000)Come to visit the project.
## Features and suggestions
The project is currently in the early stage of development and new functions are being added. If you have any functions
and suggestions for the software, please feel free to`Issues`Proposed in
If you also like the design ideas of this software, please submit it`pr`Thank you very much for your support!

183
index.html Normal file
View File

@ -0,0 +1,183 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"/>
<title>Type Words 官网 - 词文记 | 单词跟打 · 文章跟打</title>
<!-- 搜索引擎描述 -->
<meta name="description"
content="Type Words 官方网站 - 在线英语练习平台支持单词、文章跟打练习提升英语学习效率。Practice English, one strike, one step forward">
<!-- 关键词(可选,搜索引擎基本不用,但能补充信息) -->
<meta name="keywords"
content="Type Words, Typing Word, Type Words 官网, 官方网站, 英语打字练习, 单词跟打, 文章跟打, 键盘练习, 英语学习, 文章学习, 打字练习软件, 单词记忆工具, 英语学习软件, 背单词神器, 英语肌肉记忆, 键盘工作者, 免费英语学习, 音标发音, 默写练习, 在线学英语, CET-4, CET-6, TOEFL, IELTS, GRE, GMAT, SAT, 考研英语, 专四专八, 程序员英语, JavaScript API, Node.js API, Java API, Linux命令, 编程词汇, 技术英语, VSCode插件, 开源项目, GitHub趋势榜, V2EX热搜, Gitee GVP, 少数派推荐, 英语打字训练, WPM统计, 准确率分析, 商务英语, BEC, 雅思听力, 日语学习, 多语言学习, 英语口语练习, 单词拼写训练">
<meta name="author" content="zyronon"/>
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1"/>
<link rel="canonical" href="https://typewords.cc/"/>
<!-- Open Graph用于社交媒体分享微信/QQ/知乎/Facebook 等) -->
<meta property="og:title" content="Type Words 官网 - 英语打字练习平台">
<meta property="og:description"
content="Type Words 官方网站 - 在线英语练习平台支持单词、文章跟打练习提升英语学习效率。Practice English, one strike, one step forward">
<meta property="og:type" content="website">
<meta property="og:url" content="https://typewords.cc/">
<meta property="og:image" content="https://typewords.cc/favicon.png">
<!-- Twitter Card用于 Twitter 分享) -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Type Words 官网 - 英语打字练习平台">
<meta name="twitter:description"
content="Type Words 官方网站 - 在线英语练习平台支持单词、文章跟打练习提升英语学习效率。Practice English, one strike, one step forward">
<meta name="twitter:image" content="https://typewords.cc/favicon.png">
<link rel="icon" type="image/svg+xml" href="/favicon.png"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<!-- 苹果设备iOS Safari在用户添加到主屏时显示的图标-->
<link rel="apple-touch-icon" sizes="180x180" href="/favicon.png"/>
<!-- 设置浏览器地址栏颜色(在 Android Chrome 特别明显)。-->
<meta name="theme-color" content="#818CF8"/>
<link rel="manifest" href="/manifest.json">
<!-- 阻止 iOS 自动把数字识别为电话号码。-->
<!-- HandheldFriendly 和 MobileOptimized 是旧手机浏览器的优化提示(现在作用不大)。-->
<meta name="format-detection" content="telephone=no"/>
<meta name="HandheldFriendly" content="True"/>
<meta name="MobileOptimized" content="320"/>
<!-- referrer 控制请求来源信息-->
<meta name="referrer" content="origin-when-cross-origin"/>
<!-- color-scheme 告诉浏览器支持亮/暗模式-->
<meta name="color-scheme" content="light dark"/>
<script>
if (
!location.href.includes('localhost')
&& !location.href.includes('192.168')
&& !location.href.includes('172.16')
&& !location.href.includes('10.0')
) {
//51.la
(function () {
window.LA = window.LA || {
ids: [{id: "3OH8ITYRgwzo58L2", ck: "3OH8ITYRgwzo58L2"}],
id: "3OH8ITYRgwzo58L2",
ck: "3OH8ITYRgwzo58L2",
hashMode: true
};
const script = document.createElement('script');
script.src = `https://typewords.cc/libs/51.js`;
document.head.appendChild(script);
})();
// Cloudflare
(function () {
var cf = document.createElement("script");
cf.src = 'https://static.cloudflareinsights.com/beacon.min.js'
cf.setAttribute("data-cf-beacon", '{"token": "e5119992696d4155814400dd69781d68"}');
document.head.appendChild(cf);
})();
// google
(function () {
var ana = document.createElement("script");
ana.src = 'https://www.googletagmanager.com/gtag/js?id=G-50T6DRD837'
ana.onload = function () {
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-50T6DRD837');
}
document.head.appendChild(ana);
})();
// baidu
var _hmt = _hmt || [];
(function () {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?3dae52fcd5375a19905462e4ad3eb54e";
document.head.appendChild(hm);
})();
// umami
(function () {
var umami = document.createElement("script");
umami.src = 'https://typewords.cc/libs/s.js'
umami.setAttribute("data-website-id", "160308c9-7900-4b1d-a0b1-c3b25a9530f6");
document.head.appendChild(umami);
})();
// umami-saas
// (function () {
// var umami2 = document.createElement("script");
// umami2.src = 'https://stat.typewords.cc/script.js'
// umami2.setAttribute("data-website-id", "4d728ae3-5393-4efe-81dc-30dcb4f33c00");
// document.head.appendChild(umami2);
// })();
}
</script>
</head>
<body>
<noscript>
<div>You need to enable JavaScript to run Type Words.</div>
<div>你需要启用 JavaScript 来运行 Type Words.</div>
</noscript>
<div id="app"></div>
<script>
(function () {
var ua = navigator.userAgent || ''
var isIE = !!document.documentMode || /MSIE|Trident/i.test(ua)
if (!isIE) return
var style = document.createElement('style')
style.type = 'text/css'
style.appendChild(document.createTextNode(
'.ie-mask{position:fixed;left:0;top:0;right:0;bottom:0;background:rgba(0,0,0,.35);z-index:9998}' +
'.ie-dialog{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);width:28rem;max-width:90vw;background:#fff;color:#111;border-radius:.6rem;box-shadow:0 10px 30px rgba(0,0,0,.15);z-index:9999;padding:1.2rem}' +
'.ie-dialog .title{font-size:1.2rem;font-weight:700;margin-bottom:.6rem}' +
'.ie-dialog .desc{font-size:.95rem;line-height:1.6;color:#555}' +
'.ie-dialog .actions{display:flex;justify-content:flex-end;margin-top:1rem}' +
'.ie-dialog .actions > * + *{margin-left:.6rem}' +
'.ie-dialog .btn{display:inline-flex;align-items:center;justify-content:center;height:2.2rem;padding:0 1rem;border-radius:.4rem;background:#0C8CE9;color:#fff;text-decoration:none}' +
'.ie-dialog .btn-secondary{display:inline-flex;align-items:center;justify-content:center;height:2.2rem;padding:0 .9rem;border-radius:.4rem;background:#eee;color:#333;border:1px solid #ddd}' +
'@media (prefers-color-scheme: dark){.ie-dialog{background:#1e1f22;color:#e6e6e6}.ie-dialog .desc{color:#c6c6c6}.ie-dialog .btn-secondary{background:#2a2b2f;color:#e6e6e6;border-color:#3a3b3f}}'
))
document.head.appendChild(style)
var mask = document.createElement('div')
mask.className = 'ie-mask'
var dialog = document.createElement('div')
dialog.className = 'ie-dialog'
dialog.innerHTML = '<div class="title">不支持 IE 浏览器</div>' +
'<div class="desc">Type Words 使用现代技术构建,请使用 Chrome、Edge、Firefox 或 Safari 等现代浏览器访问。</div>' +
'<div class="actions">' +
'<a class="btn" href="https://www.google.cn/chrome/" target="_blank" rel="noreferrer">下载 Chrome</a>' +
'<button class="btn-secondary" type="button">我知道了</button>' +
'</div>'
function close() {
try {
document.body.removeChild(mask)
} catch (e) {
}
try {
document.body.removeChild(dialog)
} catch (e) {
}
}
mask.addEventListener('click', close)
var btn = null
try {
btn = dialog.querySelector('.btn-secondary')
} catch (e) {
}
if (btn) btn.addEventListener('click', close)
document.body.appendChild(mask)
document.body.appendChild(dialog)
})()
</script>
<script type="module" src="/src/main"></script>
</body>
</html>

3296
js_node/dict.cjs Normal file

File diff suppressed because it is too large Load Diff

19
js_node/package.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "node",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"dayjs": "^1.11.13",
"jquery": "^3.7.1",
"jsdom": "^23.0.1",
"p-limit": "^6.2.0",
"playwright": "^1.54.1"
}
}

631
js_node/pnpm-lock.yaml Normal file
View File

@ -0,0 +1,631 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
dayjs:
specifier: ^1.11.13
version: 1.11.13
jquery:
specifier: ^3.7.1
version: 3.7.1
jsdom:
specifier: ^23.0.1
version: 23.2.0
p-limit:
specifier: ^6.2.0
version: 6.2.0
playwright:
specifier: ^1.54.1
version: 1.54.1
packages:
'@asamuzakjp/css-color@3.2.0':
resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==}
'@asamuzakjp/dom-selector@2.0.2':
resolution: {integrity: sha512-x1KXOatwofR6ZAYzXRBL5wrdV0vwNxlTCK9NCuLqAzQYARqGcvFwiJA6A1ERuh+dgeA4Dxm3JBYictIes+SqUQ==}
'@csstools/color-helpers@5.0.2':
resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==}
engines: {node: '>=18'}
'@csstools/css-calc@2.1.4':
resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==}
engines: {node: '>=18'}
peerDependencies:
'@csstools/css-parser-algorithms': ^3.0.5
'@csstools/css-tokenizer': ^3.0.4
'@csstools/css-color-parser@3.0.10':
resolution: {integrity: sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==}
engines: {node: '>=18'}
peerDependencies:
'@csstools/css-parser-algorithms': ^3.0.5
'@csstools/css-tokenizer': ^3.0.4
'@csstools/css-parser-algorithms@3.0.5':
resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==}
engines: {node: '>=18'}
peerDependencies:
'@csstools/css-tokenizer': ^3.0.4
'@csstools/css-tokenizer@3.0.4':
resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
engines: {node: '>=18'}
agent-base@7.1.4:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
bidi-js@1.0.3:
resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
css-tree@2.3.1:
resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
cssstyle@4.6.0:
resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==}
engines: {node: '>=18'}
data-urls@5.0.0:
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
engines: {node: '>=18'}
dayjs@1.11.13:
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
debug@4.4.1:
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
decimal.js@10.6.0:
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
entities@6.0.1:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'}
es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'}
es-errors@1.3.0:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
es-object-atoms@1.1.1:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
es-set-tostringtag@2.1.0:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
form-data@4.0.3:
resolution: {integrity: sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==}
engines: {node: '>= 6'}
fsevents@2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
get-proto@1.0.1:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'}
has-tostringtag@1.0.2:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
html-encoding-sniffer@4.0.0:
resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
engines: {node: '>=18'}
http-proxy-agent@7.0.2:
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
engines: {node: '>= 14'}
https-proxy-agent@7.0.6:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'}
iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
is-potential-custom-element-name@1.0.1:
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
jquery@3.7.1:
resolution: {integrity: sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==}
jsdom@23.2.0:
resolution: {integrity: sha512-L88oL7D/8ufIES+Zjz7v0aes+oBMh2Xnh3ygWvL0OaICOomKEPKuPnIfBJekiXr+BHbbMjrWn/xqrDQuxFTeyA==}
engines: {node: '>=18'}
peerDependencies:
canvas: ^2.11.2
peerDependenciesMeta:
canvas:
optional: true
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
mdn-data@2.0.30:
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
p-limit@6.2.0:
resolution: {integrity: sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==}
engines: {node: '>=18'}
parse5@7.3.0:
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
playwright-core@1.54.1:
resolution: {integrity: sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==}
engines: {node: '>=18'}
hasBin: true
playwright@1.54.1:
resolution: {integrity: sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==}
engines: {node: '>=18'}
hasBin: true
psl@1.15.0:
resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==}
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
querystringify@2.2.0:
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
requires-port@1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
rrweb-cssom@0.6.0:
resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==}
rrweb-cssom@0.8.0:
resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==}
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
saxes@6.0.0:
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
engines: {node: '>=v12.22.7'}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
tough-cookie@4.1.4:
resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==}
engines: {node: '>=6'}
tr46@5.1.1:
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
engines: {node: '>=18'}
universalify@0.2.0:
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
engines: {node: '>= 4.0.0'}
url-parse@1.5.10:
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
w3c-xmlserializer@5.0.0:
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
engines: {node: '>=18'}
webidl-conversions@7.0.0:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'}
whatwg-encoding@3.1.1:
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
engines: {node: '>=18'}
whatwg-mimetype@4.0.0:
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
engines: {node: '>=18'}
whatwg-url@14.2.0:
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
engines: {node: '>=18'}
ws@8.18.3:
resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
xml-name-validator@5.0.0:
resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
engines: {node: '>=18'}
xmlchars@2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
yocto-queue@1.2.1:
resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==}
engines: {node: '>=12.20'}
snapshots:
'@asamuzakjp/css-color@3.2.0':
dependencies:
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
lru-cache: 10.4.3
'@asamuzakjp/dom-selector@2.0.2':
dependencies:
bidi-js: 1.0.3
css-tree: 2.3.1
is-potential-custom-element-name: 1.0.1
'@csstools/color-helpers@5.0.2': {}
'@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
dependencies:
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/css-color-parser@3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
dependencies:
'@csstools/color-helpers': 5.0.2
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)':
dependencies:
'@csstools/css-tokenizer': 3.0.4
'@csstools/css-tokenizer@3.0.4': {}
agent-base@7.1.4: {}
asynckit@0.4.0: {}
bidi-js@1.0.3:
dependencies:
require-from-string: 2.0.2
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
function-bind: 1.1.2
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
css-tree@2.3.1:
dependencies:
mdn-data: 2.0.30
source-map-js: 1.2.1
cssstyle@4.6.0:
dependencies:
'@asamuzakjp/css-color': 3.2.0
rrweb-cssom: 0.8.0
data-urls@5.0.0:
dependencies:
whatwg-mimetype: 4.0.0
whatwg-url: 14.2.0
dayjs@1.11.13: {}
debug@4.4.1:
dependencies:
ms: 2.1.3
decimal.js@10.6.0: {}
delayed-stream@1.0.0: {}
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
es-errors: 1.3.0
gopd: 1.2.0
entities@6.0.1: {}
es-define-property@1.0.1: {}
es-errors@1.3.0: {}
es-object-atoms@1.1.1:
dependencies:
es-errors: 1.3.0
es-set-tostringtag@2.1.0:
dependencies:
es-errors: 1.3.0
get-intrinsic: 1.3.0
has-tostringtag: 1.0.2
hasown: 2.0.2
form-data@4.0.3:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
es-set-tostringtag: 2.1.0
hasown: 2.0.2
mime-types: 2.1.35
fsevents@2.3.2:
optional: true
function-bind@1.1.2: {}
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
es-define-property: 1.0.1
es-errors: 1.3.0
es-object-atoms: 1.1.1
function-bind: 1.1.2
get-proto: 1.0.1
gopd: 1.2.0
has-symbols: 1.1.0
hasown: 2.0.2
math-intrinsics: 1.1.0
get-proto@1.0.1:
dependencies:
dunder-proto: 1.0.1
es-object-atoms: 1.1.1
gopd@1.2.0: {}
has-symbols@1.1.0: {}
has-tostringtag@1.0.2:
dependencies:
has-symbols: 1.1.0
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
html-encoding-sniffer@4.0.0:
dependencies:
whatwg-encoding: 3.1.1
http-proxy-agent@7.0.2:
dependencies:
agent-base: 7.1.4
debug: 4.4.1
transitivePeerDependencies:
- supports-color
https-proxy-agent@7.0.6:
dependencies:
agent-base: 7.1.4
debug: 4.4.1
transitivePeerDependencies:
- supports-color
iconv-lite@0.6.3:
dependencies:
safer-buffer: 2.1.2
is-potential-custom-element-name@1.0.1: {}
jquery@3.7.1: {}
jsdom@23.2.0:
dependencies:
'@asamuzakjp/dom-selector': 2.0.2
cssstyle: 4.6.0
data-urls: 5.0.0
decimal.js: 10.6.0
form-data: 4.0.3
html-encoding-sniffer: 4.0.0
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
is-potential-custom-element-name: 1.0.1
parse5: 7.3.0
rrweb-cssom: 0.6.0
saxes: 6.0.0
symbol-tree: 3.2.4
tough-cookie: 4.1.4
w3c-xmlserializer: 5.0.0
webidl-conversions: 7.0.0
whatwg-encoding: 3.1.1
whatwg-mimetype: 4.0.0
whatwg-url: 14.2.0
ws: 8.18.3
xml-name-validator: 5.0.0
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
lru-cache@10.4.3: {}
math-intrinsics@1.1.0: {}
mdn-data@2.0.30: {}
mime-db@1.52.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
ms@2.1.3: {}
p-limit@6.2.0:
dependencies:
yocto-queue: 1.2.1
parse5@7.3.0:
dependencies:
entities: 6.0.1
playwright-core@1.54.1: {}
playwright@1.54.1:
dependencies:
playwright-core: 1.54.1
optionalDependencies:
fsevents: 2.3.2
psl@1.15.0:
dependencies:
punycode: 2.3.1
punycode@2.3.1: {}
querystringify@2.2.0: {}
require-from-string@2.0.2: {}
requires-port@1.0.0: {}
rrweb-cssom@0.6.0: {}
rrweb-cssom@0.8.0: {}
safer-buffer@2.1.2: {}
saxes@6.0.0:
dependencies:
xmlchars: 2.2.0
source-map-js@1.2.1: {}
symbol-tree@3.2.4: {}
tough-cookie@4.1.4:
dependencies:
psl: 1.15.0
punycode: 2.3.1
universalify: 0.2.0
url-parse: 1.5.10
tr46@5.1.1:
dependencies:
punycode: 2.3.1
universalify@0.2.0: {}
url-parse@1.5.10:
dependencies:
querystringify: 2.2.0
requires-port: 1.0.0
w3c-xmlserializer@5.0.0:
dependencies:
xml-name-validator: 5.0.0
webidl-conversions@7.0.0: {}
whatwg-encoding@3.1.1:
dependencies:
iconv-lite: 0.6.3
whatwg-mimetype@4.0.0: {}
whatwg-url@14.2.0:
dependencies:
tr46: 5.1.1
webidl-conversions: 7.0.0
ws@8.18.3: {}
xml-name-validator@5.0.0: {}
xmlchars@2.2.0: {}
yocto-queue@1.2.1: {}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

119
js_node/test.js Normal file
View File

@ -0,0 +1,119 @@
import fs from 'fs';
import path from 'path';
import {fileURLToPath} from 'url';
import pLimit from 'p-limit';
import {spawn} from 'child_process';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const defaultConcurrency = Number(process.env.CONCURRENCY || 8);
const maxRetries = Number(process.env.RETRIES || 2);
const endpoint = process.env.ENDPOINT || 'http://localhost/v1/words/addWord';
const inputFile = path.resolve(__dirname, 'save', 'all-all2.json');
const resultDir = path.resolve(__dirname, 'result');
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function readWords(filePath) {
const raw = await fs.promises.readFile(filePath, 'utf8');
const data = JSON.parse(raw);
if (!Array.isArray(data)) {
throw new Error('输入文件不是数组');
}
return data;
}
async function postWithRetry(word, attempt = 0) {
try {
const res = await fetch(endpoint, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(word)
});
const text = await res.text().catch(() => '');
let d = JSON.parse(text);
if (!res.ok || !d['success']) {
throw new Error(`HTTP ${res.status} ${res.statusText} ${text}`);
}
return await res.json().catch(() => ({}));
} catch (err) {
if (attempt < maxRetries) {
const backoffMs = 500 * Math.pow(2, attempt);
await sleep(backoffMs);
return postWithRetry(word, attempt + 1);
}
throw err;
}
}
async function ensureDir(dir) {
await fs.promises.mkdir(dir, {recursive: true});
}
async function main() {
console.log(`[start] 读取: ${inputFile}`);
const words = await readWords(inputFile);
console.log(`[info] 单词总数: ${words.length}`);
const limit = pLimit(defaultConcurrency);
let successCount = 0;
let failCount = 0;
const failures = [];
let copy = words.filter(v => v.word === 'canal')
const toCopy = JSON.stringify(copy);
console.log(toCopy)
return
const tasks = words.map((word, index) =>
limit(async () => {
try {
if (["”", "“"].includes(word.word[0])) word.word = word.word.slice(1)
if (["”", "“"].includes(word.word[word.word.length - 1])) word.word = word.word.slice(0, word.word.length - 1);
word.word = word.word.replaceAll('”', '\"')
word.word = word.word.replaceAll('“', '\"')
word.word = word.word.replaceAll('', '(')
word.word = word.word.replaceAll('', ')')
console.log(word.word);
const result = await postWithRetry(word);
successCount++;
if ((successCount + failCount) % 100 === 0) {
console.log(`[progress] 已处理 ${successCount + failCount}/${words.length} (成功: ${successCount}, 失败: ${failCount})`);
}
return {index, word, ok: true, result};
} catch (error) {
failCount++;
failures.push({index, word: word.word, error: String(error)});
if ((successCount + failCount) % 100 === 0) {
console.log(`[progress] 已处理 ${successCount + failCount}/${words.length} (成功: ${successCount}, 失败: ${failCount})`);
}
return {index, word, ok: false, error: String(error)};
}
})
);
await Promise.all(tasks);
console.log(`[done] 完成。成功: ${successCount}, 失败: ${failCount}`);
if (failures.length > 0) {
await ensureDir(resultDir);
const ts = new Date().toISOString().replace(/[:.]/g, '-');
const failFile = path.resolve(resultDir, `failed-words-${ts}.json`);
await fs.promises.writeFile(failFile, JSON.stringify(failures, null, 2), 'utf8');
console.log(`[save] 失败明细保存至: ${failFile}`);
}
}
main().catch(err => {
console.error('[error] 程序异常:', err);
process.exitCode = 1;
});

15
js_node/压缩文件.cjs Normal file
View File

@ -0,0 +1,15 @@
let path = require("path");
let fs = require("fs");
const RESULT_DIR = path.join(__dirname, 'result');
const RESULT2_DIR = path.join(__dirname, 'result2');
(async () => {
const files = fs.readdirSync(RESULT_DIR).filter(f => f.endsWith('.json'));
for (const file of files) {
const filePath = path.join(RESULT_DIR, file);
const raw = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
fs.writeFileSync(path.join(RESULT2_DIR, file), JSON.stringify(raw), 'utf-8');
}
})();

176
js_node/爬虫.js Normal file
View File

@ -0,0 +1,176 @@
import fs from 'fs';
import path from 'path';
import {chromium} from 'playwright';
import pLimit from 'p-limit';
import {fileURLToPath} from 'url';
import dayjs from 'dayjs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// 路径设置
const normalList_FILE = path.join(__dirname, 'save', 'normalList.json');
const unnormalList_FILE = path.join(__dirname, 'save', 'unnormalList.json');
// 控制参数
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const MAX_COUNT = 999999999999;
// 爬虫主函数
async function crawlWord(val, page,) {
let word = val.word
const data = val
const url = `https://www.youdao.com/result?word=${encodeURIComponent(word)}&lang=en`;
console.log(url)
try {
await page.goto(url, {waitUntil: 'networkidle', timeout: 15000});
// const titleEl = await page.locator('.title').first();
// data.word = await titleEl.evaluate(el => el.firstChild?.nodeValue || '');
const phones = await page.$$('.per-phone .phonetic');
if (phones[0]) data.phonetic0 = (await phones[0].textContent())?.trim() || '';
if (phones[1]) data.phonetic1 = (await phones[1].textContent())?.trim() || '';
data.phonetic0 = data.phonetic0.replaceAll('/', '').trim()
data.phonetic1 = data.phonetic1.replaceAll('/', '').trim()
for (const el of await page.$$('.basic .word-exp')) {
const pos = await el.$('.pos');
const tran = await el.$('.trans');
data.trans.push({
pos: pos ? (await pos.textContent())?.trim() : '',
cn: tran ? (await tran.textContent())?.trim() : '',
});
}
if (await page.locator('div:has-text("双语例句")').count() > 0) {
for (const el of await page.$$('.blng_sents_part .trans-container ul li .col2')) {
const en = await el.$('.sen-eng');
const ch = await el.$('.sen-ch');
data.sentences.push({
c: en ? (await en.textContent())?.trim() : '',
cn: ch ? (await ch.textContent())?.trim() : '',
});
}
}
if (await page.locator('div:has-text("词典短语")').count() > 0) {
for (const el of await page.$$('.phrs ul li .phrs-content')) {
const point = await el.$('.point');
const tran = await el.$('.phr_trans');
data.phrases.push({
c: point ? (await point.textContent())?.trim() : '',
cn: tran ? (await tran.textContent())?.trim() : '',
});
}
}
try {
if (await page.locator('div:has-text("同近义词")').count() > 0) {
await page.getByText('同近义词', {timeout: 2000}).click();
await page.waitForSelector('.syno', {timeout: 3000});
for (const el of await page.$$('.syno-item')) {
const pos = await el.$('.index');
const tran = await el.$('.synptran');
const wordEl = await el.$('.clickable');
let str = wordEl ? (await wordEl.textContent())?.trim() : '';
data.synos.push({
pos: pos ? (await pos.textContent())?.trim() : '',
cn: tran ? (await tran.textContent())?.trim() : '',
ws: str.split('/').map(s => s.trim()).filter(Boolean),
});
}
}
} catch {
}
try {
if (await page.locator('div:has-text("同根词")').count() > 0) {
await page.getByText('同根词', {timeout: 2000}).click();
await page.waitForSelector('.rel_word', {timeout: 3000});
const cigen = await page.$('.trans-container > p .point');
data.relWords.root = cigen ? (await cigen.textContent())?.trim() : '';
for (const el of await page.$$('.rel_word_item')) {
let item = {pos: '', words: []};
const pos = await el.$('.pos');
item.pos = pos ? (await pos.textContent())?.trim() : '';
for (const el2 of await el.$$('.rel_content p')) {
const word = await el2.$('.point');
let wordStr = word ? (await word.textContent())?.trim() : '';
let str = el2 ? (await el2.textContent())?.trim() : '';
str = str.replace(wordStr, '');
item.words.push({c: wordStr, cn: str});
}
data.relWords.rels.push(item);
}
}
} catch {
}
try {
if (await page.locator('div:has-text("词源")').count() > 0) {
await page.getByText('词源', {timeout: 2000}).click();
await page.waitForSelector('.etymology', {timeout: 3000});
for (const el of await page.$$('.trans-cell')) {
const header = await el.$('.header');
const zh_result = await el.$('.zh_result');
data.etymology.push({
t: header ? (await header.textContent())?.trim() : '',
d: zh_result ? (await zh_result.textContent())?.trim() : '',
});
}
}
} catch {
}
return data;
} catch (err) {
console.log(err)
console.log(`🔁 ${word} 抓取失败...`);
return data;
}
}
(async () => {
const browser = await chromium.launch({headless: true});
const page = await browser.newPage()
async function start(file) {
const raw = JSON.parse(fs.readFileSync(file, 'utf-8'));
let removeList = raw.slice()
const resultMap = new Map();
let newFileName = file.replaceAll('.json', '-fetch.json')
try {
const newRaw = JSON.parse(fs.readFileSync(newFileName, 'utf-8'));
console.log('已保存:', newRaw.length);
newRaw.map(word => {
resultMap.set(word.word, word);
})
} catch (e) {
}
for (let i = 0; i < raw.length; i++) {
let word = raw[i];
console.log(`爬取:${file}${word.word},进度:${i} / ${raw.length};时间:${dayjs().format('YYYY-MM-DD HH:mm:ss')}`)
const result = await crawlWord(word, page);
if (result) {
resultMap.set(word.word, result);
fs.writeFileSync(file.replaceAll('.json', '-fetch.json'), JSON.stringify(Array.from(resultMap.values()), null, 2), 'utf-8');
removeList.shift()
fs.writeFileSync(file, JSON.stringify(removeList, null, 2), 'utf-8');
}
await sleep(300);
}
}
await start(unnormalList_FILE)
await start(normalList_FILE)
await browser.close();
console.log('\n🎉 所有任务完成!');
})();

79
js_node/转换文件.cjs Normal file
View File

@ -0,0 +1,79 @@
let path = require("path");
let fs = require("fs");
const RESULT_DIR = path.join(__dirname, 'source');
const RESULT2_DIR = path.join(__dirname, 'result');
function getDefaultWord(val) {
return {
custom: false,
id: 0,
"word": "",
"phonetic0": "",
"phonetic1": "",
"trans": [],
"sentences": [],
"phrases": [],
"synos": [],
"relWords": {
"root": "",
"rels": []
},
"etymology": [],
...val
}
}
const safeString = (str) => (typeof str === 'string' ? str.trim() : '');
const safeSplit = (str, sep) =>
safeString(str) ? safeString(str).split(sep).filter(Boolean) : [];
(async () => {
const files = fs.readdirSync(RESULT_DIR).filter(f => f.endsWith('.json'));
for (const file of files) {
const filePath = path.join(RESULT_DIR, file);
const raw = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
console.log(raw.length)
let list = raw.map((v, i) => {
const trans = v.trans.map(line => {
const regex = /(n|vt|vi|adj)\.\s*([^vna]+?)(?=\s*(n\.|vt\.|vi\.|adj\.|$))/g;
let result = [];
let match1;
while ((match1 = regex.exec(line)) !== null) {
result.push(match1[1] + ". " + match1[2].trim());
}
if (!result.length) {
return [line]
}
return result;
}).flat().map(line => {
const match = line.match(/^([^\s.]+\.?)\s*(.*)$/);
if (match) {
let pos = safeString(match[1]);
let cn = safeString(match[2]);
// 如果 pos 不是常规词性(不以字母开头),例如 "【名】"
if (!/^[a-zA-Z]+\.?$/.test(pos)) {
cn = safeString(line); // 整行放到 cn
pos = ''; // pos 置空
}
return {pos, cn};
}
return {pos: '', cn: safeString(line)};
});
return getDefaultWord({
word: v.name,
phonetic0: v.usphone || '',
phonetic1: v.ukphone || '',
trans,
})
})
fs.writeFileSync(path.join(RESULT2_DIR, file), JSON.stringify(list, null, 2), 'utf-8');
}
})();

22298
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

94
package.json Normal file
View File

@ -0,0 +1,94 @@
{
"name": "vite-project",
"private": true,
"version": "0.0.0",
"scripts": {
"start": "vite",
"dev": "vite",
"test": "",
"build": "vite build",
"build-oss": "vite build && node scripts/do.js",
"build-tsc": "vue-tsc && vite build",
"report": "vite build",
"report-oss": "vite build",
"preview": "vite preview",
"commit": "git-cz",
"prepare": "husky install",
"i18n:write": "gulp i18nwrite",
"deploy-oss": "node scripts/deploy-oss.js"
},
"dependencies": {
"@floating-ui/dom": "^1.7.4",
"@imengyu/vue3-context-menu": "^1.5.1",
"@vueuse/core": "14.0.0-alpha.0",
"axios": "^1.12.0",
"compromise": "^14.14.4",
"copy-to-clipboard": "^3.3.3",
"dayjs": "^1.11.13",
"file-saver": "^2.0.5",
"idb-keyval": "^6.2.2",
"md5": "^2.2.1",
"mitt": "^3.0.1",
"nanoid": "^5.1.5",
"pinia": "^3.0.3",
"string-comparison": "^1.3.0",
"vue": "^3.5.17",
"vue-router": "^4.5.1",
"vue-virtual-scroller": "2.0.0-beta.8"
},
"devDependencies": {
"@alicloud/pop-core": "^1.8.0",
"@iconify-json/bx": "^1.2.2",
"@iconify-json/clarity": "^1.2.4",
"@iconify-json/dinkie-icons": "^1.2.0",
"@iconify-json/eos-icons": "^1.2.4",
"@iconify-json/fluent": "^1.2.28",
"@iconify-json/icon-park-outline": "^1.2.4",
"@iconify-json/icon-park-solid": "^1.2.4",
"@iconify-json/ix": "^1.2.10",
"@iconify-json/material-symbols": "^1.2.33",
"@iconify-json/mdi": "^1.2.3",
"@iconify-json/oui": "^1.2.6",
"@iconify-json/ph": "^1.2.2",
"@iconify-json/qlementine-icons": "^1.2.11",
"@iconify-json/ri": "^1.2.5",
"@iconify-json/simple-icons": "^1.2.48",
"@iconify-json/streamline": "^1.2.5",
"@iconify-json/streamline-color": "^1.2.2",
"@iconify-json/system-uicons": "^1.2.4",
"@iconify-json/uiw": "^1.2.3",
"@types/file-saver": "^2.0.7",
"@types/lodash-es": "^4.17.12",
"@types/md5": "^2.1.33",
"@unocss/postcss": "^66.4.0",
"@vitejs/plugin-vue": "^6.0.0",
"@vitejs/plugin-vue-jsx": "^5.0.1",
"@vue/compiler-sfc": "^3.5.17",
"ali-oss": "^6.23.0",
"commitizen": "^4.3.1",
"cz-conventional-changelog": "^3.3.0",
"esm": "^3.2.25",
"git-last-commit": "^1.0.1",
"gulp": "^4.0.2",
"husky": "^8.0.3",
"prettier": "^3.7.4",
"rollup-plugin-visualizer": "^5.14.0",
"sass": "^1.89.2",
"sitemap": "^8.0.0",
"tslib": "^2.8.1",
"typescript": "^5.8.3",
"unocss": "^66.4.0",
"unplugin-icons": "^22.2.0",
"unplugin-vue-components": "^29.0.0",
"unplugin-vue-macros": "^2.14.5",
"vite": "^7.0.3",
"vite-plugin-externals": "^0.6.2",
"vue-tsc": "^3.0.1",
"xlsx": "^0.18.5"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}
}

7957
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

8
postcss.config.mjs Normal file
View File

@ -0,0 +1,8 @@
// postcss.config.mjs
import UnoCSS from '@unocss/postcss'
export default {
plugins: [
UnoCSS(),
],
}

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
public/imgs/0.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/imgs/1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
public/imgs/2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
public/imgs/3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
public/imgs/4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/imgs/5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
public/imgs/6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
public/imgs/7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
public/imgs/8.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/imgs/9.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

BIN
public/imgs/qr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 B

61
public/list/article.json Normal file
View File

@ -0,0 +1,61 @@
[
{
"id": "article_nce1",
"name": "新概念英语1",
"description": "第一册《英语初阶》讲练基本语音、语调、语法、词法、句法及句型结构,是练好英语基本功的关键。",
"category": "文章学习",
"tags": [
"新概念英语"
],
"url": "NCE_1.json",
"length": 72,
"translateLanguage": "common",
"language": "en",
"cover": "/imgs/covers/nce-1.png"
},
{
"id": "article_nce2",
"name": "新概念英语2",
"description": "第二册《实践与进步》由浅入深通过96篇幽默短文的学习学习者将能在听、说、读、写四个维度全面实践英语培养英文语感、纠正发音和培养自然语调掌握归纳总结、造句及书写简单私人信件的能力。",
"category": "文章学习",
"tags": [
"新概念英语"
],
"url": "NCE_2.json",
"length": 96,
"translateLanguage": "common",
"language": "en",
"update": true,
"cover": "/imgs/covers/nce-2.png"
},
{
"id": "article_nce3",
"name": "新概念英语3",
"description": "从第三册《培养技能》开始,文章更加注重分析句子之间内在的逻辑关系,并进一步扩充讲解词汇、语法及句型的实战运用。通过这一册的学习,学员们听、说、读、写的能力有一个新的飞跃,能够熟练地表达和使用英语。",
"category": "文章学习",
"tags": [
"新概念英语"
],
"url": "NCE_3.json",
"length": 60,
"translateLanguage": "common",
"language": "en",
"update": true,
"cover": "/imgs/covers/nce-3.png"
},
{
"id": "article_nce4",
"name": "新概念英语4",
"description": "第四册《流利英语》,涵盖了文化、经济、哲学、艺术、体育、政治等三十多个学科门类,语言文字精美独到,句型结构复杂多变而又地道酣畅,适用于有志于进一步深入学习和掌握英语到游刃有余的境界的同学。学完四册的同学可以轻松阅读英文报刊与文学作品 ,加以适当实战,语言能力足以胜任欧美的生活和工作。",
"category": "文章学习",
"tags": [
"新概念英语"
],
"url": "NCE_4.json",
"length": 48,
"translateLanguage": "common",
"language": "en",
"update": true,
"cover": "/imgs/covers/nce-4.png"
}
]

View File

@ -0,0 +1,61 @@
[
{
"id": "article_nce1",
"name": "新概念英语1",
"description": "第一册《英语初阶》讲练基本语音、语调、语法、词法、句法及句型结构,是练好英语基本功的关键。",
"category": "文章学习",
"tags": [
"新概念英语"
],
"url": "NCE_1.json",
"length": 72,
"translateLanguage": "common",
"language": "en",
"cover": "/imgs/covers/nce-1.png"
},
{
"id": "article_nce2",
"name": "新概念英语2",
"description": "第二册《实践与进步》由浅入深通过96篇幽默短文的学习学习者将能在听、说、读、写四个维度全面实践英语培养英文语感、纠正发音和培养自然语调掌握归纳总结、造句及书写简单私人信件的能力。",
"category": "文章学习",
"tags": [
"新概念英语"
],
"url": "NCE_2.json",
"length": 96,
"translateLanguage": "common",
"language": "en",
"update": true,
"cover": "/imgs/covers/nce-2.png"
},
{
"id": "article_nce3",
"name": "新概念英语3",
"description": "从第三册《培养技能》开始,文章更加注重分析句子之间内在的逻辑关系,并进一步扩充讲解词汇、语法及句型的实战运用。通过这一册的学习,学员们听、说、读、写的能力有一个新的飞跃,能够熟练地表达和使用英语。",
"category": "文章学习",
"tags": [
"新概念英语"
],
"url": "NCE_3.json",
"length": 60,
"translateLanguage": "common",
"language": "en",
"update": true,
"cover": "/imgs/covers/nce-3.png"
},
{
"id": "article_nce4",
"name": "新概念英语4",
"description": "第四册《流利英语》,涵盖了文化、经济、哲学、艺术、体育、政治等三十多个学科门类,语言文字精美独到,句型结构复杂多变而又地道酣畅,适用于有志于进一步深入学习和掌握英语到游刃有余的境界的同学。学完四册的同学可以轻松阅读英文报刊与文学作品 ,加以适当实战,语言能力足以胜任欧美的生活和工作。",
"category": "文章学习",
"tags": [
"新概念英语"
],
"url": "NCE_4.json",
"length": 48,
"translateLanguage": "common",
"language": "en",
"update": true,
"cover": "/imgs/covers/nce-4.png"
}
]

View File

@ -0,0 +1,184 @@
[
{
"id": "cet4",
"name": "CET-4",
"description": "大学英语四级词库",
"category": "中国考试",
"tags": [
"大学英语"
],
"url": "CET4_T.json",
"length": 2607,
"language": "en",
"translateLanguage": "zh-CN"
},
{
"id": "cet6",
"name": "CET-6",
"description": "大学英语六级词库",
"category": "中国考试",
"tags": [
"大学英语"
],
"url": "CET6_T.json",
"length": 2345,
"language": "en",
"translateLanguage": "zh-CN"
},
{
"id": "level4",
"name": "专四",
"description": "英语专业四级词库",
"category": "中国考试",
"tags": [
"大学英语"
],
"url": "Level4luan_2_T.json",
"length": 4025,
"language": "en",
"translateLanguage": "zh-CN"
},
{
"id": "level8",
"name": "专八",
"description": "英语专业八级词库",
"category": "中国考试",
"tags": [
"大学英语"
],
"url": "Level8luan_2_T.json",
"length": 12197,
"language": "en",
"translateLanguage": "zh-CN"
},
{
"id": "kaoyan",
"name": "考研",
"description": "研究生英语入学考试词库",
"category": "中国考试",
"tags": [
"考研"
],
"url": "KaoYan_3_T.json",
"length": 3728,
"language": "en",
"translateLanguage": "zh-CN"
},
{
"id": "toefl",
"name": "TOEFL",
"description": "托福考试常见词",
"category": "国际考试",
"tags": [
"TOEFL"
],
"url": "TOEFL_3_T.json",
"length": 4264,
"language": "en",
"translateLanguage": "zh-CN"
},
{
"id": "ielts",
"name": "IELTS",
"description": "雅思词库",
"category": "国际考试",
"tags": [
"IELTS"
],
"url": "IELTS_3_T.json",
"length": 3575,
"language": "en",
"translateLanguage": "zh-CN"
},
{
"id": "gaokao3500",
"name": "高考 3500 词",
"description": "高考常见词 3500",
"category": "青少年英语",
"tags": [
"通用"
],
"url": "GaoKao_3500.json",
"length": 3893,
"language": "en",
"translateLanguage": "zh-CN"
},
{
"id": "coder",
"name": "Coder Dict",
"description": "程序员常见单词词库",
"category": "代码练习",
"tags": [
"通用"
],
"url": "it-words.json",
"length": 1700,
"language": "en",
"translateLanguage": "zh-CN"
},
{
"id": "itVocabulary",
"name": "计算机专用英语",
"description": "大学计算机专业英语词汇",
"category": "代码练习",
"tags": [
"通用"
],
"url": "itVocabulary.json",
"length": 1665,
"language": "en",
"translateLanguage": "zh-CN"
},
{
"id": "nce-new-1",
"name": "新概念英语-1",
"description": "新概念英语第一册",
"category": "青少年英语",
"tags": [
"新概念英语"
],
"url": "nce-new-1.json",
"length": 908,
"language": "en",
"translateLanguage": "zh-CN"
},
{
"id": "nce-new-2",
"name": "新概念英语-2",
"description": "新概念英语第二册",
"category": "青少年英语",
"tags": [
"新概念英语"
],
"url": "nce-new-2.json",
"length": 862,
"language": "en",
"translateLanguage": "zh-CN"
},
{
"id": "nce-new-3",
"name": "新概念英语-3",
"description": "新概念英语第三册",
"category": "青少年英语",
"tags": [
"新概念英语"
],
"url": "nce-new-3.json",
"length": 1062,
"language": "en",
"translateLanguage": "zh-CN"
},
{
"id": "nce-new-4",
"name": "新概念英语-4",
"description": "新概念英语第四册",
"category": "青少年英语",
"tags": [
"新概念英语"
],
"url": "nce-new-4.json",
"length": 793,
"language": "en",
"translateLanguage": "zh-CN"
}
]

3243
public/list/word.json Normal file

File diff suppressed because it is too large Load Diff

BIN
public/logo-text-black.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
public/logo-text-white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

16
public/manifest.json Normal file
View File

@ -0,0 +1,16 @@
{
"name": "Type Words - 词文记 | 单词跟打 · 文章跟打",
"short_name": "",
"start_url": ".",
"description": "在线英语打字练习平台,支持单词跟打与文章跟打,帮助提升打字速度与英语学习效率。",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#ffffff",
"icons": [
{
"src": "/favicon.png",
"sizes": "800x800",
"type": "image/png"
}
]
}

87
public/migrate.html Normal file
View File

@ -0,0 +1,87 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>TypeWords 数据迁移(旧域名)</title>
</head>
<body>
<h2 id="title">等待新域名发送迁移指令...</h2>
<pre id="log"></pre>
<script>
function log(msg) {
console.log(msg);
document.getElementById('log').textContent += msg + "\n";
}
// 1⃣ 先动态加载 idb-keyval
function loadIDBKeyval() {
return new Promise((resolve) => {
let script = document.createElement("script");
script.src = '/libs/idb-keyval.js';
script.onload = function () {
log("idb-keyval 加载完成");
resolve(window.idbKeyval);
};
document.head.appendChild(script);
});
}
loadIDBKeyval(); // 确保 idb-keyval 已经加载
// 2⃣ 读取 IndexedDB
async function readAllStorageForMigration(db) {
// localStorage 数据
const localStorageData = {
PracticeSaveWord: localStorage.getItem('PracticeSaveWord'),
PracticeSaveArticle: localStorage.getItem('PracticeSaveArticle')
};
// IndexedDB 数据key 对应你的老项目
const keys = [
'type-words-app-version',
'typing-word-dict',
'typing-word-setting',
'typing-word-files'
];
const indexedDBData = {};
for (let key of keys) {
let res = await db.get(key);
if (res) indexedDBData[key] = res
}
return {
localStorage: localStorageData,
indexedDB: indexedDBData
};
}
// 3⃣ 接收新域名指令
window.addEventListener('message', async (event) => {
if (event.data?.type !== 'REQUEST_MIGRATION_DATA') return;
// 安全校验 origin可选
// if (event.origin !== 'https://typewords.cc') return;
log("收到迁移指令,开始读取数据...");
const db = await loadIDBKeyval(); // 确保 idb-keyval 已经加载
const data = await readAllStorageForMigration(db);
log("读取完成,发送数据给新域名");
event.source.postMessage({
type: 'MIGRATION_RESULT',
payload: data
}, event.origin);
log("已发送迁移数据");
// 自动关闭窗口(延迟 500ms
setTimeout(() => {
document.getElementById('title').textContent = '迁移完成,请手动关闭页面'
window.close();
}, 500);
});
</script>
</body>
</html>

100
public/privacy-policy.html Normal file
View File

@ -0,0 +1,100 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>隐私政策</title>
</head>
<body
style="display:flex;justify-content:center">
<div class="privacy-page"
style="width: 60vw;"
>
<h1 style="text-align: center">隐私政策</h1>
<div class="content">
<section>
<h2>一、引言</h2>
<p>
我们非常重视您的隐私保护。本隐私政策说明了我们如何收集、使用、存储和保护您的个人信息。在使用本应用之前,请您仔细阅读本隐私政策。</p>
</section>
<section>
<h2>二、信息收集</h2>
<p>我们可能收集以下信息:</p>
<p><strong>1. 账户信息:</strong>当您注册账户时,我们会收集您的手机号、邮箱地址、密码等信息。</p>
<p><strong>2. 学习数据:</strong>我们会记录您的学习进度、学习记录、练习数据等信息,以便为您提供个性化的学习服务。
</p>
<p><strong>3. 设备信息:</strong>我们可能收集您的设备型号、操作系统版本、唯一设备标识符等信息,用于改善服务质量和安全性。
</p>
<p><strong>4. 日志信息:</strong>当您使用本应用时我们可能自动收集某些信息包括IP地址、访问时间、访问页面等。
</p>
</section>
<section>
<h2>三、信息使用</h2>
<p>我们使用收集的信息用于以下目的:</p>
<p>1. 提供、维护和改进我们的服务;</p>
<p>2. 处理您的注册、登录、学习记录等请求;</p>
<p>3. 向您发送服务通知、更新和安全提醒;</p>
<p>4. 进行数据分析,以改善用户体验和服务质量;</p>
<p>5. 检测、预防和解决技术问题;</p>
<p>6. 遵守法律法规要求。</p>
</section>
<section>
<h2>四、信息存储</h2>
<p>1. 我们采用行业标准的安全措施来保护您的个人信息,防止未经授权的访问、使用或泄露。</p>
<p>2. 您的个人信息将存储在安全的服务器上,我们会对数据进行加密处理。</p>
<p>3. 我们仅在为实现本隐私政策所述目的所必需的期间内保留您的个人信息。</p>
</section>
<section>
<h2>五、信息共享</h2>
<p>我们不会向第三方出售、交易或转让您的个人信息,除非:</p>
<p>1. 获得您的明确同意;</p>
<p>2. 法律法规要求或司法机关、行政机关依法要求提供;</p>
<p>3. 为履行我们的服务协议或本隐私政策,我们可能需要与我们的服务提供商共享某些信息。</p>
</section>
<section>
<h2>六、Cookie和类似技术</h2>
<p>
我们可能使用Cookie和类似技术来收集信息、改善用户体验和分析服务使用情况。您可以通过浏览器设置管理Cookie但这可能影响某些功能的正常使用。</p>
</section>
<section>
<h2>七、您的权利</h2>
<p>根据相关法律法规,您对自己的个人信息享有以下权利:</p>
<p>1. <strong>访问权:</strong>您有权访问我们持有的关于您的个人信息;</p>
<p>2. <strong>更正权:</strong>您有权要求更正不准确的个人信息;</p>
<p>3. <strong>删除权:</strong>在特定情况下,您有权要求删除您的个人信息;</p>
<p>4. <strong>撤回同意:</strong>您有权随时撤回您之前给予的同意;</p>
<p>5. <strong>投诉权:</strong>如果您认为我们对您个人信息的处理违反了相关法律法规,您有权向相关监管部门投诉。
</p>
</section>
<section>
<h2>八、未成年人保护</h2>
<p>
我们非常重视未成年人的个人信息保护。如果您是未成年人,建议您请您的父母或监护人仔细阅读本隐私政策,并在征得您的父母或监护人同意的前提下使用我们的服务。</p>
</section>
<section>
<h2>九、隐私政策更新</h2>
<p>
我们可能会不时更新本隐私政策。我们会在本页面上发布新的隐私政策,并通过适当方式通知您。如果您不同意更新后的隐私政策,您可以选择停止使用我们的服务。</p>
</section>
<section>
<h2>十、联系我们</h2>
<p>如果您对本隐私政策有任何疑问、意见或建议,或需要行使您的相关权利,请通过以下方式联系我们:</p>
<p>邮箱zyronon@163.com</p>
</section>
<div class="update-time">
<p>最后更新时间2025年11月11日</p>
</div>
</div>
</div>
</body>
</html>

BIN
public/qq.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

4
public/robots.txt Normal file
View File

@ -0,0 +1,4 @@
User-agent: *
Disallow:
Sitemap: https://typewords.cc/sitemap.xml

12
public/service-worker.js Normal file
View File

@ -0,0 +1,12 @@
// 'install' 事件在 Service Worker 首次被安装时触发。
self.addEventListener('install', event => {
self.skipWaiting();
});
// 'activate' 事件在 Service Worker 变为激活状态时触发。
self.addEventListener('activate', event => {
return self.clients.claim();
});
self.addEventListener('fetch', event => {
});

BIN
public/skywork-ai.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

781
public/static-home.html Normal file
View File

@ -0,0 +1,781 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"/>
<!-- 百度站长HTML标签验证 -->
<meta name="baidu-site-verification" content="codeva-NoSMtV313P" />
<title>Type Words 官网 - 词文记 | 单词跟打 · 文章跟打</title>
<!-- 搜索引擎描述 -->
<meta name="description"
content="Type Words 官方网站 - 在线英语练习平台支持单词、文章跟打练习提升英语学习效率。Practice English, one strike, one step forward">
<!-- 关键词(可选,搜索引擎基本不用,但能补充信息) -->
<meta name="keywords"
content="Type Words, Typing Word, Type Words 官网, 官方网站, 英语打字练习, 单词跟打, 文章跟打, 键盘练习, 英语学习, 文章学习, 打字练习软件, 单词记忆工具, 英语学习软件, 背单词神器, 英语肌肉记忆, 键盘工作者, 免费英语学习, 音标发音, 默写练习, 在线学英语, CET-4, CET-6, TOEFL, IELTS, GRE, GMAT, SAT, 考研英语, 专四专八, 程序员英语, JavaScript API, Node.js API, Java API, Linux命令, 编程词汇, 技术英语, VSCode插件, 开源项目, GitHub趋势榜, V2EX热搜, Gitee GVP, 少数派推荐, 英语打字训练, WPM统计, 准确率分析, 商务英语, BEC, 雅思听力, 日语学习, 多语言学习, 英语口语练习, 单词拼写训练">
<meta name="author" content="zyronon"/>
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1"/>
<link rel="canonical" href="https://typewords.cc/"/>
<!-- Open Graph用于社交媒体分享微信/QQ/知乎/Facebook 等) -->
<meta property="og:title" content="Type Words 官网 - 英语打字练习平台">
<meta property="og:description"
content="Type Words 官方网站 - 在线英语练习平台支持单词、文章跟打练习提升英语学习效率。Practice English, one strike, one step forward">
<meta property="og:type" content="website">
<meta property="og:url" content="https://typewords.cc/">
<meta property="og:image" content="https://typewords.cc/favicon.png">
<!-- Twitter Card用于 Twitter 分享) -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Type Words 官网 - 英语打字练习平台">
<meta name="twitter:description"
content="Type Words 官方网站 - 在线英语练习平台支持单词、文章跟打练习提升英语学习效率。Practice English, one strike, one step forward">
<meta name="twitter:image" content="https://typewords.cc/favicon.png">
<link rel="icon" type="image/svg+xml" href="/favicon.png"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<!-- 苹果设备iOS Safari在用户添加到主屏时显示的图标-->
<link rel="apple-touch-icon" sizes="180x180" href="/favicon.png"/>
<!-- 设置浏览器地址栏颜色(在 Android Chrome 特别明显)。-->
<meta name="theme-color" content="#818CF8"/>
<link rel="manifest" href="/manifest.json">
<!-- 阻止 iOS 自动把数字识别为电话号码。-->
<!-- HandheldFriendly 和 MobileOptimized 是旧手机浏览器的优化提示(现在作用不大)。-->
<meta name="format-detection" content="telephone=no"/>
<meta name="HandheldFriendly" content="True"/>
<meta name="MobileOptimized" content="320"/>
<!-- referrer 控制请求来源信息-->
<meta name="referrer" content="origin-when-cross-origin"/>
<!-- color-scheme 告诉浏览器支持亮/暗模式-->
<meta name="color-scheme" content="light dark"/>
<style>
:root {
--color-bg: #E6E8EB;
--color-card-bg: rgb(247, 247, 247);
--color-card-text: #111827;
--color-line: #cecece;
--color-h2: rgb(91, 91, 91);
--accent: #818CF8;
--accent-2: #60A5FA;
--shadow: 0 10px 30px rgba(0, 0, 0, .08);
}
html.dark {
--color-bg: #0E1217;
--color-card-bg: rgb(30, 31, 34);
--color-card-text: #c6c6c6;
--color-line: #333333;
--color-h2: rgb(151, 151, 151);
--shadow: 0 10px 30px rgba(0, 0, 0, .35);
}
body {
background: var(--color-bg);
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
color: var(--color-card-text);
}
h1 {
font-size: 4.8rem !important;
background: linear-gradient(120deg, #bd34fe 30%, #41d1ff);
-webkit-text-fill-color: transparent;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
margin: 0;
font-weight: 800 !important;
letter-spacing: -0.02em;
}
h2 {
font-size: 1.4rem !important;
font-weight: normal !important;
color: var(--color-h2);
margin: 0;
}
.card {
position: relative;
border-radius: 1rem;
padding: 1.2rem;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.6rem;
margin-bottom: 0;
background: var(--color-card-bg);
color: var(--color-card-text);
box-shadow: var(--shadow);
border: 1px solid var(--color-line);
transition: transform .2s ease, box-shadow .2s ease;
}
.card:hover {
transform: translateY(-3px);
box-shadow: 0 12px 32px rgba(0, 0, 0, .12);
}
.card .emoji {
display: inline-block;
background: rgb(226 232 240 / 1);
padding: 0.3rem .6rem;
border-radius: 0.4rem;
font-size: 1.5rem;
}
.card .title {
font-weight: bold;
}
.card ul {
margin: 0;
padding-left: 1.2rem;
}
a {
color: dodgerblue !important;
}
.base-button {
cursor: pointer;
box-sizing: border-box;
display: inline-flex;
align-items: center;
justify-content: center;
outline: none;
text-align: center;
transition: .2s ease;
user-select: none;
vertical-align: middle;
white-space: nowrap;
border-radius: .6rem;
color: #fff;
background: linear-gradient(135deg, var(--accent), var(--accent-2));
padding: 0 1.4rem;
height: 2.7rem;
font-size: 0.95rem;
box-shadow: 0 8px 20px rgba(129, 140, 248, .25);
}
.base-button + .base-button {
margin-left: 2rem;
}
.base-button:hover {
transform: translateY(-1px);
opacity: .95;
}
.base-button.secondary {
background: transparent;
color: var(--color-card-text);
border: 1px solid var(--color-line);
box-shadow: none;
}
.icon {
cursor: pointer;
width: 2rem;
height: 2rem;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: .3rem;
background: transparent;
transition: all .3s;
color: dimgray;
}
.icon:hover {
background: rgb(12, 140, 233);
svg {
color: white !important;
}
}
.mask {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.3);
display: none;
opacity: 0;
transition: all .3s;
}
.dialog {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background: rgb(247, 247, 247);
width: 30rem;
max-width: 85vw;
border-radius: 1rem;
display: none;
opacity: 0;
transition: all .3s;
}
.dialog header {
padding: 1rem;
display: flex;
justify-content: space-between;
}
.dialog header .title {
font-size: 1.4rem;
}
.dialog-body {
padding: 0 1.2rem 1.2rem 1.2rem;
}
.wrapper {
display: flex;
flex-direction: column;
justify-content: space-between;
min-height: 100vh;
}
.nav {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: saturate(180%) blur(8px);
display: none;
}
.nav-inner {
width: min(1200px, 90%);
display: flex;
align-items: center;
justify-content: space-between;
}
.brand {
display: flex;
align-items: center;
gap: .6rem;
font-weight: 700;
}
.brand-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--accent);
}
.nav-actions {
display: flex;
align-items: center;
gap: .6rem;
}
.content {
margin-top: 7rem;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 2.2rem;
}
.text-center {
text-align: center;
}
.sky {
margin-top: 3rem;
border-top: 1px solid var(--color-line);
border-bottom: 1px solid var(--color-line);
padding: 1.2rem 0;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 0.4rem;
width: 100%;
margin-bottom: 1rem;
color: var(--color-card-text);
display: none;
}
.container {
width: min(1200px, 92%);
}
.sky-img {
width: 100%;
border-radius: 0.5rem;
}
.card-wrap {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
margin-bottom: 1.2rem;
gap: 1rem;
}
.card { width: auto; }
@media (max-width: 768px) {
h1 { font-size: 3rem !important; }
.content { margin-top: 4rem; gap: 1.4rem; }
.base-button { width: 100%; margin: .5rem 0; height: 2.8rem; font-size: 1rem; }
.base-button + .base-button{margin-left: 0;}
.bottom { flex-direction: column; align-items: flex-start; gap: .6rem; }
.sky a { width: 100% !important; }
.sky-img { width: 100%; }
}
@media (max-width: 480px) {
h1 { font-size: 2.4rem !important; }
.content { margin-top: 3.2rem; gap: 1.2rem; }
}
html, body { -webkit-tap-highlight-color: transparent; }
.icon { padding: .2rem; }
.bottom {
display: flex;
gap: 1rem;
margin: 1rem 0 2rem 0;
width: 100%;
padding-top: 1.5rem;
border-top: 1px solid var(--color-line);
justify-content: center;
align-items: center;
}
.gap-1 {
gap: 1rem;
}
.center {
display: flex;
justify-content: center;
align-items: center;
}
.img {
width: 16rem;
border-radius: 1rem;
margin-top: 1.2rem;
box-shadow: var(--shadow);
}
.cursor-pointer {
cursor: pointer;
}
</style>
<script>
if (
!location.href.includes('localhost')
&& !location.href.includes('192.168')
&& !location.href.includes('172.16')
&& !location.href.includes('10.0')
) {
//51.la
(function () {
window.LA = window.LA || {
ids: [{id: "3OH8ITYRgwzo58L2", ck: "3OH8ITYRgwzo58L2"}],
id: "3OH8ITYRgwzo58L2",
ck: "3OH8ITYRgwzo58L2",
hashMode: true
};
const script = document.createElement('script');
script.src = `https://typewords.cc/libs/51.js`;
document.head.appendChild(script);
})();
// Cloudflare
(function () {
var cf = document.createElement("script");
cf.src = 'https://static.cloudflareinsights.com/beacon.min.js'
cf.setAttribute("data-cf-beacon", '{"token": "e5119992696d4155814400dd69781d68"}');
document.head.appendChild(cf);
})();
// google
(function () {
var ana = document.createElement("script");
ana.src = 'https://www.googletagmanager.com/gtag/js?id=G-50T6DRD837'
ana.onload = function () {
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-50T6DRD837');
}
document.head.appendChild(ana);
})();
// baidu
var _hmt = _hmt || [];
(function () {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?3dae52fcd5375a19905462e4ad3eb54e";
document.head.appendChild(hm);
})();
// umami
(function () {
var umami = document.createElement("script");
umami.src = 'https://typewords.cc/libs/s.js'
umami.setAttribute("data-website-id", "160308c9-7900-4b1d-a0b1-c3b25a9530f6");
document.head.appendChild(umami);
})();
// umami-saas
// (function () {
// var umami2 = document.createElement("script");
// umami2.src = 'https://stat.typewords.cc/script.js'
// umami2.setAttribute("data-website-id", "4d728ae3-5393-4efe-81dc-30dcb4f33c00");
// document.head.appendChild(umami2);
// })();
}
</script>
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js').then(registration => {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}).catch(error => {
console.log('ServiceWorker registration failed: ', error);
});
});
}
</script>
<script>
function nav(url) {
window.location.href = url;
// history.pushState(null, "", url);
}
function toggleEl(val, close = false) {
let le = document.querySelector(val)
if (le) {
if (['none', ''].includes(le.style.display) && !close) {
le.style.display = 'block';
setTimeout(function () {
le.style.opacity = 1;
}, 10)
} else {
le.style.opacity = 0;
setTimeout(function () {
le.style.display = 'none';
}, 300)
}
}
}
function toggleWechatDialog() {
toggleEl('.mask')
toggleEl('#wechatDialog')
}
function toggleQQDialog() {
toggleEl('.mask')
toggleEl('#qqDialog')
}
function toggleXhsDialog() {
toggleEl('.mask')
toggleEl('#xhsDialog')
}
function closeDialog() {
toggleEl('.mask')
toggleEl('#wechatDialog', true)
toggleEl('#xhsDialog', true)
toggleEl('#qqDialog', true)
}
window.onload = () => {
function getSystemTheme() {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
} else if (window.matchMedia('(prefers-color-scheme: light)').matches) {
return 'light';
}
return 'light'; // 默认浅色模式
}
document.documentElement.className = getSystemTheme()
}
</script>
</head>
<body>
<div class="wrapper">
<div class="nav">
<div class="nav-inner">
<div class="brand">
<div class="brand-dot"></div>
<span>Type Words</span>
</div>
<div class="nav-actions">
<div class="base-button secondary" onclick="nav('/words')">单词练习</div>
<div class="base-button secondary" onclick="nav('/articles')">文章练习</div>
<div class="base-button secondary" onclick="nav('/vip')">会员介绍</div>
<div class="base-button" onclick="nav('/login')">登录</div>
</div>
</div>
</div>
<div class="content">
<h1>Type Words</h1>
<div class="text-center">
<h2>学习英语,一次敲击,一点进步,开源单词与文章练习工具</h2>
</div>
<div class="">
<div class="base-button" onclick="nav('/words')">开始单词练习</div>
<div class="base-button" onclick="nav('/articles')">开始文章练习</div>
<!-- <div class="base-button secondary" onclick="nav('/vip')">会员介绍</div>-->
</div>
<div class="sky">
<a href="https://skywork.ai/p/GrXQb4" style="width: 40%;" target="_blank">
<img src="https://typewords.cc/skywork-ai.png"
alt="Skywork.AI"
class="sky-img"></a>
<span>赞助:<a href="https://skywork.ai/p/GrXQb4" class="color-blue!" target="_blank">Skywork.AI: 10 tasks in 1 hour, not 10 hours →Limited free spots: 127 left</a></span>
</div>
<div class="container">
<div class="card-wrap">
<div class="card">
<div class="emoji">📚</div>
<div class="title">单词练习</div>
<div class="desc">
<ul>
<li>四种模式:跟打/听写/辨认/默写</li>
<li>智能模式:智能规划复习与默写</li>
<li>自由模式:不受限制,自行规划</li>
</ul>
</div>
</div>
<div class="card">
<div class="emoji">✍️</div>
<div class="title">文章练习</div>
<div class="desc">
<ul>
<li>内置常见书籍,也可自行添加文章</li>
<li>跟打 + 默写双模式,让背诵更高效</li>
<li>支持边听边默写,强化记忆</li>
</ul>
</div>
</div>
<div class="card">
<div class="emoji">📕</div>
<div class="title">收藏、错词本、已掌握</div>
<div class="desc">
<ul>
<li>输入错误自动添加到错词本</li>
<li>主动添加到已掌握,后续自动跳过</li>
<li>主动添加到收藏中,以便巩固复习</li>
</ul>
</div>
</div>
<div class="card">
<div class="emoji">🌐</div>
<div class="title">海量词库</div>
<div class="desc">
内置小学、初中、高中、四六级、考研、雅思、托福、GRE、GMAT、SAT、BEC、专四、专八等词库
</div>
</div>
</div>
<div class="card-wrap">
<div class="card">
<div class="emoji">🆓</div>
<div class="title">免费开源</div>
<div class="desc">
<ul>
<li>完全开源,可审查、可修改</li>
<li>免费使用</li>
<li>私有部署</li>
</ul>
</div>
</div>
<div class="card">
<div class="emoji">⚙️</div>
<div class="title">高度自由</div>
<div class="desc">
<ul>
<li>丰富的键盘音效</li>
<li>可自定义快捷键</li>
<li>高度定制化的设置选项</li>
</ul>
</div>
</div>
<div class="card">
<div class="emoji">🎨</div>
<div class="title">简洁高效</div>
<div class="desc">
<ul>
<li>简洁设计现代化UI</li>
<li>界面清爽,操作简单</li>
<li>不强制关注任何平台</li>
</ul>
</div>
</div>
<div class="card">
<div class="emoji">🎯</div>
<div class="title">个性学习</div>
<div class="desc">
<ul>
<li>自由添加词典与文章</li>
<li>定制个性学习计划</li>
<li>多种学习复习策略</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="bottom">
<div class="center gap-1">
<a
href="https://github.com/zyronon/TypeWords"
target="_blank"
rel="noreferrer"
aria-label="Github Address">
<div class="icon">
<svg viewBox="0 0 24 24" width="1.4em" height="1.4em">
<path fill="currentColor"
d="M12 .297c-6.63 0-12 5.373-12 12c0 5.303 3.438 9.8 8.205 11.385c.6.113.82-.258.82-.577c0-.285-.01-1.04-.015-2.04c-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729c1.205.084 1.838 1.236 1.838 1.236c1.07 1.835 2.809 1.305 3.495.998c.108-.776.417-1.305.76-1.605c-2.665-.3-5.466-1.332-5.466-5.93c0-1.31.465-2.38 1.235-3.22c-.135-.303-.54-1.523.105-3.176c0 0 1.005-.322 3.3 1.23c.96-.267 1.98-.399 3-.405c1.02.006 2.04.138 3 .405c2.28-1.552 3.285-1.23 3.285-1.23c.645 1.653.24 2.873.12 3.176c.765.84 1.23 1.91 1.23 3.22c0 4.61-2.805 5.625-5.475 5.92c.42.36.81 1.096.81 2.22c0 1.606-.015 2.896-.015 3.286c0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"></path>
</svg>
</div>
</a>
<div class="icon" onclick="toggleWechatDialog()">
<svg viewBox="0 0 24 24" width="1.4em" height="1.4em" color="#22C55E">
<path fill="currentColor"
d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213c0 .163.13.295.29.295a.33.33 0 0 0 .167-.054l1.903-1.114a.86.86 0 0 1 .717-.098a10.2 10.2 0 0 0 2.837.403c.276 0 .543-.027.811-.05c-.857-2.578.157-4.972 1.932-6.446c1.703-1.415 3.882-1.98 5.853-1.838c-.576-3.583-4.196-6.348-8.596-6.348M5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178a1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18m5.34 2.867c-1.797-.052-3.746.512-5.28 1.786c-1.72 1.428-2.687 3.72-1.78 6.22c.942 2.453 3.666 4.229 6.884 4.229c.826 0 1.622-.12 2.361-.336a.72.72 0 0 1 .598.082l1.584.926a.3.3 0 0 0 .14.047c.134 0 .24-.111.24-.247c0-.06-.023-.12-.038-.177l-.327-1.233a.6.6 0 0 1-.023-.156a.49.49 0 0 1 .201-.398C23.024 18.48 24 16.82 24 14.98c0-3.21-2.931-5.837-6.656-6.088V8.89c-.135-.01-.27-.027-.407-.03zm-2.53 3.274c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983a.976.976 0 0 1-.969-.983c0-.542.434-.982.97-.982zm4.844 0c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983a.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982"></path>
</svg>
</div>
<div class="icon" onclick="toggleQQDialog()">
<svg viewBox="0 0 24 24" width="1.4em" height="1.4em" color="#F87171">
<g fill="none">
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"/>
<path fill="currentColor"
d="M12 2a6.285 6.285 0 0 0-6.276 5.937l-.146 2.63a28 28 0 0 0-.615 1.41c-1.24 3.073-1.728 5.773-1.088 6.032c.335.135.913-.426 1.566-1.432a6.67 6.67 0 0 0 1.968 3.593c-1.027.35-1.91.828-1.91 1.33c0 .509 2.48.503 4.239.5h.001c.549-.002 1.01-.008 1.38-.057a6.7 6.7 0 0 0 1.76 0c.37.05.833.055 1.382.056c1.76.004 4.239.01 4.239-.499c0-.502-.883-.979-1.909-1.33a6.67 6.67 0 0 0 1.967-3.586c.65 1.002 1.227 1.56 1.56 1.425c.64-.259.154-2.96-1.088-6.032a28 28 0 0 0-.607-1.395l-.147-2.645A6.285 6.285 0 0 0 12 2"/>
</g>
</svg>
</div>
<div class="icon" onclick="toggleXhsDialog()">
<svg viewBox="0 0 24 24" width="1.4em" height="1.4em" color="#EF4444">
<path fill="currentColor"
d="M22.405 9.879c.002.016.01.02.07.019h.725a.797.797 0 0 0 .78-.972a.794.794 0 0 0-.884-.618a.795.795 0 0 0-.692.794c0 .101-.002.666.001.777m-11.509 4.808c-.203.001-1.353.004-1.685.003a2.5 2.5 0 0 1-.766-.126a.025.025 0 0 0-.03.014L7.7 16.127a.025.025 0 0 0 .01.032c.111.06.336.124.495.124c.66.01 1.32.002 1.981 0q.017 0 .023-.015l.712-1.545a.025.025 0 0 0-.024-.036zM.477 9.91c-.071 0-.076.002-.076.01l-.01.08c-.027.397-.038.495-.234 3.06c-.012.24-.034.389-.135.607c-.026.057-.033.042.003.112c.046.092.681 1.523.787 1.74c.008.015.011.02.017.02c.008 0 .033-.026.047-.044q.219-.282.371-.606c.306-.635.44-1.325.486-1.706c.014-.11.021-.22.03-.33l.204-2.616l.022-.293c.003-.029 0-.033-.03-.034zm7.203 3.757a1.4 1.4 0 0 1-.135-.607c-.004-.084-.031-.39-.235-3.06a.4.4 0 0 0-.01-.082c-.004-.011-.052-.008-.076-.008h-1.48c-.03.001-.034.005-.03.034l.021.293q.114 1.473.233 2.946c.05.4.186 1.085.487 1.706c.103.215.223.419.37.606c.015.018.037.051.048.049c.02-.003.742-1.642.804-1.765c.036-.07.03-.055.003-.112m3.861-.913h-.872a.126.126 0 0 1-.116-.178l1.178-2.625a.025.025 0 0 0-.023-.035l-1.318-.003a.148.148 0 0 1-.135-.21l.876-1.954a.025.025 0 0 0-.023-.035h-1.56q-.017 0-.024.015l-.926 2.068c-.085.169-.314.634-.399.938a.5.5 0 0 0-.02.191a.46.46 0 0 0 .23.378a1 1 0 0 0 .46.119h.59c.041 0-.688 1.482-.834 1.972a.5.5 0 0 0-.023.172a.47.47 0 0 0 .23.398c.15.092.342.12.475.12l1.66-.001q.017 0 .023-.015l.575-1.28a.025.025 0 0 0-.024-.035m-6.93-4.937H3.1a.032.032 0 0 0-.034.033c0 1.048-.01 2.795-.01 6.829c0 .288-.269.262-.28.262h-.74c-.04.001-.044.004-.04.047c.001.037.465 1.064.555 1.263c.01.02.03.033.051.033c.157.003.767.009.938-.014c.153-.02.3-.06.438-.132c.3-.156.49-.419.595-.765c.052-.172.075-.353.075-.533q.003-3.495-.007-6.991a.03.03 0 0 0-.032-.032zm11.784 6.896q-.002-.02-.024-.022h-1.465c-.048-.001-.049-.002-.05-.049v-4.66c0-.072-.005-.07.07-.07h.863c.08 0 .075.004.075-.074V8.393c0-.082.006-.076-.08-.076h-3.5c-.064 0-.075-.006-.075.073v1.445c0 .083-.006.077.08.077h.854c.075 0 .07-.004.07.07v4.624c0 .095.008.084-.085.084c-.37 0-1.11-.002-1.304 0c-.048.001-.06.03-.06.03l-.697 1.519s-.014.025-.008.036s.013.008.058.008q2.622.003 5.243.002c.03-.001.034-.006.035-.033zm4.177-3.43q0 .021-.02.024c-.346.006-.692.004-1.037.004q-.021-.003-.022-.024q-.006-.651-.01-1.303c0-.072-.006-.071.07-.07l.733-.003c.041 0 .081.002.12.015c.093.025.16.107.165.204c.006.431.002 1.153.001 1.153m2.67.244a1.95 1.95 0 0 0-.883-.222h-.18c-.04-.001-.04-.003-.042-.04V10.21q.001-.198-.025-.394a1.8 1.8 0 0 0-.153-.53a1.53 1.53 0 0 0-.677-.71a2.2 2.2 0 0 0-1-.258c-.153-.003-.567 0-.72 0c-.07 0-.068.004-.068-.065V7.76c0-.031-.01-.041-.046-.039H17.93s-.016 0-.023.007q-.008.008-.008.023v.546c-.008.036-.057.015-.082.022h-.95c-.022.002-.028.008-.03.032v1.481c0 .09-.004.082.082.082h.913c.082 0 .072.128.072.128v1.148s.003.117-.06.117h-1.482c-.068 0-.06.082-.06.082v1.445s-.01.068.064.068h1.457c.082 0 .076-.006.076.079v3.225c0 .088-.007.081.082.081h1.43c.09 0 .082.007.082-.08v-3.27c0-.029.006-.035.033-.035l2.323-.003a.7.7 0 0 1 .28.061a.46.46 0 0 1 .274.407c.008.395.003.79.003 1.185c0 .259-.107.367-.33.367h-1.218c-.023.002-.029.008-.028.033q.276.655.57 1.303a.05.05 0 0 0 .04.026c.17.005.34.002.51.003c.15-.002.517.004.666-.01a2 2 0 0 0 .408-.075c.59-.18.975-.698.976-1.313v-1.981q.001-.191-.034-.38c0 .078-.029-.641-.724-.998"></path>
</svg>
</div>
<a
href="https://x.com/typewords2"
target="_blank"
rel="noreferrer"
aria-label="关注我的 X 账户 typewords2">
<div class="icon">
<svg viewBox="0 0 24 24" width="1.4em" height="1.4em" color="#60A5FA">
<path fill="currentColor"
d="M22.213 5.656a8.4 8.4 0 0 1-2.402.658A4.2 4.2 0 0 0 21.649 4c-.82.488-1.719.83-2.655 1.015a4.182 4.182 0 0 0-7.126 3.814a11.87 11.87 0 0 1-8.621-4.37a4.17 4.17 0 0 0-.566 2.103c0 1.45.739 2.731 1.86 3.481a4.2 4.2 0 0 1-1.894-.523v.051a4.185 4.185 0 0 0 3.355 4.102a4.2 4.2 0 0 1-1.89.072A4.185 4.185 0 0 0 8.02 16.65a8.4 8.4 0 0 1-6.192 1.732a11.83 11.83 0 0 0 6.41 1.88c7.694 0 11.9-6.373 11.9-11.9q0-.271-.012-.541a8.5 8.5 0 0 0 2.086-2.164"></path>
</svg>
</div>
</a>
<a
href="mailto:zyronon@163.com"
target="_blank"
rel="noreferrer"
aria-label="发送邮件到 zyronon@163.com">
<div class="icon">
<svg viewBox="0 0 24 24" width="1.4em" height="1.4em" color="#60A5FA">
<path fill="currentColor"
d="M4 20q-.825 0-1.412-.587T2 18V6q0-.825.588-1.412T4 4h16q.825 0 1.413.588T22 6v12q0 .825-.587 1.413T20 20zm8-7l8-5V6l-8 5l-8-5v2z"></path>
</svg>
</div>
</a>
</div>
<div><a href="https://beian.mps.gov.cn/#/query/webSearch?code=51015602001426" target="_blank">川公网安备51015602001426号 </a></div>
<div><a href="https://beian.miit.gov.cn/" target="_blank">蜀ICP备2025157466号-2</a></div>
</div>
<div class="mask" onclick="closeDialog()"></div>
<div class="dialog" id="wechatDialog">
<header>
<div class="title">微信群</div>
<svg
onclick="toggleWechatDialog()"
viewBox="0 0 20 20" width="24" height="1.2em" class="cursor-pointer">
<path fill="currentColor"
d="m4.089 4.216l.057-.07a.5.5 0 0 1 .638-.057l.07.057L10 9.293l5.146-5.147a.5.5 0 0 1 .638-.057l.07.057a.5.5 0 0 1 .057.638l-.057.07L10.707 10l5.147 5.146a.5.5 0 0 1 .057.638l-.057.07a.5.5 0 0 1-.638.057l-.07-.057L10 10.707l-5.146 5.147a.5.5 0 0 1-.638.057l-.07-.057a.5.5 0 0 1-.057-.638l.057-.07L9.293 10L4.146 4.854a.5.5 0 0 1-.057-.638l.057-.07z"></path>
</svg>
</header>
<div class="dialog-body">
<span>加入我们的用户社群后,您可以与我们的开发团队进行沟通,分享您的使用体验和建议,帮助我们改进产品,同时也能够及时了解我们的最新动态和更新内容。</span>
<div class="center">
<img src="/wechat.png" alt="微信群二维码" class="img">
</div>
</div>
</div>
<div class="dialog" id="xhsDialog">
<header>
<div class="title">小红书</div>
<svg
onclick="toggleXhsDialog()"
viewBox="0 0 20 20" width="24" height="1.2em" class="cursor-pointer">
<path fill="currentColor"
d="m4.089 4.216l.057-.07a.5.5 0 0 1 .638-.057l.07.057L10 9.293l5.146-5.147a.5.5 0 0 1 .638-.057l.07.057a.5.5 0 0 1 .057.638l-.057.07L10.707 10l5.147 5.146a.5.5 0 0 1 .057.638l-.057.07a.5.5 0 0 1-.638.057l-.07-.057L10 10.707l-5.146 5.147a.5.5 0 0 1-.638.057l-.07-.057a.5.5 0 0 1-.057-.638l.057-.07L9.293 10L4.146 4.854a.5.5 0 0 1-.057-.638l.057-.07z"></path>
</svg>
</header>
<div class="dialog-body">
<span>关注小红书后,您可以获得开发团队的最新动态和更新内容,反馈您的使用体验和建议,帮助我们改进产品,同时也能够及时了解我们的最新动态和更新内容。</span>
<div class="center">
<img src="/xhs.png" alt="小红书二维码" class="img">
</div>
</div>
</div>
<div class="dialog" id="qqDialog">
<header>
<div class="title">QQ群</div>
<svg
onclick="toggleQQDialog()"
viewBox="0 0 20 20" width="24" height="1.2em" class="cursor-pointer">
<path fill="currentColor"
d="m4.089 4.216l.057-.07a.5.5 0 0 1 .638-.057l.07.057L10 9.293l5.146-5.147a.5.5 0 0 1 .638-.057l.07.057a.5.5 0 0 1 .057.638l-.057.07L10.707 10l5.147 5.146a.5.5 0 0 1 .057.638l-.057.07a.5.5 0 0 1-.638.057l-.07-.057L10 10.707l-5.146 5.147a.5.5 0 0 1-.638.057l-.07-.057a.5.5 0 0 1-.057-.638l.057-.07L9.293 10L4.146 4.854a.5.5 0 0 1-.057-.638l.057-.07z"></path>
</svg>
</header>
<div class="dialog-body">
<span>加入我们的用户社群后,您可以与我们的开发团队进行沟通,分享您的使用体验和建议,帮助我们改进产品,同时也能够及时了解我们的最新动态和更新内容。</span>
<div class="center">
<img src="/qq.jpg" alt="QQ群二维码" class="img">
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,83 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>用户协议</title>
</head>
<body
style="display:flex;justify-content:center">
<div class="privacy-page"
style="width: 60vw;"
>
<h1 style="text-align: center">用户协议</h1>
<div class="content">
<section>
<h2>一、总则</h2>
<p>欢迎使用本应用!在使用本应用之前,请您仔细阅读本用户协议(以下简称"本协议")。当您注册、登录、使用(以下统称"使用")本应用时,即表示您已阅读、理解并同意接受本协议的全部内容。</p>
</section>
<section>
<h2>二、服务内容</h2>
<p>本应用为用户提供单词学习、文章阅读等在线教育服务。我们保留随时修改或中断服务而不需通知用户的权利,我们行使修改或中断服务的权利,不需对用户或第三方负责。</p>
</section>
<section>
<h2>三、用户账户</h2>
<p>1. 用户在使用本应用前需要注册一个账户。用户应当使用真实、准确、完整的信息注册账户。</p>
<p>2. 用户有责任维护账户信息的安全,对账户下的所有活动负责。</p>
<p>3. 用户不得将账户转让、出售或以其他方式提供给第三方使用。</p>
</section>
<section>
<h2>四、用户行为规范</h2>
<p>用户在使用本应用时,应当遵守相关法律法规,不得从事以下行为:</p>
<p>1. 发布、传播违法、有害、威胁、辱骂、骚扰、侵权、诽谤、淫秽、暴力或其他不当内容;</p>
<p>2. 侵犯他人知识产权、隐私权或其他合法权益;</p>
<p>3. 干扰或破坏本应用的正常运行;</p>
<p>4. 使用自动化工具或脚本进行数据采集、批量操作等;</p>
<p>5. 其他违反法律法规或本协议的行为。</p>
</section>
<section>
<h2>五、知识产权</h2>
<p>1. 本应用的所有内容,包括但不限于文字、图片、音频、视频、软件、程序、版面设计等,均受知识产权法保护。</p>
<p>2. 未经我们书面许可,用户不得复制、传播、展示、镜像、上传、下载本应用的任何内容。</p>
</section>
<section>
<h2>六、隐私保护</h2>
<p>我们重视用户的隐私保护。关于我们如何收集、使用、存储和保护您的个人信息,请详见《隐私政策》。</p>
</section>
<section>
<h2>七、免责声明</h2>
<p>1. 用户明确同意使用本应用的风险由用户个人承担。</p>
<p>2. 我们不对因不可抗力或非我们原因造成的服务中断或终止承担责任。</p>
<p>3. 我们不对用户在使用本应用过程中产生的任何直接、间接、偶然、特殊及后续的损害承担责任。</p>
</section>
<section>
<h2>八、协议修改</h2>
<p>我们有权随时修改本协议的任何条款。一旦本协议的内容发生变动,我们将会通过适当方式向用户提示修改内容。如果用户不同意我们对本协议相关条款所做的修改,用户有权停止使用本应用。如果用户继续使用本应用,则视为用户接受我们对本协议相关条款所做的修改。</p>
</section>
<section>
<h2>九、法律适用与争议解决</h2>
<p>1. 本协议的订立、执行和解释及争议的解决均应适用中华人民共和国法律。</p>
<p>2. 如双方就本协议内容或其执行发生任何争议,双方应尽量友好协商解决;协商不成时,任何一方均可向我们所在地的人民法院提起诉讼。</p>
</section>
<section>
<h2>十、其他</h2>
<p>1. 本协议构成双方对本协议之约定事项及其他有关事宜的完整协议,除本协议规定的之外,未赋予本协议各方其他权利。</p>
<p>2. 如本协议中的任何条款无论因何种原因完全或部分无效或不具有执行力,本协议的其余条款仍应有效并且有约束力。</p>
</section>
<div class="update-time">
<p>最后更新时间2025年11月11日</p>
</div>
</div>
</div>
</body>
</html>

BIN
public/wechat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

BIN
public/xhs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

141
scripts/deploy-oss.js Normal file
View File

@ -0,0 +1,141 @@
import OSS from 'ali-oss'
import fs from 'fs'
import path from 'path'
import Core from '@alicloud/pop-core'
const {
OSS_REGION,
OSS_KEY_ID,
OSS_KEY_SECRET,
OSS_BUCKET,
} = process.env
if (!OSS_REGION || !OSS_KEY_ID || !OSS_KEY_SECRET || !OSS_BUCKET) {
console.error('❌ 缺少必要的环境变量,请检查 GitHub Secrets 配置')
process.exit(1)
}
const client = new OSS({
region: OSS_REGION,
accessKeyId: OSS_KEY_ID,
accessKeySecret: OSS_KEY_SECRET,
bucket: OSS_BUCKET
})
const cdnClient = new Core({
accessKeyId: OSS_KEY_ID,
accessKeySecret: OSS_KEY_SECRET,
endpoint: 'https://cdn.aliyuncs.com',
apiVersion: '2018-05-10'
})
// 遍历 dist 目录,统计文件
function getAllFiles(dir, fileList = []) {
const files = fs.readdirSync(dir)
for (const file of files) {
const filePath = path.join(dir, file)
const stat = fs.statSync(filePath)
if (stat.isDirectory()) {
getAllFiles(filePath, fileList)
} else {
fileList.push(filePath)
}
}
return fileList
}
// 上传文件,显示进度,可跳过指定目录
/**
* 上传文件并清理远端多余文件
* @param files 本地文件完整路径列表
* @param localBase 本地基准路径
* @param ignoreDirs 相对 localBase 的目录名数组上传时跳过删除远端时保留
*/
async function uploadFilesWithClean(files, localBase = './dist', ignoreDirs = []) {
// 1⃣ 过滤掉忽略的目录
const filteredFiles = files.filter(file => {
const relativePath = path.relative(localBase, file)
const topDir = relativePath.split(path.sep)[0]
return !ignoreDirs.includes(topDir)
})
// 2⃣ 获取远端已有文件列表
console.log('📄 获取远端文件列表...')
let remoteFiles = []
let marker = ''
do {
const result = await client.list({
prefix: '',
'max-keys': 1000,
marker,
})
if (result.objects) {
remoteFiles.push(...result.objects.map(f => f.name))
}
marker = result.nextMarker || ''
} while (marker)
// 3⃣ 上传文件
const total = filteredFiles.length
let count = 0
const uploadedFiles = []
for (const file of filteredFiles) {
const relativePath = path.relative(localBase, file)
const remotePath = relativePath.split(path.sep).join('/') // POSIX 路径
await client.put(remotePath, file)
uploadedFiles.push(remotePath)
count++
const percent = ((count / total) * 100).toFixed(1)
process.stdout.write(`\r📤 上传进度: ${count}/${total} (${percent}%) ${remotePath} `)
}
console.log('\n✅ 文件上传完成')
// 4⃣ 删除远端多余文件(远端存在但本地未上传),同时保留 ignoreDirs
const toDelete = remoteFiles.filter(f => {
const topDir = f.split('/')[0]
return !uploadedFiles.includes(f) && !ignoreDirs.includes(topDir)
})
if (toDelete.length) {
console.log('🗑 删除远端多余文件:', toDelete)
// 分批删除,防止数量过多
const batchSize = 1000
for (let i = 0; i < toDelete.length; i += batchSize) {
const batch = toDelete.slice(i, i + batchSize)
await client.deleteMulti(batch)
}
console.log('✅ 多余文件删除完成')
} else {
console.log(' 无需删除远端文件')
}
}
// 刷新 CDN
async function refreshCDN(domain) {
console.log(`🔄 刷新 ${domain} CDN 缓存...`)
const params = {
ObjectPath: `https://${domain}/`,
ObjectType: 'Directory'
}
const requestOption = {method: 'POST'}
const result = await cdnClient.request('RefreshObjectCaches', params, requestOption)
console.log('✅ CDN 刷新完成:', result)
}
async function main() {
const files = getAllFiles('./dist')
console.log(`📁 共找到 ${files.length} 个文件,开始上传...`)
await uploadFilesWithClean(files, './dist', ['dicts', 'sound', 'libs','imgs'])
// await uploadFilesWithClean(files, './dist', ['sound','libs','imgs'])
await refreshCDN('2study.top')
await refreshCDN('typewords.cc')
}
main().catch(err => {
console.error('❌ 部署失败:', err)
process.exit(1)
})

50
scripts/do.js Normal file
View File

@ -0,0 +1,50 @@
const {SitemapStream, streamToPromise} = require('sitemap')
const {createWriteStream} = require('fs')
const {resolve} = require('path')
const fs = require('fs')
async function generateSitemap() {
const bookList = require('../public/list/article.json')
const dictList = require('../public/list/word.json')
const SITE_URL = 'https://typewords.cc'
// 静态路由(首页、练习页等)
const staticPages = [
{url: '/index.html', changefreq: 'monthly', priority: 1.0},
{url: '/', changefreq: 'daily', priority: 1.0},
{url: '/words', changefreq: 'daily', priority: 0.9},
{url: '/articles', changefreq: 'daily', priority: 0.9},
{url: '/setting', changefreq: 'monthly', priority: 0.3},
]
// 动态页面示例(假设你有文章或单词数据)
const dynamicPages = bookList.flat().map(book => {
return {url: '/practice-articles/' + book.id, changefreq: 'weekly', priority: 0.8}
}).concat(dictList.flat().map(book => {
return {url: '/practice-words/' + book.id, changefreq: 'weekly', priority: 0.8}
}))
const sitemap = new SitemapStream({hostname: SITE_URL})
const writeStream = createWriteStream(resolve(__dirname, '../dist/sitemap.xml'))
sitemap.pipe(writeStream)
// 添加静态页
staticPages.forEach(page => sitemap.write(page))
// 添加动态页
dynamicPages.forEach(page => sitemap.write(page))
sitemap.end()
await streamToPromise(sitemap)
console.log('✅ sitemap.xml 已生成在 dist 目录')
}
function renameHtml() {
//首页为了seo被剥离出去了现在是一个静态页面用nginx 重定向控制对应的跳转
fs.renameSync('dist/index.html', 'dist/app.html')
fs.renameSync('dist/static-home.html', 'dist/index.html')
}
generateSitemap()
renameHtml()

60
scripts/gulpfile.esm.js Normal file
View File

@ -0,0 +1,60 @@
import { src, dest } from 'gulp';
import through from 'through2';
import * as XLSX from 'xlsx';
import * as path from 'path';
function excel2json() {
let json = {};
const stream = through.obj(function(file, encode, cb) {
if (!file.isBuffer()) {
return cb(null, file);
}
const workbook = XLSX.read(file.contents);
// const excelData = XLSX.utils.sheet_to_json(
// workbook.Sheets[workbook.SheetNames[0]],
// );
const excelData = XLSX.utils.sheet_to_json(workbook.Sheets['Sheet1']);
json = excelData.reduce((result, current) => {
let newCurrent = {};
for (var key in current) {
var letterPattern = /[a-zA-Z]+/g;
var matches = key.match(letterPattern);
if (matches) {
var string = matches[0].toLocaleLowerCase();
newCurrent[string] = current[key].replace(/@{/g, '{');
}
}
result[newCurrent.key] = {};
result[newCurrent.key]['en'] = newCurrent.en || '';
result[newCurrent.key]['zh'] = newCurrent.zh || '';
result[newCurrent.key]['id'] = newCurrent.id || '';
result[newCurrent.key]['tw'] = newCurrent.tw || '';
result[newCurrent.key]['th'] = newCurrent.th || '';
result[newCurrent.key]['ru'] = newCurrent.ru || '';
result[newCurrent.key]['vi'] = newCurrent.vi || '';
result[newCurrent.key]['es'] = newCurrent.es || '';
result[newCurrent.key]['pt'] = newCurrent.pt || '';
result[newCurrent.key]['ja'] = newCurrent.ja || '';
result[newCurrent.key]['uk'] = newCurrent.uk || '';
result[newCurrent.key]['ko'] = newCurrent.ko || '';
result[newCurrent.key]['de'] = newCurrent.de || '';
result[newCurrent.key]['fr'] = newCurrent.fr || '';
return result;
}, json);
file.contents = Buffer.from(JSON.stringify(json, null, '\t'));
file.path = path.join(file.base, 'i18n.json');
cb(null, file);
});
return stream;
}
// 将翻译好的excel写入json
function i18nwrite() {
return src(['./src/locales/i18n.xlsx'])
.pipe(excel2json())
.pipe(dest('src/locales'));
}
export { i18nwrite };

45
scripts/push-sitemap.js Normal file
View File

@ -0,0 +1,45 @@
const fs = require("fs");
const bookList = require('../public/list/article.json')
const dictList = require('../public/list/word.json')
async function pushUrls() {
// 配置区:改成你的
const site = "https://typewords.cc"; // 必须和百度站长平台注册的域名一致
const token = ""; // 在百度站长平台获取
// 读取 urls.txt每行一个 URL
let urls = bookList.flat().map(book => {
return site + '/practice-articles/' + book.id
}).concat(dictList.flat().map(book => {
return site + '/practice-words/' + book.id
})).concat([
site + '/words',
site + '/articles',
site + '/setting',
]).slice(7, 17)
if (urls.length === 0) {
console.error("❌ urls.txt 里没有 URL");
return;
}
console.log(`📄 读取到 ${urls.length} 个 URL准备推送到百度...`);
const api = `http://data.zz.baidu.com/urls?site=${site}&token=${token}`;
const body = urls.join("\n");
try {
const res = await fetch(api, {
method: "POST",
headers: {"Content-Type": "text/plain"},
body
});
const data = await res.json();
console.log("✅ 百度返回:", data);
} catch (err) {
console.error("❌ 推送失败:", err);
}
}
pushUrls();

136
src/App.vue Normal file
View File

@ -0,0 +1,136 @@
<script setup lang="ts">
import { onMounted, watch } from 'vue'
import { BaseState, useBaseStore } from '@/stores/base'
import { useRuntimeStore } from '@/stores/runtime'
import { useSettingStore } from '@/stores/setting'
import useTheme from '@/hooks/theme'
import { loadJsLib, shakeCommonDict } from '@/utils'
import { get, set } from 'idb-keyval'
import { useRoute } from 'vue-router'
import { APP_VERSION, AppEnv, DictId, LOCAL_FILE_KEY, Origin, SAVE_DICT_KEY, SAVE_SETTING_KEY } from '@/config/env'
import { syncSetting } from '@/apis'
import { useUserStore } from '@/stores/user'
import MigrateDialog from '@/components/MigrateDialog.vue'
const store = useBaseStore()
const runtimeStore = useRuntimeStore()
const settingStore = useSettingStore()
const userStore = useUserStore()
const { setTheme } = useTheme()
let lastAudioFileIdList = []
let isInitializing = true //
watch(store.$state, (n: BaseState) => {
//
if (isInitializing) return
let data = shakeCommonDict(n)
set(SAVE_DICT_KEY.key, JSON.stringify({ val: data, version: SAVE_DICT_KEY.version }))
//
let bookList = data.article.bookList.filter(v => v.custom || [DictId.articleCollect].includes(v.id))
let audioFileIdList = []
bookList.forEach(v => {
// audioFileId
v.articles
.filter(s => !s.audioSrc && s.audioFileId)
.forEach(a => {
// id watch
audioFileIdList.push(a.audioFileId)
})
})
if (audioFileIdList.toString() !== lastAudioFileIdList.toString()) {
let result = []
//使
get(LOCAL_FILE_KEY).then((fileList: Array<{ id: string; file: Blob }>) => {
if (fileList && fileList.length > 0) {
audioFileIdList.forEach(a => {
let item = fileList.find(b => b.id === a)
item && result.push(item)
})
set(LOCAL_FILE_KEY, result)
lastAudioFileIdList = audioFileIdList
}
})
}
})
watch(
() => settingStore.$state,
n => {
if (isInitializing) return
set(SAVE_SETTING_KEY.key, JSON.stringify({ val: n, version: SAVE_SETTING_KEY.version }))
if (AppEnv.CAN_REQUEST) {
syncSetting(null, settingStore.$state)
}
},
{ deep: true }
)
async function init() {
isInitializing = true //
await userStore.init()
await store.init()
await settingStore.init()
store.load = true
isInitializing = false //
setTheme(settingStore.theme)
if (settingStore.first) {
set(APP_VERSION.key, APP_VERSION.version)
} else {
get(APP_VERSION.key).then(r => {
runtimeStore.isNew = r ? APP_VERSION.version > Number(r) : true
})
}
window.umami?.track('host', { host: window.location.host })
}
onMounted(init)
//
let showTransfer = $ref(false)
onMounted(() => {
if (new URLSearchParams(window.location.search).get('from_old_site') === '1' && location.origin === Origin) {
if (localStorage.getItem('__migrated_from_2study_top__')) return
setTimeout(() => {
showTransfer = true
}, 1000)
}
})
// let transitionName = $ref('go')
// const route = useRoute()
// watch(() => route.path, (to, from) => {
// return transitionName = ''
// console.log('watch', to, from)
// //footer5
// let noAnimation = [
// '/pc/practice',
// '/pc/dict',
// '/mobile',
// '/'
// ]
// if (noAnimation.indexOf(from) !== -1 && noAnimation.indexOf(to) !== -1) {
// return transitionName = ''
// }
//
// const toDepth = routes.findIndex(v => v.path === to)
// const fromDepth = routes.findIndex(v => v.path === from)
// transitionName = toDepth > fromDepth ? 'go' : 'back'
// console.log('transitionName', transitionName, toDepth, fromDepth)
// })
</script>
<template>
<!-- <router-view v-slot="{ Component }">-->
<!-- <transition :name="transitionName">-->
<!-- <keep-alive :exclude="runtimeStore.excludeRoutes">-->
<!-- <component :is="Component"/>-->
<!-- </keep-alive>-->
<!-- </transition>-->
<!-- </router-view>-->
<router-view></router-view>
<MigrateDialog v-model="showTransfer" @ok="init" />
</template>

10
src/apis/dict.ts Normal file
View File

@ -0,0 +1,10 @@
import http from '@/utils/http.ts'
import type { Dict } from '@/types/types.ts'
export function copyOfficialDict(params?, data?) {
return http<Dict>('dict/copyOfficialDict', data, params, 'post')
}
export function deleteDict(params?, data?) {
return http<Dict>('dict/delete', data, params, 'post')
}

78
src/apis/index.ts Normal file
View File

@ -0,0 +1,78 @@
import http, {axiosInstance, AxiosResponse} from "@/utils/http";
import type { Dict } from "@/types/types.ts";
import { cloneDeep } from "@/utils";
function remove(data?: any) {
if (data) {
let s = cloneDeep(data)
delete s.words
delete s.articles
delete s.statistics
return s;
}
}
export function dictListVersion() {
return http<number>('dict/dictListVersion', null, null, 'get')
}
export function myDictList(params?) {
return http('dict/myDictList', null, params, 'get')
}
export function add2MyDict(data) {
return http<number>('dict/add2MyDict', remove(data), null, 'post')
}
export function addStat(data) {
return http('dict/addStat', data, null, 'post')
}
export function detail(params?, data?) {
return http<Dict>('dict/detail', data, params, 'get')
}
export function setUserDictProp(params?, data?) {
return http<Dict>('dict/setUserDictProp', remove(data), remove(params), 'post')
}
export function syncSetting(params?, data?) {
return http<Dict>('dict/syncSetting', remove(data), remove(params), 'post')
}
export function getSetting(params?, data?) {
return http<Dict>('dict/getSetting', remove(data), remove(params), 'get')
}
export function addDict(params?, data?) {
return http<Dict>('dict/addDict', remove(data), remove(params), 'post')
}
export function uploadImportData<T>(data, onUploadProgress): Promise<AxiosResponse<T>> {
return axiosInstance({
url: 'dict/uploadImportData',
method: 'post',
headers: {
contentType: 'formdata',
},
timeout: 1000000000,
data,
onUploadProgress
})
}
export function upload(data, onUploadProgress) {
return axiosInstance({
url: 'file/upload',
method: 'post',
headers: {
contentType: 'formdata',
},
data,
onUploadProgress
})
}
export function getProgress() {
return http<{ status: number; reason: string }>('dict/getProgress', null, null, 'get')
}

77
src/apis/member.ts Normal file
View File

@ -0,0 +1,77 @@
import http from '@/utils/http'
export type LevelBenefits = {
"level": {
"id": number,
"name": string,
"code": string,
"level": number,
"price": string,
"price_auto": string,
"yearly_price": string,
"description": string,
"color": string,
"icon": string,
"is_active": number,
"created_at": string,
"updated_at": string
},
"benefits": {
"code": string,
"name": string,
"type": boolean,
"unit": null,
"value": string
}[]
}
export type CouponInfo = {
"id": number,
"code": string,
"name": string,
"type": string,
"value"?: string,
"min_amount"?: string,
"max_discount"?: string,
"applicable_levels": {
code: string,
name: string,
level: string,
}[]
"usage_limit": number,
"total_usage": number,
"start_date": string
"end_date": string
"is_active": number,
"created_at": string
"updated_at": string
"is_valid": boolean,
}
export function levelBenefits(params) {
return http<LevelBenefits>('member/levelBenefits', null, params, 'get')
}
export function orderCreate(params) {
return http<{ orderNo: string, result: string, }>('/member/orderCreate', params, null, 'post')
}
export function alipayQuery(params) {
return http('/member/alipayQuery', null, params, 'get')
}
export function testPay() {
return http('/member/testPay', null, null, 'get')
}
export function orderStatus(params) {
return http('/member/orderStatus', null, params, 'get')
}
export function couponInfo(params) {
return http<CouponInfo>('/member/couponInfo', null, params, 'get')
}
export function setAutoRenewApi(params) {
return http('/member/setAutoRenew', params, null, 'post')
}

117
src/apis/user.ts Normal file
View File

@ -0,0 +1,117 @@
import http from '@/utils/http.ts'
import {CodeType} from "@/types/enum.ts";
// 用户登录接口
export interface LoginParams {
account?: string
password?: string
phone?: string
code?: string
type: 'code' | 'pwd'
}
export interface User {
id: string
email?: string
phone?: string
username?: string
avatar?: string,
hasPwd?: boolean,
member: {
levelDesc: string,
status: string,
active: boolean,
endDate: number,
autoRenew: boolean,
plan: string,
planDesc: string,
}
}
// 用户注册接口
export interface RegisterParams {
account: string
password: string
code: string
}
export interface RegisterResponse {
token: string
user: {
id: string
email?: string
phone: string
nickname?: string
avatar?: string
}
}
// 发送验证码接口
export interface SendCodeParams {
val: string
type: CodeType
}
// 重置密码接口
export interface ResetPasswordParams {
account: string
code: string
newPassword: string
}
// 微信登录接口
export interface WechatLoginParams {
code: string
state?: string
}
export function loginApi(params: LoginParams) {
return http<{ token:string }>('user/login', params, null, 'post')
}
export function registerApi(params: RegisterParams) {
return http<{ token:string }>('user/register', params, null, 'post')
}
export function sendCode(params: SendCodeParams) {
return http<boolean>('user/sendCode', null, params, 'get')
}
export function resetPasswordApi(params: ResetPasswordParams) {
return http<boolean>('user/resetPassword', params, null, 'post')
}
export function wechatLogin(params: WechatLoginParams) {
return http<User>('user/wechatLogin', params, null, 'post')
}
export function refreshToken() {
return http<{ token: string }>('user/refreshToken', null, null, 'post')
}
// 获取用户信息
export function getUserInfo() {
return http<User>('user/userInfo', null, null, 'get')
}
// 设置密码
export function setPassword(data) {
return http('user/setPassword', data, null, 'post')
}
// 修改邮箱
export function changeEmailApi(data) {
return http('user/changeEmail', data, null, 'post')
}
// 修改手机号
export function changePhoneApi(data) {
return http('user/changePhone', data, null, 'post')
}
// 修改用户信息
export function updateUserInfoApi(data) {
return http('user/updateUserInfo', data, null, 'post')
}

6
src/apis/words.ts Normal file
View File

@ -0,0 +1,6 @@
import http from '@/utils/http.ts'
import type { Dict } from '@/types/types.ts'
export function wordDelete(params?, data?) {
return http<Dict>('word/delete', data, params, 'post')
}

100
src/assets/css/anim.scss Normal file
View File

@ -0,0 +1,100 @@
.list-move, /* 对移动中的元素应用的过渡 */
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
/* 确保将离开的元素从布局流中删除
以便能够正确地计算移动的动画 */
.list-leave-active {
position: absolute;
}
.fade-enter-active,
.fade-leave-active {
transition: all .5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
@keyframes shake {
10%,
90% {
transform: translate3d(-1px, 0, 0);
}
20%,
80% {
transform: translate3d(2px, 0, 0);
}
30%,
50%,
70% {
transform: translate3d(-4px, 0, 0);
}
40%,
60% {
transform: translate3d(4px, 0, 0);
}
}
@keyframes shakeBottom {
10%,
90% {
transform: translate3d(-1px, 0.3rem, 0);
}
20%,
80% {
transform: translate3d(2px, 0.3rem, 0);
}
30%,
50%,
70% {
transform: translate3d(-4px, 0.3rem, 0);
}
40%,
60% {
transform: translate3d(4px, 0.3rem, 0);
}
}
.go-enter-from {
transform: translate3d(100%, 0, 0);
}
//最终状态
.back-enter-to, .back-enter-from, .go-enter-to, .go-leave-from {
transform: translate3d(0, 0, 0);
}
.go-leave-to {
transform: translate3d(-100%, 0, 0);
}
.go-enter-active, .go-leave-active, .back-enter-active, .back-leave-active {
transition: all .3s;
}
.back-enter-from {
transform: translate3d(-100%, 0, 0);
}
.back-leave-to {
transform: translate3d(100%, 0, 0);
}

473
src/assets/css/main.scss Normal file
View File

@ -0,0 +1,473 @@
@use 'anim' as *;
@use 'shepherd.css';
:root {
--color-reverse-white: white;
--color-reverse-black: black;
--bg-history: white;
--color-item-border: rgb(226, 226, 226);
--color-tooltip-bg: white;
--color-font-2: rgb(46, 46, 46);
--color-font-3: rgb(102, 116, 135);
--color-font-active-1: white;
--color-scrollbar: #c1c1c1;
--color-sub-gray: #c0bfbf;
--article-width: 50vw;
--article-toolbar-width: 50vw;
--article-panel-width: 20rem;
--article-panel-margin-left: calc(50vw + var(--article-width) / 2 + var(--aside-width) / 2 + 1rem);
--toolbar-width: 50rem;
--panel-width: 24rem;
--modal-padding: 1.3rem;
--space: 0.9rem;
--stat-gap: 1rem;
--word-panel-margin-left: calc(50vw + var(--aside-width) / 2 + var(--toolbar-width) / 2 + 1rem);
--anim-time: 0.5s;
--color-input-color: black;
--color-input-bg: white;
--color-input-border: #bfbfbf;
--color-input-icon: #d3d4d7;
--color-textarea-bg: white;
--color-article: black;
--aside-width: 12rem;
--font-family: -apple-system, sans-serif;
--word-font-family: ui-monospace, sans-serif;
--en-article-family: Georgia, sans-serif;
--zh-article-family: 'Songti SC', 'SimSun', 'Noto Serif CJK SC', serif;
--color-primary: #e6e8eb;
--color-second: rgb(247, 247, 247);
--color-third: rgb(228, 230, 232);
--color-fourth: rgb(218, 220, 222);
--color-fifth: rgb(253, 246, 236);
//--color-card-active: #FED7AA;
--color-card-active: rgb(253, 246, 236);
--color-icon-hightlight: rgb(12, 140, 233);
//--color-icon-hightlight: rgb(12, 140, 233);
--color-sub-text: gray;
--color-main-text: rgb(91, 91, 91);
--color-select-bg: rgb(12, 140, 233);
--color-progress-bar: #d1d5df;
--color-label-bg: whitesmoke;
--color-link: #2563eb;
--color-card-bg: white;
--bg-card-primary: white;
--bg-card-secend: rgb(247, 247, 247);
--bg-book: rgb(226 232 240);
--color-line: rgb(226, 226, 226);
--color-translate-main: black;
--color-translate-second: #818181;
}
html.dark {
--color-reverse-white: black;
--color-reverse-black: white;
--color-primary: #202124;
--color-second: #292a2d;
--color-third: #35373a;
--color-fourth: rgb(70, 70, 70);
--color-fifth: rgb(84, 84, 84);
--color-card-active: rgb(84, 84, 84);
--color-icon-hightlight: rgb(147, 173, 227);
--color-sub-text: #b8b8b8;
--color-main-text: rgba(249, 250, 251, 0.8);
--color-select-bg: rgb(147, 173, 227);
--bg-history: rgb(43, 45, 48);
--color-item-border: rgb(41, 41, 41);
--color-tooltip-bg: #35373a;
--color-font-2: rgba(255, 255, 255, 0.5);
--color-font-3: rgba(255, 255, 255, 0.3);
--color-sub-gray: #383737;
--color-scrollbar: rgb(92, 93, 94);
--color-input-color: white;
--color-input-bg: var(--color-third);
--color-textarea-bg: rgb(43, 45, 48);
--color-article: white;
--color-progress-bar: rgb(73, 77, 82) !important;
--color-label-bg: rgb(10, 10, 10);
--color-card-bg: var(--color-second);
--bg-card-primary: rgb(30, 31, 34);
--bg-card-secend: rgb(43, 45, 48);
--bg-book: #35373a;
--color-line: rgb(66, 66, 66);
}
@media (max-width: 1720px) {
:root {
--toolbar-width: 50vw;
--panel-width: 20rem;
--space: 0.5rem;
--article-width: 60vw;
--article-toolbar-width: 60vw;
--article-panel-width: 17rem;
}
}
@media (max-width: 1560px) {
:root {
--article-width: 60vw;
--article-toolbar-width: 60vw;
--article-panel-width: 15rem;
}
}
@media (max-width: 1366px) {
:root {
--panel-width: 20vw;
--toolbar-width: 50vw;
--stat-gap: 0.5rem;
--space: 0.3rem;
--article-width: 70vw;
--article-toolbar-width: 70vw;
--article-panel-width: 13rem;
}
}
@media (max-width: 1150px) {
:root {
--article-width: 85vw;
--article-toolbar-width: 85vw;
--article-panel-width: 16rem;
}
}
// 移动端适配
@media (max-width: 768px) {
:root {
--toolbar-width: 100vw;
--panel-width: 100vw;
--article-width: 100vw;
--article-toolbar-width: 100vw;
--space: 0.5rem;
--stat-gap: 0.3rem;
--article-panel-width: 100vw;
--word-panel-margin-left: 0;
}
}
@media (max-width: 480px) {
:root {
--space: 0.3rem;
--stat-gap: 0.2rem;
}
}
.anim {
transition: background var(--anim-time),
color var(--anim-time),
border var(--anim-time),
opacity var(--anim-time);
}
.en-article-family {
font-family: var(--en-article-family);
}
.font-family {
font-family: var(--font-family);
}
html,
body {
//font-size: 1px;
@apply p-0 m-0 overflow-x-hidden;
color: var(--color-main-text);
font-family: var(--font-family);
background: var(--color-primary);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.page {
@apply relative z-1 h-full w-full flex flex-col;
}
.mobile-page {
@apply fixed left-0 right-0 bottom-0 top-0 overflow-auto font-size-18 w-full h-full flex flex-col;
& > .page-content {
@apply p-10 box-border overflow-auto;
}
}
#app {
width: 100vw;
min-height: 100vh;
}
a {
color: var(--color-link);
text-decoration: none;
}
.link {
color: var(--color-link);
@apply hover:opacity-80;
}
.cp {
@apply cursor-pointer;
}
@supports selector(::-webkit-scrollbar) {
::-webkit-scrollbar {
width: 0.5rem;
height: 0.6rem;
}
::-webkit-scrollbar-track {
background: transparent;
border-radius: 0.1rem;
}
::-webkit-scrollbar-thumb {
background: var(--color-scrollbar);
border-radius: 0.6rem;
}
}
.panel-page-item {
@apply flex flex-col h-full box-border;
padding-bottom: var(--space);
}
.scroll {
padding: 0 var(--space);
@apply flex-1 overflow-auto;
}
.virtual-list {
@apply overflow-overlay h-full;
padding: 0 var(--space);
}
.list-item-wrapper {
padding-bottom: 1rem;
}
.common-list-item {
@apply cursor-pointer w-full box-border bg-third rounded-lg flex justify-between gap-1 transition-all duration-300 p-2;
color: var(--color-main-text);
border: 1px solid var(--color-item-border);
.left {
@apply flex gap-1;
.title-wrapper {
@apply flex flex-col gap-0.5 word-break-break-word;
}
}
.right {
@apply flex flex-col gap-0.5 transition-all duration-300;
}
svg {
@apply opacity-0;
}
&.active {
@apply bg-fifth;
.item-sub-title {
color: var(--color-sub-text);
}
}
.fill {
svg {
opacity: 1 !important;
}
}
&:hover {
@extend .active;
svg {
opacity: 1;
}
}
.item-title {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--color-main-text);
flex-wrap: wrap;
span {
flex-shrink: 0;
}
.word {
font-size: 1.2rem;
}
.phonetic {
font-size: 0.9rem;
color: gray;
}
}
.item-sub-title {
font-size: 1rem;
color: gray;
}
}
.word-shadow {
color: transparent !important;
text-shadow: #b0b0b0 0 0 0.5rem;
user-select: none;
}
.common-title {
min-height: 2.8rem;
font-size: 1.1rem;
color: var(--color-main-text);
display: flex;
justify-content: center;
align-items: center;
}
.slide {
flex: 1;
width: 100%;
transition: height 0.3s;
position: relative;
overflow: hidden;
.slide-infinite {
z-index: 1;
margin-top: 0;
transition: all 0.3s;
}
.slide-list {
height: 100%;
width: 100%;
display: flex;
position: relative;
}
}
.container2 {
@apply w-5/10 pt-5;
}
.center {
@apply flex justify-center items-center;
}
.center-col {
@extend .center;
@apply flex-col;
}
.card-white {
@apply card;
background: var(--color-card-bg);
}
.inline-center {
@apply inline-flex justify-center items-center;
}
.title {
@apply text-lg font-medium;
}
.book {
@extend .anim;
@apply p-3 rounded-md relative cursor-pointer hover:bg-fifth flex flex-col justify-between shrink-0;
background: var(--bg-book);
$w: 7rem;
width: $w;
height: calc($w * 1.4);
}
.line {
width: 100%;
border-bottom: 1px solid var(--color-line);
@apply hover:text-blue-700;
}
.line-white {
width: 100%;
border-bottom: 1px dashed #ababab;
}
.page-title {
@apply text-2xl;
}
.red-point {
@apply bg-red w-3 h-3 rounded-full absolute right-3;
}
.cursor {
position: absolute;
left: 0;
top: 0;
height: 1.8rem;
animation: underline 1s infinite steps(1, start);
}
@keyframes underline {
0%,
100% {
border-left: 0.1rem solid var(--color-article);
}
50% {
border-left: 0.1rem solid transparent;
}
}
#typing-listener {
@apply fixed left-[-9999px] top-[-9999px] w-1 h-1 opacity-0.01 z-[-1] pointer-events-none border-none outline-none bg-transparent text-transparent;
font-size: 16px; // 防止iOS缩放
}
.btn-no-margin {
.base-button + .base-button {
margin-left: 0 !important;
}
}
.target-number {
@apply text-3xl!;
color: rgb(176, 116, 211) !important;
}

View File

@ -0,0 +1,10 @@
.shepherd-button{background:#3288e6;border:0;border-radius:3px;color:hsla(0,0%,100%,.75);cursor:pointer;margin-right:.5rem;padding:.5rem 1.5rem;transition:all .5s ease}.shepherd-button:not(:disabled):hover{background:#196fcc;color:hsla(0,0%,100%,.75)}.shepherd-button.shepherd-button-secondary{background:#f1f2f3;color:rgba(0,0,0,.75)}.shepherd-button.shepherd-button-secondary:not(:disabled):hover{background:#d6d9db;color:rgba(0,0,0,.75)}.shepherd-button:disabled{cursor:not-allowed}
.shepherd-footer{border-bottom-left-radius:5px;border-bottom-right-radius:5px;display:flex;justify-content:flex-end;padding:0 .75rem .75rem}.shepherd-footer .shepherd-button:last-child{margin-right:0}
.shepherd-cancel-icon{background:transparent;border:none;color:hsla(0,0%,50%,.75);cursor:pointer;font-size:2em;font-weight:400;margin:0;padding:0;transition:color .5s ease}.shepherd-cancel-icon:hover{color:rgba(0,0,0,.75)}.shepherd-has-title .shepherd-content .shepherd-cancel-icon{color:hsla(0,0%,50%,.75)}.shepherd-has-title .shepherd-content .shepherd-cancel-icon:hover{color:rgba(0,0,0,.75)}
.shepherd-title{color:rgba(0,0,0,.75);display:flex;flex:1 0 auto;font-size:1rem;font-weight:400;margin:0;padding:0}
.shepherd-header{align-items:center;border-top-left-radius:5px;border-top-right-radius:5px;display:flex;justify-content:flex-end;line-height:2em;padding:.75rem .75rem 0}.shepherd-has-title .shepherd-content .shepherd-header{background:#e6e6e6;padding:1em}
.shepherd-text{color:rgba(0,0,0,.75);font-size:1rem;line-height:1.3em;padding:.75em}.shepherd-text p{margin-top:0}.shepherd-text p:last-child{margin-bottom:0}
.shepherd-content{border-radius:5px;outline:none;padding:0}
.shepherd-element{background:#fff;border:none;border-radius:5px;box-shadow:0 1px 4px rgba(0,0,0,.2);margin:0;max-width:400px;opacity:0;outline:none;padding:0;transition:opacity .3s,visibility .3s;visibility:hidden;width:100%;z-index:9999}.shepherd-enabled.shepherd-element{opacity:1;visibility:visible}.shepherd-element[data-popper-reference-hidden]:not(.shepherd-centered){opacity:0;pointer-events:none;visibility:hidden}.shepherd-element,.shepherd-element *,.shepherd-element :after,.shepherd-element :before{box-sizing:border-box}.shepherd-arrow,.shepherd-arrow:before{height:16px;position:absolute;width:16px;z-index:-1}.shepherd-arrow:before{background:#fff;content:"";transform:rotate(45deg)}.shepherd-element[data-popper-placement^=top]>.shepherd-arrow{bottom:-8px}.shepherd-element[data-popper-placement^=bottom]>.shepherd-arrow{top:-8px}.shepherd-element[data-popper-placement^=left]>.shepherd-arrow{right:-8px}.shepherd-element[data-popper-placement^=right]>.shepherd-arrow{left:-8px}.shepherd-element.shepherd-centered>.shepherd-arrow{opacity:0}.shepherd-element.shepherd-has-title[data-popper-placement^=bottom]>.shepherd-arrow:before{background-color:#e6e6e6}.shepherd-target-click-disabled.shepherd-enabled.shepherd-target,.shepherd-target-click-disabled.shepherd-enabled.shepherd-target *{pointer-events:none}
.shepherd-modal-overlay-container{height:0;left:0;opacity:0;overflow:hidden;pointer-events:none;position:fixed;top:0;transition:all .3s ease-out,height 0s .3s,opacity .3s 0s;width:100vw;z-index:9997}.shepherd-modal-overlay-container.shepherd-modal-is-visible{height:100vh;opacity:.5;transform:translateZ(0);transition:all .3s ease-out,height 0s 0s,opacity .3s 0s}.shepherd-modal-overlay-container.shepherd-modal-is-visible path{pointer-events:all}

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1698646880606" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1765" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M573.44 389.12V245.76H358.4c-28.16 0-51.2 23.04-51.2 51.2v460.8c0 28.16 23.04 51.2 51.2 51.2h307.2c28.16 0 51.2-23.04 51.2-51.2V389.12h-143.36z m43.52 101.376L502.784 629.76l-21.504-20.48-21.504 20.992v-41.984l133.12-85.504-147.456 71.68-38.4-35.84v-4.608l209.92-48.128v4.608z" fill="#4A68CC" opacity=".2" p-id="1766"></path><path d="M102.4 896a409.6 51.2 0 1 0 819.2 0 409.6 51.2 0 1 0-819.2 0Z" fill="#4A68CC" opacity=".1" p-id="1767"></path><path d="M96.256 525.312c0 8.704 6.656 15.36 15.36 15.36s15.36-6.656 15.36-15.36-6.656-15.36-15.36-15.36c-8.192 0-15.36 7.168-15.36 15.36zM147.456 822.272c0 8.704 6.656 15.36 15.36 15.36s15.36-6.656 15.36-15.36-6.656-15.36-15.36-15.36c-8.192 0-15.36 7.168-15.36 15.36zM926.72 847.872c-15.36 4.096-18.944 7.168-23.04 23.04-4.096-15.36-7.168-18.944-23.04-23.04 15.36-4.096 18.944-7.68 23.04-23.04 4.096 15.36 7.168 18.944 23.04 23.04zM199.68 467.456c-31.232 8.192-37.888 14.848-46.08 46.08-8.192-31.232-14.848-37.888-46.08-46.08 31.232-8.192 37.888-14.848 46.08-46.08 8.192 31.232 14.848 37.888 46.08 46.08zM821.76 214.528c-15.36 4.096-18.432 7.168-22.528 22.528-4.096-15.36-7.168-18.432-22.528-22.528 15.36-4.096 18.432-7.168 22.528-22.528 3.584 15.36 7.168 18.432 22.528 22.528zM882.688 135.68c-39.936 10.24-48.128 18.944-58.88 58.88-10.24-39.936-18.944-48.128-58.88-58.88 39.936-10.24 48.128-18.944 58.88-58.88 10.24 39.424 18.944 48.128 58.88 58.88zM783.36 337.408c0-1.024 0-2.048-0.512-2.56v-0.512c0-0.512-0.512-1.536-0.512-2.048 0-0.512 0-0.512-0.512-1.024s-0.512-1.024-1.024-1.536l-0.512-0.512c-0.512-0.512-1.024-1.536-1.536-2.048l-143.36-143.36c-0.512-0.512-1.024-1.024-2.048-1.536l-0.512-0.512c-0.512-0.512-1.024-0.512-1.536-1.024-0.512 0-0.512 0-1.024-0.512s-1.536-0.512-2.048-0.512h-0.512c-1.024 0-2.048-0.512-2.56-0.512H358.4c-65.024 0-117.76 52.736-117.76 117.76v460.8c0 65.024 52.736 117.76 117.76 117.76h307.2c65.024 0 117.76-52.736 117.76-117.76V337.408c0 0.512 0 0.512 0 0z m-143.36-105.984L731.136 322.56H640V231.424z m25.6 613.376H358.4c-48.128 0-87.04-38.912-87.04-87.04V296.96c0-48.128 38.912-87.04 87.04-87.04h250.88v128c0 8.704 6.656 15.36 15.36 15.36h128v404.48c0 48.128-38.912 87.04-87.04 87.04z" fill="#4A68CC" opacity=".5" p-id="1768"></path></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

40
src/components/About.vue Normal file
View File

@ -0,0 +1,40 @@
<script setup lang="ts">
import { GITHUB } from "@/config/env.ts";
import ChannelIcons from "@/components/ChannelIcons/ChannelIcons.vue";
import WeChat from "@/components/ChannelIcons/WeChat.vue";
</script>
<template>
<h1>Type Words</h1>
<div class="w-120">
<p class="text-xl">
感谢使用本项目本项目是开源项目如果觉得有帮助请在 GitHub 点个 Star您的支持是我持续改进的动力
</p>
<p>
GitHub地址<a :href="GITHUB" target="_blank">{{ GITHUB }}</a>
</p>
<div class="flex flex-col gap-2 mt-20">
<div class="flex items-center">
微信反馈<WeChat/>
</div>
<div class="">
工单反馈<a :href="`https://v.wjx.cn/vm/ev0W7fv.aspx#`"
target="_blank">https://v.wjx.cn/vm/ev0W7fv.aspx#</a>
</div>
<div class="flex items-center">
其他渠道
<ChannelIcons type="horizontal"
:share="false"
:wechat="false"
:github="false"
/>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,28 @@
<script setup lang="ts">
import BaseIcon from "@/components/BaseIcon.vue";
import { useAttrs } from "vue";
import { useNav } from "@/utils";
const attrs = useAttrs()
const router = useNav()
function onClick() {
if (!attrs.onClick) {
router.back()
}
}
</script>
<template>
<BaseIcon
title="返回"
@click="onClick"
>
<IconFluentChevronLeft28Filled/>
</BaseIcon>
</template>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,146 @@
<script setup lang="ts">
import Tooltip from '@/components/base/Tooltip.vue'
interface IProps {
keyboard?: string
active?: boolean
disabled?: boolean
loading?: boolean
size?: 'small' | 'normal' | 'large'
type?: 'primary' | 'info' | 'orange'
}
withDefaults(defineProps<IProps>(), {
type: 'primary',
size: 'normal',
})
defineEmits(['click'])
</script>
<template>
<Tooltip :disabled="!keyboard" :title="`${keyboard}`">
<div
class="base-button"
v-bind="$attrs"
@click="e => !disabled && !loading && $emit('click', e)"
:class="[active && 'active', size, type, (disabled || loading) && 'disabled']"
>
<span :style="{ opacity: loading ? 0 : 1 }"><slot></slot></span>
<IconEosIconsLoading v-if="loading" class="loading" width="18" :color="type === 'info' ? '#000000' : '#ffffff'" />
</div>
</Tooltip>
</template>
<style>
:root {
--btn-primary: rgb(75, 85, 99);
--btn-primary-disabled: #90969e;
--btn-primary-hover: rgb(105, 121, 143);
--btn-info: white;
--btn-info-hover: #eaeaea;
--btn-orange: #facc15;
--btn-orange-hover: #bfac61;
}
html.dark {
--btn-info: #1b1b1b;
--btn-info-hover: #3a3a3a;
}
</style>
<style scoped lang="scss">
.base-button {
cursor: pointer;
box-sizing: border-box;
display: inline-flex;
align-items: center;
justify-content: center;
outline: none;
text-align: center;
transition: all 0.3s;
user-select: none;
vertical-align: middle;
white-space: nowrap;
border-radius: 0.3rem;
padding: 0 0.9rem;
font-size: 0.9rem;
height: 2rem;
color: white;
& + .base-button {
margin-left: 1rem;
}
&.disabled {
opacity: 0.6;
cursor: not-allowed;
user-select: none;
color: rgba(#fff, 0.4);
}
.loading {
position: absolute;
}
&.small {
border-radius: 0.3rem;
padding: 0 0.6rem;
height: 1.6rem;
font-size: 0.8rem;
}
&.large {
padding: 0 1.3rem;
height: 2.4rem;
font-size: 0.9rem;
border-radius: 0.5rem;
}
& > span {
line-height: 1;
transform: translateY(-5%);
:deep(a) {
color: white;
}
}
&.primary {
background: var(--btn-primary);
&.disabled {
opacity: 1;
background: var(--btn-primary-disabled);
}
&:hover:not(.disabled) {
background: var(--btn-primary-hover);
}
}
&.info {
background: var(--btn-info);
border: 1px solid var(--color-main-text);
color: var(--color-main-text);
&:hover:not(.disabled) {
background: var(--btn-info-hover);
}
}
&.orange {
background: var(--btn-orange);
color: black;
&:hover:not(.disabled) {
background: var(--btn-orange-hover);
color: rgba(0, 0, 0, 0.6);
}
}
&.active {
opacity: 0.4;
}
}
</style>

View File

@ -0,0 +1,62 @@
<script setup lang="ts">
import Tooltip from "@/components/base/Tooltip.vue";
defineProps<{
title?: string,
disabled?: boolean,
noBg?: boolean,
}>()
const emit = defineEmits(['click'])
</script>
<template>
<Tooltip :title="title">
<div
v-bind="$attrs"
@click="e => (!disabled) && emit('click',e)"
class="icon-wrapper"
:class="{disabled,noBg}"
>
<slot/>
</div>
</Tooltip>
</template>
<style scoped lang="scss">
$w: 1.4rem;
.icon-wrapper {
cursor: pointer;
//padding: 2rem;
width: 2rem;
height: 2rem;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: .3rem;
background: transparent;
transition: all .3s;
&:hover:not(.disabled,.noBg) {
background: var(--color-icon-hightlight);
color: white;
:deep(svg) {
color: white;
}
}
&.disabled {
cursor: not-allowed;
opacity: .3;
}
:deep(svg) {
width: $w;
height: $w;
}
}
</style>

View File

@ -0,0 +1,38 @@
<script setup lang="ts">
</script>
<template>
<div class="flex justify-center">
<div class="page 3xl:w-[50vw] 2xl:w-[60vw] xl:w-[70vw] lg:w-[75vw]">
<slot></slot>
</div>
</div>
</template>
<style scoped lang="scss">
.page {
min-height: calc(100vh - 1.2rem);
margin-top: 1.2rem;
}
//
@media (max-width: 768px) {
.page {
width: 100vw !important;
margin-top: 0.5rem;
min-height: calc(100vh - 0.5rem);
padding: 0 0.5rem;
box-sizing: border-box;
}
}
//
@media (max-width: 480px) {
.page {
margin-top: 0.3rem;
min-height: calc(100vh - 0.3rem);
padding: 0 0.3rem;
}
}
</style>

View File

@ -0,0 +1,344 @@
<script setup lang="tsx">
import { nextTick, onMounted, useSlots } from 'vue'
import MiniDialog from '@/components/dialog/MiniDialog.vue'
import BaseIcon from '@/components/BaseIcon.vue'
import BaseButton from '@/components/BaseButton.vue'
import { debounce } from '@/utils'
import PopConfirm from '@/components/PopConfirm.vue'
import Empty from '@/components/Empty.vue'
import Pagination from '@/components/base/Pagination.vue'
import Checkbox from '@/components/base/checkbox/Checkbox.vue'
import DeleteIcon from '@/components/icon/DeleteIcon.vue'
import Dialog from '@/components/dialog/Dialog.vue'
import BaseInput from '@/components/base/BaseInput.vue'
import { Host } from '@/config/env.ts'
import { Sort } from '@/types/enum.ts'
const props = withDefaults(
defineProps<{
loading?: boolean
showToolbar?: boolean
showCheckbox?: boolean
showPagination?: boolean
exportLoading?: boolean
importLoading?: boolean
request?: Function
list?: any[]
}>(),
{
loading: true,
showCheckbox: false,
showToolbar: true,
showPagination: true,
exportLoading: false,
importLoading: false,
}
)
const emit = defineEmits<{
add: []
click: [
val: {
item: any
index: number
},
]
import: [e: Event]
export: []
del: [ids: number[]]
sort: [type: Sort, pageNo: number, pageSize: number]
}>()
let listRef: any = $ref()
let showCheckbox = $ref(false)
function scrollToBottom() {
nextTick(() => {
listRef?.scrollTo(0, listRef.scrollHeight)
})
}
function scrollToTop() {
nextTick(() => {
listRef?.scrollTo(0, 0)
})
}
function scrollToItem(index: number) {
nextTick(() => {
listRef?.children[index]?.scrollIntoView({ block: 'center', behavior: 'smooth' })
})
}
let selectIds = $ref([])
let selectAll = $computed(() => {
return !!selectIds.length
})
function toggleSelect(item) {
let rIndex = selectIds.findIndex(v => v === item.id)
if (rIndex > -1) {
selectIds.splice(rIndex, 1)
} else {
selectIds.push(item.id)
}
}
function toggleSelectAll() {
if (selectAll) {
selectIds = []
} else {
selectIds = params.list.map(v => v.id)
}
}
let showSortDialog = $ref(false)
let showSearchInput = $ref(false)
let showImportDialog = $ref(false)
const closeImportDialog = () => (showImportDialog = false)
function sort(type: Sort) {
if ([Sort.reverse, Sort.random].includes(type)) {
emit('sort', type, params.pageNo, params.pageSize)
} else {
emit('sort', type, 1, params.total)
}
showSortDialog = false
}
function handleBatchDel() {
emit('del', selectIds)
selectIds = []
}
const s = useSlots()
defineExpose({
scrollToBottom,
scrollToItem,
closeImportDialog,
getData,
})
let loading2 = $ref(false)
let params = $ref({
pageNo: 1,
pageSize: 50,
total: 0,
list: [],
sortType: null,
searchKey: '',
})
function search(key: string) {
if (!params.searchKey) {
params.pageNo = 1
}
params.searchKey = key
getData()
}
function cancelSearch() {
params.searchKey = ''
showSearchInput = false
getData()
}
async function getData() {
if (props.request) {
loading2 = true
let { list, total } = await props.request(params)
params.list = list
params.total = total
loading2 = false
} else {
params.list = props.list ?? []
}
}
function handlePageNo(e) {
params.pageNo = e
getData()
scrollToTop()
}
onMounted(async () => {
getData()
})
defineRender(() => {
const d = item => (
<Checkbox modelValue={selectIds.includes(item.id)} onChange={() => toggleSelect(item)} size="large" />
)
return (
<div class="flex flex-col gap-3">
{props.showToolbar && (
<div>
{showSearchInput ? (
<div class="flex gap-4">
<BaseInput
clearable
modelValue={params.searchKey}
onUpdate:modelValue={debounce(e => search(e), 500)}
class="flex-1"
autofocus
>
{{
subfix: () => <IconFluentSearch24Regular class="text-lg text-gray" />,
}}
</BaseInput>
<BaseButton onClick={cancelSearch}>取消</BaseButton>
</div>
) : (
<div class="flex justify-between items-center">
{showCheckbox ? (
<div class="flex gap-2 items-center">
<Checkbox
disabled={!params.list.length}
onChange={() => toggleSelectAll()}
modelValue={selectAll}
size="large"
/>
<span>
{selectIds.length} / {params.total}
</span>
</div>
) : <div>{params.total}</div>}
<div class="flex gap-2 relative">
{selectIds.length && showCheckbox ? (
<PopConfirm title="确认删除所有选中数据?" onConfirm={handleBatchDel}>
<BaseButton type="info">确认</BaseButton>
</PopConfirm>
) : null}
<BaseIcon onClick={() => (showCheckbox = !showCheckbox)} title="批量删除">
<DeleteIcon />
</BaseIcon>
<BaseIcon onClick={() => (showImportDialog = true)} title="导入">
<IconSystemUiconsImport />
</BaseIcon>
<BaseIcon onClick={() => emit('export')} title="导出">
{props.exportLoading ? <IconEosIconsLoading /> : <IconPhExportLight />}
</BaseIcon>
<BaseIcon onClick={() => emit('add')} title="添加单词">
<IconFluentAdd20Regular />
</BaseIcon>
<BaseIcon
disabled={!params.list.length}
title="改变顺序"
onClick={() => (showSortDialog = !showSortDialog)}
>
<IconFluentArrowSort20Regular />
</BaseIcon>
<BaseIcon
disabled={!params.list.length}
onClick={() => (showSearchInput = !showSearchInput)}
title="搜索"
>
<IconFluentSearch20Regular />
</BaseIcon>
<MiniDialog
modelValue={showSortDialog}
onUpdate:modelValue={e => (showSortDialog = e)}
style="width: 8rem;"
>
<div class="mini-row-title">列表顺序设置</div>
<div class="flex flex-col gap2 btn-no-margin">
<BaseButton onClick={() => sort(Sort.reverse)}>翻转当前页</BaseButton>
<BaseButton onClick={() => sort(Sort.reverseAll)}>翻转所有</BaseButton>
<div class="line"></div>
<BaseButton onClick={() => sort(Sort.random)}>随机当前页</BaseButton>
<BaseButton onClick={() => sort(Sort.randomAll)}>随机所有</BaseButton>
</div>
</MiniDialog>
</div>
</div>
)}
</div>
)}
<div class="relative flex-1 overflow-hidden">
{params.list.length ? (
<div class="overflow-auto h-full" ref={e => (listRef = e)}>
{params.list.map((item, index) => {
return (
<div class="list-item-wrapper" key={item.word}>
{s.default({
checkbox: showCheckbox ? d : () => void 0,
item,
index: params.pageSize * (params.pageNo - 1) + index + 1,
})}
</div>
)
})}
</div>
) : !loading2 ? (
<Empty />
) : null}
{loading2 && (
<div class="absolute top-0 left-0 bottom-0 right-0 bg-black bg-op-10 center text-4xl">
<IconEosIconsLoading color="gray" />
</div>
)}
</div>
{props.showPagination && (
<div class="flex justify-end">
<Pagination
currentPage={params.pageNo}
onUpdate:current-page={handlePageNo}
pageSize={params.pageSize}
onUpdate:page-size={e => (params.pageSize = e)}
pageSizes={[20, 50, 100, 200]}
layout="total,sizes"
total={params.total}
/>
</div>
)}
<Dialog modelValue={showImportDialog} onUpdate:modelValue={closeImportDialog} title="导入教程">
<div className="w-100 p-4 pt-0">
<div>请按照模板的格式来填写数据</div>
<div class="color-red">单词项为必填其他项可不填</div>
<div>翻译一行一个翻译前面词性后面内容如n.取消多个翻译请换行</div>
<div>
例句一行原文一行译文多个请换<span class="color-red"></span>
</div>
<div>
短语一行原文一行译文多个请换<span class="color-red"></span>
</div>
<div>同义词同根词词源请前往官方词典然后编辑其中某个单词参考其格式</div>
<div class="mt-6">
模板下载地址<a href={`https://${Host}/libs/单词导入模板.xlsx`}>单词导入模板</a>
</div>
<div class="mt-4">
<BaseButton
onClick={() => {
let d: HTMLDivElement = document.querySelector('#upload-trigger')
d.click()
}}
loading={props.importLoading}
>
导入
</BaseButton>
<input
id="upload-trigger"
type="file"
accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
onChange={e => emit('import', e)}
class="w-0 h-0 opacity-0"
/>
</div>
</div>
</Dialog>
</div>
)
})
</script>
<style scoped lang="scss"></style>

83
src/components/Book.vue Normal file
View File

@ -0,0 +1,83 @@
<script setup lang="ts">
import type { Dict } from "@/types/types";
import Progress from '@/components/base/Progress.vue'
import Checkbox from "@/components/base/checkbox/Checkbox.vue";
interface IProps {
item?: Partial<Dict>;
quantifier?: string
isAdd: boolean
showCheckbox?: boolean
checked?: boolean
showProgress?: boolean
isUser?: boolean //
}
const props = withDefaults(defineProps<IProps>(), {
showProgress: true,
isUser: false
})
defineEmits<{
check: []
}>()
const progress = $computed(() => {
return Number(((props.item?.lastLearnIndex / props.item?.length) * 100).toFixed())
})
const studyProgress = $computed(() => {
if (!props.showProgress) return
return props.item?.lastLearnIndex ? props.item?.lastLearnIndex + '/' : ''
})
</script>
<template>
<div :id="item?.id" v-if="!isAdd">
<div class="book overflow-hidden relative">
<img class="absolute top-0 left-0 w-full object-cover" v-if="item?.cover" :src='item.cover' alt=""/>
<div class="text-base mt-1" v-else>{{ item?.name }}</div>
<div class="absolute bottom-4 right-3 z-1" :class="item?.cover && 'color-white'">
<div>{{ studyProgress }}{{ item?.length }}{{ quantifier }}</div>
</div>
<div class="absolute bottom-2 left-3 right-3">
<Progress v-if="(item?.lastLearnIndex) && showProgress" class="mt-1"
:percentage="progress"
:show-text="false"></Progress>
</div>
<Checkbox v-if="showCheckbox"
:model-value="checked"
@change="$emit('check')"
class="absolute left-3 bottom-3 z-2"/>
<div class="custom z-1" v-if="item.custom">自定义</div>
<!-- <div class="custom bg-red! color-white z-1" v-else-if="item.update">更新中</div>-->
<!-- <div class="sync bg-red! color-white z-1" v-if="!item.sync && isUser && !showCheckbox">未同步</div>-->
</div>
<div class="text-base mt-1" v-if="item?.cover">{{ item?.name }}</div>
</div>
<div v-else class="book" id="no-book">
<div class="h-full center text-2xl">
<IconFluentAdd16Regular/>
</div>
</div>
</template>
<style scoped lang="scss">
.custom {
position: absolute;
top: 4px;
right: -22px;
padding: 1px 20px;
background: var(--color-label-bg);
font-size: 11px;
transform: rotate(45deg);
}
.sync {
@extend .custom;
bottom: 4px;
left: -22px;
top: unset;
right: unset;
}
</style>

View File

@ -0,0 +1,80 @@
<script setup lang="ts">
import { GITHUB } from "@/config/env";
import BaseIcon from "@/components/BaseIcon.vue";
import { defineAsyncComponent } from "vue";
import ShareIcon from "@/components/ChannelIcons/ShareIcon.vue";
import WeChat from "@/components/ChannelIcons/WeChat.vue";
import Github from "@/components/ChannelIcons/Github.vue";
withDefaults(defineProps<{
type?: 'vertical' | 'horizontal',
share?: boolean,
wechat?: boolean,
github?: boolean,
}>(), {
type: "vertical",
share: true,
github: true,
wechat: true
})
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
let showXhsDialog = $ref(false)
let showQQDialog = $ref(false)
</script>
<template>
<div class="center" :class="type === 'vertical' ? 'flex-col gap-1' : 'gap-4'">
<Github v-if="github"/>
<WeChat v-if="wechat"/>
<BaseIcon title="QQ群" @click="showQQDialog = true">
<IconUiwQq class="color-red"/>
</BaseIcon>
<BaseIcon title="小红书" @click="showXhsDialog = true">
<IconSimpleIconsXiaohongshu class="color-red-500"/>
</BaseIcon>
<a href="https://x.com/typewords2" target="_blank" rel="noreferrer" aria-label="关注我的 X 账户 typewords2">
<BaseIcon title="推特">
<IconRiTwitterFill class="color-blue"/>
</BaseIcon>
</a>
<a href="mailto:zyronon@163.com" target="_blank" rel="noreferrer" aria-label="发送邮件到 zyronon@163.com">
<BaseIcon title="邮箱">
<IconMaterialSymbolsMail class="color-blue"/>
</BaseIcon>
</a>
<ShareIcon v-if="share"/>
</div>
<Dialog v-model="showXhsDialog" title="小红书">
<div class="w-120 p-6 pt-0">
<div class="mb-4">
关注小红书后您可以获得开发团队的最新动态和更新内容反馈您的使用体验和建议帮助我们改进产品同时也能够及时了解我们的最新动态和更新内容
</div>
<div class="text-center">
<img src="/xhs.png" alt="小红书二维码" class="w-70 rounded-xl shadow-lg">
</div>
</div>
</Dialog>
<Dialog v-model="showQQDialog" title="QQ 交流群">
<div class="w-120 p-6 pt-0">
<div class="mb-4">
<span>加入我们的用户社群后您可以与我们的开发团队进行沟通分享您的使用体验和建议帮助我们改进产品同时也能够及时了解我们的最新动态和更新内容</span>
</div>
<div class="text-center">
<img src="/qq.jpg" alt="QQ群二维码" class="w-70 rounded-xl shadow-lg">
</div>
</div>
</Dialog>
</template>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,18 @@
<script setup lang="ts">
import { GITHUB } from "@/config/env.ts";
import BaseIcon from "@/components/BaseIcon.vue";
</script>
<template>
<a :href="GITHUB" target="_blank" rel="noreferrer" aria-label="GITHUB 项目地址"
class="color-[--color-reverse-black] github">
<BaseIcon title="Github">
<IconSimpleIconsGithub/>
</BaseIcon>
</a>
</template>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,256 @@
<script setup lang="ts">
import { APP_NAME, LIB_JS_URL, Origin } from "@/config/env.ts";
import BaseIcon from "@/components/BaseIcon.vue";
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
import { usePracticeStore } from "@/stores/practice.ts";
import { useBaseStore } from "@/stores/base.ts";
import { loadJsLib, msToHourMinute } from "@/utils";
import dayjs from "dayjs";
import Toass from "../base/toast/Toast.ts";
import { useUserStore } from "@/stores/user.ts";
import Progress from "@/components/base/Progress.vue";
import { defineAsyncComponent } from "vue";
const practiceStore = usePracticeStore()
const baseStore = useBaseStore()
const userStore = useUserStore()
let showShareDialog = $ref(false)
let loading1 = $ref(false)
let loading2 = $ref(false)
let posterEl = $ref<HTMLDivElement | null>(null)
let imgIndex = $ref(Math.floor(Math.random() * 10))
//
const studyStats = $computed(() => {
return {
total: practiceStore.total,
newWords: practiceStore.newWordNumber,
review: practiceStore.reviewWordNumber + practiceStore.writeWordNumber,
wrong: practiceStore.wrong,
correct: practiceStore.total - practiceStore.wrong,
time: msToHourMinute(practiceStore.spend),
date: dayjs().format('MM月DD日'),
dictionary: baseStore.sdict.name || '未知词书'
}
})
//
async function copyImageToClipboard() {
try {
loading1 = true
const snapdom = await loadJsLib('snapdom', LIB_JS_URL.SNAPDOM);
const blob = await snapdom.toBlob(posterEl, { scale: 2, type: 'png' })
if (!blob) throw new Error('capture failed')
if (navigator.clipboard && (window as any).ClipboardItem) {
await navigator.clipboard.write([new (window as any).ClipboardItem({ [blob.type || 'image/png']: blob })])
Toass.success('图片已复制到剪贴板!')
} else {
await downloadImage()
}
} catch (error) {
Toass.error('复制失败!')
await downloadImage()
} finally {
loading1 = false
}
}
//
async function downloadImage() {
loading2 = true
const snapdom = await loadJsLib('snapdom', LIB_JS_URL.SNAPDOM);
snapdom.download(posterEl, { scale: 2 })
loading2 = false
}
//
function changeBackground() {
const newIndex = Math.floor(Math.random() * 9) // 0-8
imgIndex = newIndex >= imgIndex ? newIndex + 1 : newIndex
}
//
const studyProgress = $computed(() => {
if (!baseStore.sdict.length) return 0
return Math.round((baseStore.sdict.lastLearnIndex / baseStore.sdict.length) * 100)
})
const sentence = $computed(() => {
let list = [
{ en: 'Actions speak louder than words.', cn: '行动胜于言语' },
{ en: 'Keep going, never give up!', cn: '坚持就是胜利' },
{ en: 'Where there\'s a will, there\'s a way.', cn: '有志者事竟成' },
{ en: 'Every cloud has a silver lining.', cn: '黑暗中总有一线光明' },
{ en: 'Time heals all wounds.', cn: '时间能治愈一切创伤' },
{ en: 'Never say die.', cn: '永不言败' },
{ en: 'The best is yet to come.', cn: '最好的尚未到来' },
{ en: 'Believe you can and you\'re halfway there.', cn: '相信你自己,你已经成功了一半' },
{ en: 'No pain, no gain.', cn: '没有付出就没有收获' },
{ en: 'Dream big and dare to fail.', cn: '大胆梦想,勇于失败' },
{ en: 'Home is where the heart is.', cn: '心在哪里,家就在哪里' },
{ en: 'Knowledge is power.', cn: '知识就是力量' },
{ en: 'Practice makes perfect.', cn: '熟能生巧' },
{ en: 'When in Rome, do as the Romans do.', cn: '入乡随俗' },
{ en: 'Just do it.', cn: '只管去做' },
{ en: 'So far, so good.', cn: '到目前为止,一切还好' },
{ en: 'The early bird catches the worm.', cn: '早起的鸟儿有虫吃' },
{ en: 'Every day is a new beginning.', cn: '每一天都是新的开始' },
{ en: 'Success is a journey, not a destination.', cn: '成功是旅程,不是终点' },
{ en: 'Your only limit is your mind.', cn: '你唯一的限制是你的思维' },
{ en: 'A friend in need is a friend indeed.', cn: '患难见真情' },
{ en: 'Silence is golden.', cn: '沉默是金' },
{ en: 'Let bygones be bygones.', cn: '让过去的成为过去' },
{ en: 'Keep calm and carry on.', cn: '保持冷静,继续前进' },
{ en: 'Live and learn.', cn: '活到老,学到老' },
{ en: 'Mistakes are proof that you are trying.', cn: '错误证明你在努力尝试' },
{ en: 'Better late than never.', cn: '迟做总比不做好' },
{ en: 'Be the change you wish to see in the world.', cn: '成为你希望在世界上看到的改变' },
{ en: 'The journey of a thousand miles begins with a single step.', cn: '千里之行,始于足下' },
{ en: 'When one door closes, another opens.', cn: '当一扇门关闭时,另一扇会打开' },
]
return list[Math.floor(Math.random() * list.length)]
})
</script>
<template>
<!-- 分享学习总结按钮 -->
<BaseIcon @click="showShareDialog = true"
class="bounce">
<IconFluentShare20Regular class="text-blue-500 hover:text-blue-600"/>
</BaseIcon>
<!-- 学习总结分享图片生成对话框 -->
<Dialog v-model="showShareDialog" title="分享">
<div class="flex min-w-160 max-w-200 p-6 pt-0 gap-space">
<!-- 左侧海报预览区域 -->
<div ref="posterEl" class="flex-1 border-r border-gray-200 bg-gray-100 rounded-xl overflow-hidden relative">
<div class="flex p-5 gap-space flex-col justify-between relative z-2 color-white h-full box-border">
<div class="flex flex-col flex-1 space-y-3">
<!-- 顶部用户信息 -->
<div class="flex items-center">
<div v-if="userStore.user?.username"
class="w-12 h-12 bg-gray-600 rounded-full mr-3 flex items-center justify-center">
<IconSimpleIconsGithub class="w-6 h-6 text-white"/>
</div>
<div>
<div class="font-semibold text-lg">{{ userStore.user?.username }}</div>
<div class="">{{ dayjs().format('YYYY年MM月DD日') }}</div>
</div>
<div class="ml-auto text-xs">
Type Words | 英语学习
</div>
</div>
<div class="bg-gray-900/30 py-4 center flex-col rounded-2xl">
<div class="text-center mb-2 text-xl">
我学习了{{ studyStats.time }} {{ baseStore.sdict.name }}
</div>
<!-- Progress Overview -->
<div class="w-90/100 flex items-center gap-space">
<div class="shrink-0">进度</div>
<Progress :percentage="studyProgress" size="normal"/>
</div>
</div>
<!-- 统计数据 -->
<div class="grid grid-cols-3 gap-4">
<div class="stat-card">
<div class="text-2xl font-bold">{{ studyStats.newWords }}</div>
<div class="text-base">新词</div>
</div>
<div class="stat-card">
<div class="text-2xl font-bold">{{ studyStats.review }}</div>
<div class="text-base">复习</div>
</div>
<div class="stat-card">
<div class="text-2xl font-bold">{{ studyStats.wrong }}</div>
<div class="text-base">错词</div>
</div>
</div>
<!-- 励志语句 -->
<div class="bg-gray-900/30 py-4 rounded-2xl center flex-col flex-1 p-4">
<div class="text-3xl text-center italic mb-2 en-article-family">{{ sentence.en }}</div>
<div class="text-base italic">{{ sentence.cn }}</div>
</div>
</div>
<!-- 底部品牌信息 -->
<div class="bg-gray-900/30 py-4 rounded-2xl p-4">
<div class="flex justify-between items-end">
<div class="space-y-2">
<div class="font-bold text-2xl">Type Words</div>
<div class="text-base ">{{ Origin }}</div>
<div class="text-xs ">一次敲击一点进步开源单词学习工具</div>
</div>
<img :src="`/imgs/qr.png`" class="w-20 w-20 rounded-md overflow-hidden" alt="">
</div>
</div>
</div>
<img :src="`/imgs/${imgIndex}.jpg`" class="w-full object-cover object-center absolute top-0 " alt="">
</div>
<!-- 右侧分享引导区域 -->
<div class="flex-1 pt-0 ">
<div class="">
<div class="text-2xl font-bold mb-4 flex items-center">
<span class="mr-2">🎯</span>
分享你的进步
</div>
<div class="flex items-start">
<span class="mr-2">🚀</span>
{{ APP_NAME }}学习英语也能成为超酷的事情
</div>
<div class="flex items-start">
<span class="mr-2">📸</span>
快来分享你的学习图片让你的进步刷屏朋友圈成为最受瞩目的英语学霸😎
</div>
<div class="flex items-start">
<span class="mr-2">💪</span>
这不只是简单的打卡更是你秀出英语实力的舞台
</div>
<div class="flex items-start">
<span class="mr-2">🔥</span>
分享你的学习记录收获朋友们的点赞和认可让你的朋友圈也掀起一股英语学习的热潮
</div>
</div>
<div class="space-y-4 mt-24">
<!-- 个性化装扮 -->
<div @click="changeBackground"
class="flex items-center justify-start gap-space color-black px-6 py-3 bg-gray-200 rounded-lg cp hover:bg-gray-300 transition-all duration-200">
<IconMdiSparkles class="w-4 h-4 text-yellow-500"/>
换个背景
</div>
<!-- 分享战绩 -->
<div @click="copyImageToClipboard"
class="flex items-center justify-start gap-space px-6 py-3 bg-gradient-to-r from-green-500 to-green-600 text-white cp rounded-lg hover:from-green-600 hover:to-green-700 transition-all duration-200">
<IconEosIconsLoading class="text-xl" v-if="loading1"/>
<IconFluentCopy20Regular class="w-5 h-5" v-else/>
<span class="font-medium">复制到剪贴板</span>
</div>
<div @click="downloadImage"
class="flex items-center justify-start gap-space px-6 py-3 bg-gradient-to-r from-purple-500 to-purple-600 text-white cp rounded-lg hover:from-purple-600 hover:to-purple-700 transition-all duration-200">
<IconEosIconsLoading class="text-xl" v-if="loading2"/>
<IconFluentArrowDownload20Regular class="w-5 h-5" v-else/>
<span class="font-medium">保存高清海报</span>
</div>
</div>
</div>
</div>
</Dialog>
</template>
<style scoped lang="scss">
.stat-card {
@apply text-center bg-gray-900/30 py-4 rounded-2xl;
}
</style>

View File

@ -0,0 +1,26 @@
<script setup lang="ts">
import BaseIcon from "@/components/BaseIcon.vue";
let showWechatDialog = $ref(false)
</script>
<template>
<BaseIcon title="微信群" @click="showWechatDialog = true">
<IconSimpleIconsWechat class="color-green-500"/>
</BaseIcon>
<Dialog v-model="showWechatDialog" title="微信群">
<div class="w-120 p-6 pt-0">
<div class="mb-4">
加入我们的用户社群后您可以与我们的开发团队进行沟通分享您的使用体验和建议帮助我们改进产品同时也能够及时了解我们的最新动态和更新内容
</div>
<div class="text-center">
<img src="/wechat.png" alt="微信群二维码" class="w-70 rounded-xl shadow-lg">
</div>
</div>
</Dialog>
</template>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,41 @@
<script setup lang="ts">
import { defineAsyncComponent, onMounted, watch } from "vue";
import { useSettingStore } from "@/stores/setting.ts";
import { jump2Feedback } from "@/utils";
import { useDisableEventListener } from "@/hooks/event.ts";
import ConflictNoticeText from "@/components/ConflictNoticeText.vue";
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
let settingStore = useSettingStore()
let show = $ref(false)
watch(() => settingStore.load, (n) => {
if (n && settingStore.conflictNotice) {
setTimeout(() => {
show = true
}, 300)
}
}, { immediate: true })
useDisableEventListener(() => show)
</script>
<template>
<Dialog
v-model="show"
title="重要提示"
footer
padding
:closeOnClickBg="false"
cancel-button-text="不再提醒"
confirm-button-text="关闭"
@cancel="settingStore.conflictNotice = false"
>
<div class="w-150 center flex-col color-main">
<ConflictNoticeText/>
</div>
</Dialog>
</template>

View File

@ -0,0 +1,31 @@
<script setup lang="ts">
import { jump2Feedback } from "@/utils";
import WeChat from "@/components/ChannelIcons/WeChat.vue";
</script>
<template>
<div class="text">
如果您安装了 <span class="font-bold text-red">调速 Vim 音视频增强</span> 等插件/脚本它们会拦截键盘按下事件<span
class="font-bold text-red">导致在本网站练习时按 'A' 'S' 'D' 等键无反应</span>您可以根据以下步骤解决冲突
</div>
<ul class="m-0">
<li>用浏览器无痕模式打开本网站确认能否正常输入</li>
<li>
无痕模式下无法输入<span class="color-link mx-1 cp" @click="jump2Feedback">点此</span>反馈或者加微信群反馈<WeChat/>
</li>
<li>无痕模式下可以输入则是插件/脚本导致的冲突</li>
<li>临时禁用对应插件/脚本或在对应插件/脚本的设置里面排除本网站</li>
<li>可安装此
<a href="https://chromewebstore.google.com/detail/one-click-extensions-mana/pbgjpgbpljobkekbhnnmlikbbfhbhmem"
target="_blank">插件(Chrome版本需翻墙)</a>,
<a href="https://microsoftedge.microsoft.com/addons/detail/%E5%BF%AB%E6%8D%B7%E6%89%A9%E5%B1%95%E7%AE%A1%E7%90%86/jdodenbllldnoogfmbmmgpieafbnaogm"
target="_blank">插件(Edge版本无需翻墙)</a>,
来快速激活禁用其他插件
</li>
</ul>
</template>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,79 @@
<script setup lang="ts">
import BaseButton from "@/components/BaseButton.vue";
import {watchEffect} from "vue";
import Textarea from "@/components/base/Textarea.vue";
import Toast from "@/components/base/toast/Toast.ts";
interface IProps {
value: string,
disabled: boolean,
}
const props = withDefaults(defineProps<IProps>(), {
value: '',
disabled: false,
})
const emit = defineEmits([
'save'
])
let editVal = $ref('')
let edit = $ref(false)
watchEffect(() => {
editVal = props.value
})
function save() {
emit('save', editVal)
edit = false
}
function toggle() {
if (props.disabled) return Toast.info('请等候翻译完成')
edit = !edit
editVal = props.value
}
</script>
<template>
<div
v-if="edit"
class="edit-text">
<Textarea
v-model="editVal"
ref="inputRef"
textarea
autosize
autofocus
type="textarea"
:input-style="`color: var(--color-font-1);font-size: 1rem;`"
/>
<div class="flex justify-end mt-2">
<BaseButton @click="toggle">取消</BaseButton>
<BaseButton @click="save">应用</BaseButton>
</div>
</div>
<div
v-else
class="text"
@click="toggle">
{{ value }}
</div>
</template>
<style scoped lang="scss">
.edit-text {
margin-top: .6rem;
color: var(--color-font-1);
}
.text {
color: var(--color-font-1);
font-size: 1.2rem;
min-height: 1.1rem;
}
</style>

43
src/components/Empty.vue Normal file
View File

@ -0,0 +1,43 @@
<script setup lang="ts">
import BaseButton from "@/components/BaseButton.vue";
defineProps<{
text?: string
showAdd?: boolean
}>()
defineEmits<{
add: []
}>()
</script>
<template>
<div class="empty">
<img src="@/assets/img/缺省页_空白页-通用.svg" alt="">
<span>{{ text ?? '空荡荡的~' }}</span>
<BaseButton v-if="showAdd" @click="$emit('add')">添加</BaseButton>
</div>
</template>
<style scoped lang="scss">
.empty {
width: 100%;
height: 100%;
min-height: 18rem;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
font-size: .9rem;
gap: 1.3rem;
span {
font-family: var(--font-family);
}
img {
margin-top: -3rem;
width: 9rem;
}
}
</style>

29
src/components/Header.vue Normal file
View File

@ -0,0 +1,29 @@
<script setup lang="ts">
import BackIcon from "@/components/BackIcon.vue";
import { useAttrs } from "vue";
interface IProps {
title: string;
showBackIcon?: boolean;
}
withDefaults(defineProps<IProps>(), {
title: '',
showBackIcon: true,
})
const attrs = useAttrs()
</script>
<template>
<div class="mb-3 text-xl font-bold relative min-h-8">
<BackIcon class="z-2 relative" v-bind="attrs" v-if="showBackIcon" />
<span class="absolute text-center w-full left-0" @click.stop>{{ title }}</span>
</div>
</template>
<style scoped lang="scss">
</style>

30
src/components/Logo.vue Normal file
View File

@ -0,0 +1,30 @@
<script setup lang="ts">
import { useSettingStore } from '@/stores/setting.ts'
import { useRouter } from 'vue-router'
import { IS_DEV } from '@/config/env'
const settingStore = useSettingStore()
const router = useRouter()
function goHome() {
if (IS_DEV) {
router.push('/')
} else {
location.href = window.atob('aHR0cHM6Ly90eXBld29yZHMuY2M=')
}
}
</script>
<template>
<div class="center mb-2" @click="goHome">
<img v-show="settingStore.theme === 'dark'" src="/logo-text-white.png" alt="" />
<img v-show="settingStore.theme !== 'dark'" src="/logo-text-black.png" alt="" />
</div>
</template>
<style scoped lang="scss">
img {
cursor: pointer;
height: 2rem;
}
</style>

Some files were not shown because too many files have changed in this diff Show More