Compare commits
No commits in common. "V1.2" and "V1" have entirely different histories.
|
@ -1,7 +1,7 @@
|
||||||
# Custom Environment Variables (Must be named with VITE_ prefix)
|
# Custom Environment Variables (Must be named with VITE_ prefix)
|
||||||
|
|
||||||
VITE_API_UPLOADS_URL = `http://127.0.0.1:10007/uploads`
|
## Development Environment Address Prefix (usually '/' or './') TODO:
|
||||||
VITE_API_BASE_URL = `http://127.0.0.1:10007/api`
|
VITE_API_BASE_URL = `http://127.0.0.1:10007`
|
||||||
|
|
||||||
## Router Mode, hash or html5
|
## Router Mode, hash or html5
|
||||||
VITE_ROUTER_HISTORY = 'html5'
|
VITE_ROUTER_HISTORY = 'html5'
|
||||||
|
|
28
.github/ISSUE_TEMPLATE/bug_report.md
vendored
28
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -1,28 +0,0 @@
|
||||||
---
|
|
||||||
name: Bug Report
|
|
||||||
about: Create a report to help us improve
|
|
||||||
title: KUN Visual Novel Bug Report
|
|
||||||
labels: bug, enhancement
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Describe the bug**
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
**To Reproduce**
|
|
||||||
Steps to reproduce the behavior:
|
|
||||||
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '....'
|
|
||||||
3. Scroll down to '....'
|
|
||||||
4. See error
|
|
||||||
|
|
||||||
**Expected behavior**
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
|
|
||||||
**Screenshots**
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context about the problem here.
|
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
@ -1,20 +0,0 @@
|
||||||
---
|
|
||||||
name: Feature Request
|
|
||||||
about: Suggest an idea or enhancement
|
|
||||||
title: KUN Visual Novel Feature Request
|
|
||||||
labels: enhancement
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
|
||||||
A clear and concise description of the problem or need. Ex. I'm always frustrated when [...]
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the feature request here.
|
|
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
|
@ -6,7 +6,6 @@
|
||||||
"Amayui",
|
"Amayui",
|
||||||
"arpa",
|
"arpa",
|
||||||
"Asuka",
|
"Asuka",
|
||||||
"atrule",
|
|
||||||
"axios",
|
"axios",
|
||||||
"azkhx",
|
"azkhx",
|
||||||
"bangumi",
|
"bangumi",
|
||||||
|
@ -14,8 +13,6 @@
|
||||||
"Bishoujo",
|
"Bishoujo",
|
||||||
"Chuudoku",
|
"Chuudoku",
|
||||||
"Codepen",
|
"Codepen",
|
||||||
"commonmark",
|
|
||||||
"cooldown",
|
|
||||||
"cout",
|
"cout",
|
||||||
"dompurify",
|
"dompurify",
|
||||||
"fontawesome",
|
"fontawesome",
|
||||||
|
@ -47,7 +44,6 @@
|
||||||
"Mangekyou",
|
"Mangekyou",
|
||||||
"Maniwa",
|
"Maniwa",
|
||||||
"Meister",
|
"Meister",
|
||||||
"Milkdown",
|
|
||||||
"Minato",
|
"Minato",
|
||||||
"mingcute",
|
"mingcute",
|
||||||
"Mirai",
|
"Mirai",
|
||||||
|
@ -60,7 +56,6 @@
|
||||||
"nawa",
|
"nawa",
|
||||||
"NEKOPARA",
|
"NEKOPARA",
|
||||||
"non-moe",
|
"non-moe",
|
||||||
"nord",
|
|
||||||
"nprogress",
|
"nprogress",
|
||||||
"okaidia",
|
"okaidia",
|
||||||
"Otome",
|
"Otome",
|
||||||
|
@ -68,7 +63,6 @@
|
||||||
"persistedstate",
|
"persistedstate",
|
||||||
"Pinia",
|
"Pinia",
|
||||||
"prismjs",
|
"prismjs",
|
||||||
"prosemirror",
|
|
||||||
"rdquo",
|
"rdquo",
|
||||||
"Roka",
|
"Roka",
|
||||||
"Sahou",
|
"Sahou",
|
||||||
|
@ -78,7 +72,6 @@
|
||||||
"Senren",
|
"Senren",
|
||||||
"Sensei",
|
"Sensei",
|
||||||
"Shabondama",
|
"Shabondama",
|
||||||
"Shiki",
|
|
||||||
"shinnku",
|
"shinnku",
|
||||||
"Shugaten",
|
"Shugaten",
|
||||||
"signin",
|
"signin",
|
||||||
|
@ -107,7 +100,6 @@
|
||||||
"Vite",
|
"Vite",
|
||||||
"VNDB",
|
"VNDB",
|
||||||
"vueup",
|
"vueup",
|
||||||
"waifu",
|
|
||||||
"Wataridori",
|
"Wataridori",
|
||||||
"weixin",
|
"weixin",
|
||||||
"Wenders",
|
"Wenders",
|
||||||
|
|
143
LICENSE
143
LICENSE
|
@ -1,5 +1,5 @@
|
||||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
GNU GENERAL PUBLIC LICENSE
|
||||||
Version 3, 19 November 2007
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
@ -7,15 +7,17 @@
|
||||||
|
|
||||||
Preamble
|
Preamble
|
||||||
|
|
||||||
The GNU Affero General Public License is a free, copyleft license for
|
The GNU General Public License is a free, copyleft license for
|
||||||
software and other kinds of works, specifically designed to ensure
|
software and other kinds of works.
|
||||||
cooperation with the community in the case of network server software.
|
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
The licenses for most software and other practical works are designed
|
||||||
to take away your freedom to share and change the works. By contrast,
|
to take away your freedom to share and change the works. By contrast,
|
||||||
our General Public Licenses are intended to guarantee your freedom to
|
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
|
share and change all versions of a program--to make sure it remains free
|
||||||
software for all its users.
|
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
|
When we speak of free software, we are referring to freedom, not
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
@ -24,34 +26,44 @@ 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
|
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.
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
Developers that use our General Public Licenses protect your rights
|
To protect your rights, we need to prevent others from denying you
|
||||||
with two steps: (1) assert copyright on the software, and (2) offer
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
you this License which gives you legal permission to copy, distribute
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
and/or modify the software.
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
A secondary benefit of defending all users' freedom is that
|
For example, if you distribute copies of such a program, whether
|
||||||
improvements made in alternate versions of the program, if they
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
receive widespread use, become available for other developers to
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
incorporate. Many developers of free software are heartened and
|
or can get the source code. And you must show them these terms so they
|
||||||
encouraged by the resulting cooperation. However, in the case of
|
know their rights.
|
||||||
software used on network servers, this result may fail to come about.
|
|
||||||
The GNU General Public License permits making a modified version and
|
|
||||||
letting the public access it on a server without ever releasing its
|
|
||||||
source code to the public.
|
|
||||||
|
|
||||||
The GNU Affero General Public License is designed specifically to
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
ensure that, in such cases, the modified source code becomes available
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
to the community. It requires the operator of a network server to
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
provide the source code of the modified version running there to the
|
|
||||||
users of that server. Therefore, public use of a modified version, on
|
|
||||||
a publicly accessible server, gives the public access to the source
|
|
||||||
code of the modified version.
|
|
||||||
|
|
||||||
An older license, called the Affero General Public License and
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
published by Affero, was designed to accomplish similar goals. This is
|
that there is no warranty for this free software. For both users' and
|
||||||
a different license, not a version of the Affero GPL, but Affero has
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
released a new version of the Affero GPL which permits relicensing under
|
changed, so that their problems will not be attributed erroneously to
|
||||||
this license.
|
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
|
The precise terms and conditions for copying, distribution and
|
||||||
modification follow.
|
modification follow.
|
||||||
|
@ -60,7 +72,7 @@ modification follow.
|
||||||
|
|
||||||
0. Definitions.
|
0. Definitions.
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
works, such as semiconductor masks.
|
works, such as semiconductor masks.
|
||||||
|
@ -537,45 +549,35 @@ 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
|
the Program, the only way you could satisfy both those terms and this
|
||||||
License would be to refrain entirely from conveying the Program.
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, if you modify the
|
|
||||||
Program, your modified version must prominently offer all users
|
|
||||||
interacting with it remotely through a computer network (if your version
|
|
||||||
supports such interaction) an opportunity to receive the Corresponding
|
|
||||||
Source of your version by providing access to the Corresponding Source
|
|
||||||
from a network server at no charge, through some standard or customary
|
|
||||||
means of facilitating copying of software. This Corresponding Source
|
|
||||||
shall include the Corresponding Source for any work covered by version 3
|
|
||||||
of the GNU General Public License that is incorporated pursuant to the
|
|
||||||
following paragraph.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
Notwithstanding any other provision of this License, you have
|
||||||
permission to link or combine any covered work with a work licensed
|
permission to link or combine any covered work with a work licensed
|
||||||
under version 3 of the GNU General Public License into a single
|
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
|
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,
|
License will continue to apply to the part which is the covered work,
|
||||||
but the work with which it is combined will remain governed by version
|
but the special requirements of the GNU Affero General Public License,
|
||||||
3 of the GNU General Public License.
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
the GNU Affero General Public License from time to time. Such new versions
|
the GNU General Public License from time to time. Such new versions will
|
||||||
will be similar in spirit to the present version, but may differ in detail to
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
address new problems or concerns.
|
address new problems or concerns.
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
Each version is given a distinguishing version number. If the
|
||||||
Program specifies that a certain numbered version of the GNU Affero General
|
Program specifies that a certain numbered version of the GNU General
|
||||||
Public License "or any later version" applies to it, you have the
|
Public License "or any later version" applies to it, you have the
|
||||||
option of following the terms and conditions either of that numbered
|
option of following the terms and conditions either of that numbered
|
||||||
version or of any later version published by the Free Software
|
version or of any later version published by the Free Software
|
||||||
Foundation. If the Program does not specify a version number of the
|
Foundation. If the Program does not specify a version number of the
|
||||||
GNU Affero General Public License, you may choose any version ever published
|
GNU General Public License, you may choose any version ever published
|
||||||
by the Free Software Foundation.
|
by the Free Software Foundation.
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
If the Program specifies that a proxy can decide which future
|
||||||
versions of the GNU Affero General Public License can be used, that proxy's
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
public statement of acceptance of a version permanently authorizes you
|
public statement of acceptance of a version permanently authorizes you
|
||||||
to choose that version for the Program.
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
@ -633,29 +635,40 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||||
Copyright (C) <year> <name of author>
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU Affero General Public License as published
|
it under the terms of the GNU General Public License as published by
|
||||||
by the Free Software Foundation, either version 3 of the License, or
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU Affero General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
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.
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
If your software can interact with users remotely through a computer
|
If the program does terminal interaction, make it output a short
|
||||||
network, you should also make sure that it provides a way for users to
|
notice like this when it starts in an interactive mode:
|
||||||
get its source. For example, if your program is a web application, its
|
|
||||||
interface could display a "Source" link that leads users to an archive
|
<program> Copyright (C) <year> <name of author>
|
||||||
of the code. There are many ways you could offer source, and different
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
solutions will be better for different programs; see section 13 for the
|
This is free software, and you are welcome to redistribute it
|
||||||
specific requirements.
|
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,
|
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.
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
<https://www.gnu.org/licenses/>.
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program
|
||||||
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
|
may consider it more useful to permit linking proprietary applications with
|
||||||
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License. But first, please read
|
||||||
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
![Logo](https://github.com/KUN1007/kun-galgame-vue/blob/unfixed-loli/src/assets/images/favicon.png)
|
![Logo](https://github.com/KUN1007/kun-galgame-vue/blob/layout/src/assets/images/favicon.png)Logo
|
||||||
|
|
||||||
The image is sourced from the game [Ark Order](https://apps.qoo-app.com/en/app/9593), featuring the character '鲲' (Kun).
|
The image is sourced from the game [Ark Order](https://apps.qoo-app.com/en/app/9593), featuring the character '鲲' (Kun).
|
||||||
|
|
||||||
|
|
94
index.html
94
index.html
|
@ -10,100 +10,12 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="KUN Visual Novel Forum. 鲲 Galgame 论坛。 The CUTEST Visual Novel Forum!世界上最萌的 Galgame 论坛. Topic, Technique. NO ADs Forever. Free Forever"
|
content="The CUTEST Visual Novel Forum!世界上最萌的 Galgame 论坛. Topic, Technique. NO ADs Forever. Free Forever"
|
||||||
/>
|
/>
|
||||||
<title>KUN Visual Novel Forum| 鲲 Galgame 论坛</title>
|
<title>KUNGalgame</title>
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--kungalgame-blue-1: #b6e3ff;
|
|
||||||
--kungalgame-blue-4: #218bff;
|
|
||||||
|
|
||||||
--kungalgame-trans-blue-1: #b6e3ff77;
|
|
||||||
|
|
||||||
--kungalgame-shadow-0: 0px 0px 17px 5px var(--kungalgame-blue-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#kungalgame-loading-container {
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#kungalgame-loading {
|
|
||||||
height: 4.8px;
|
|
||||||
background: var(--kungalgame-blue-4);
|
|
||||||
box-shadow: var(--kungalgame-shadow-0);
|
|
||||||
box-sizing: border-box;
|
|
||||||
animation: kungalgame-loading 7.7s linear infinite;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
#kungalgame-loading::after,
|
|
||||||
#kungalgame-loading::before {
|
|
||||||
content: '';
|
|
||||||
width: 10px;
|
|
||||||
height: 1px;
|
|
||||||
background: var(--kungalgame-blue-4);
|
|
||||||
position: absolute;
|
|
||||||
top: 9px;
|
|
||||||
right: -2px;
|
|
||||||
opacity: 0;
|
|
||||||
transform: rotate(-45deg) translateX(0px);
|
|
||||||
box-sizing: border-box;
|
|
||||||
animation: coli1 0.3s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
#kungalgame-loading::before {
|
|
||||||
top: -4px;
|
|
||||||
transform: rotate(45deg);
|
|
||||||
animation: coli2 0.3s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
#kungalgame-loading h2 {
|
|
||||||
position: absolute;
|
|
||||||
top: 17px;
|
|
||||||
color: var(--kungalgame-blue-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes kungalgame-loading {
|
|
||||||
0% {
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes coli1 {
|
|
||||||
0% {
|
|
||||||
transform: rotate(-45deg) translateX(0px);
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(-45deg) translateX(-45px);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes coli2 {
|
|
||||||
0% {
|
|
||||||
transform: rotate(45deg) translateX(0px);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(45deg) translateX(-45px);
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app"></div>
|
||||||
<div id="kungalgame-loading-container">
|
|
||||||
<div id="kungalgame-loading"><h2>Loading</h2></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
52
package.json
52
package.json
|
@ -18,43 +18,33 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@milkdown/core": "7.3.6",
|
"@vueup/vue-quill": "^1.2.0",
|
||||||
"@milkdown/ctx": "7.3.6",
|
|
||||||
"@milkdown/plugin-clipboard": "7.3.6",
|
|
||||||
"@milkdown/plugin-history": "7.3.6",
|
|
||||||
"@milkdown/plugin-indent": "7.3.6",
|
|
||||||
"@milkdown/plugin-listener": "7.3.6",
|
|
||||||
"@milkdown/plugin-prism": "7.3.6",
|
|
||||||
"@milkdown/plugin-tooltip": "7.3.6",
|
|
||||||
"@milkdown/plugin-trailing": "7.3.6",
|
|
||||||
"@milkdown/preset-commonmark": "7.3.6",
|
|
||||||
"@milkdown/preset-gfm": "7.3.6",
|
|
||||||
"@milkdown/prose": "7.3.6",
|
|
||||||
"@milkdown/transformer": "7.3.6",
|
|
||||||
"@milkdown/utils": "7.3.6",
|
|
||||||
"@milkdown/vue": "7.3.6",
|
|
||||||
"@prosemirror-adapter/vue": "0.2.6",
|
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
|
"dompurify": "^3.0.6",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"pinia-plugin-persistedstate": "^3.2.1",
|
"pinia-plugin-persistedstate": "^3.2.0",
|
||||||
"refractor": "^4.8.1",
|
"quill-blot-formatter": "^1.0.5",
|
||||||
"vue": "^3.4.24",
|
"quill-image-compress": "^1.2.30",
|
||||||
"vue-i18n": "^9.13.1",
|
"quill-magic-url": "^4.2.0",
|
||||||
"vue-router": "^4.3.2"
|
"vue": "^3.3.6",
|
||||||
|
"vue-i18n": "^9.5.0",
|
||||||
|
"vue-router": "^4.2.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify/vue": "^4.1.2",
|
"@iconify/vue": "^4.1.1",
|
||||||
"@types/node": "^20.12.7",
|
"@types/dompurify": "^3.0.4",
|
||||||
"@types/nprogress": "^0.2.3",
|
"@types/js-cookie": "^3.0.5",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@types/node": "^20.8.7",
|
||||||
"rollup-plugin-visualizer": "^5.12.0",
|
"@types/nprogress": "^0.2.2",
|
||||||
"sass": "^1.75.0",
|
"@vitejs/plugin-vue": "^4.4.0",
|
||||||
"typescript": "^5.4.5",
|
"rollup-plugin-visualizer": "^5.9.2",
|
||||||
"vite": "^5.2.10",
|
"sass": "^1.69.4",
|
||||||
"vue-tsc": "^2.0.14"
|
"typescript": "^5.2.2",
|
||||||
|
"vite": "^4.5.0",
|
||||||
|
"vue-tsc": "^1.8.19"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"kun",
|
"kun",
|
||||||
|
@ -68,4 +58,4 @@
|
||||||
"visual novel"
|
"visual novel"
|
||||||
],
|
],
|
||||||
"license": "LGPL-3.0-or-later"
|
"license": "LGPL-3.0-or-later"
|
||||||
}
|
}
|
||||||
|
|
3154
pnpm-lock.yaml
3154
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -1,2 +0,0 @@
|
||||||
User-agent: *
|
|
||||||
Allow: /
|
|
|
@ -9,9 +9,6 @@ import Info from '@/components/alert/Info.vue'
|
||||||
const Capture = defineAsyncComponent(
|
const Capture = defineAsyncComponent(
|
||||||
() => import('@/components/capture/Capture.vue')
|
() => import('@/components/capture/Capture.vue')
|
||||||
)
|
)
|
||||||
const KUNGalgameSearchBox = defineAsyncComponent(
|
|
||||||
() => import('@/components/search/KUNGalgameSearchBox.vue')
|
|
||||||
)
|
|
||||||
|
|
||||||
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
|
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
|
@ -47,9 +44,6 @@ onBeforeMount(() => {
|
||||||
<!-- Global capture component -->
|
<!-- Global capture component -->
|
||||||
<Capture />
|
<Capture />
|
||||||
|
|
||||||
<!-- Global search component -->
|
|
||||||
<KUNGalgameSearchBox />
|
|
||||||
|
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,6 @@ export interface EditUpdateTopicRequestData {
|
||||||
content: string
|
content: string
|
||||||
tags: string[]
|
tags: string[]
|
||||||
category: string[]
|
category: string[]
|
||||||
edited: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request data format for getting hot tags
|
// Request data format for getting hot tags
|
||||||
|
|
|
@ -5,25 +5,11 @@ import type * as Home from './types/home'
|
||||||
|
|
||||||
// URLs to be requested
|
// URLs to be requested
|
||||||
const homeURLs = {
|
const homeURLs = {
|
||||||
search: `/home/search`,
|
|
||||||
home: `/home/topic`,
|
home: `/home/topic`,
|
||||||
navHot: `/home/nav/hot`,
|
navHot: `/home/nav/hot`,
|
||||||
navNew: `/home/nav/new`,
|
navNew: `/home/nav/new`,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Home page topic list
|
|
||||||
export async function getHomeSearchTopicApi(
|
|
||||||
requestData: Home.HomeSearchTopicRequestData
|
|
||||||
): Promise<Home.HomeSearchTopicResponseData> {
|
|
||||||
const queryParams = objectToQueryParams(requestData)
|
|
||||||
|
|
||||||
const response = await fetchGet<Home.HomeSearchTopicResponseData>(
|
|
||||||
`${homeURLs.search}?${queryParams}`
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
// Home page topic list
|
// Home page topic list
|
||||||
export async function getHomeTopicApi(
|
export async function getHomeTopicApi(
|
||||||
requestData: Home.HomeTopicRequestData
|
requestData: Home.HomeTopicRequestData
|
||||||
|
|
|
@ -1,19 +1,3 @@
|
||||||
export interface HomeSearchTopic {
|
|
||||||
tid: number
|
|
||||||
title: string
|
|
||||||
content: string
|
|
||||||
category: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HomeSearchTopicRequestData {
|
|
||||||
keywords: string
|
|
||||||
category: string
|
|
||||||
page: number
|
|
||||||
limit: number
|
|
||||||
sortField: string
|
|
||||||
sortOrder: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface HomeUserInfo {
|
interface HomeUserInfo {
|
||||||
uid: number
|
uid: number
|
||||||
avatar: string
|
avatar: string
|
||||||
|
@ -33,6 +17,7 @@ export interface HomeNewTopic {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HomeTopicRequestData {
|
export interface HomeTopicRequestData {
|
||||||
|
keywords: string
|
||||||
category: string
|
category: string
|
||||||
page: number
|
page: number
|
||||||
limit: number
|
limit: number
|
||||||
|
@ -59,12 +44,11 @@ export interface HomeTopic {
|
||||||
upvote_time: number
|
upvote_time: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HomeSearchTopicResponseData = KUNGalgameResponseData<
|
// 10 hot topics on the left side
|
||||||
HomeSearchTopic[]
|
|
||||||
>
|
|
||||||
|
|
||||||
export type HomeHotTopicResponseData = KUNGalgameResponseData<HomeHotTopic[]>
|
export type HomeHotTopicResponseData = KUNGalgameResponseData<HomeHotTopic[]>
|
||||||
|
|
||||||
|
// 10 latest topics on the left side
|
||||||
export type HomeNewTopicResponseData = KUNGalgameResponseData<HomeNewTopic[]>
|
export type HomeNewTopicResponseData = KUNGalgameResponseData<HomeNewTopic[]>
|
||||||
|
|
||||||
|
// Topics displayed in the middle
|
||||||
export type HomeTopicResponseData = KUNGalgameResponseData<HomeTopic[]>
|
export type HomeTopicResponseData = KUNGalgameResponseData<HomeTopic[]>
|
||||||
|
|
|
@ -16,8 +16,6 @@ export * from './non-moe/types/nonMoe'
|
||||||
export * from './ranking/types/ranking'
|
export * from './ranking/types/ranking'
|
||||||
export * from './topic/types'
|
export * from './topic/types'
|
||||||
export * from './update-log/types/updateLog'
|
export * from './update-log/types/updateLog'
|
||||||
export * from './pool/types/pool'
|
|
||||||
export * from './technique/types/technique'
|
|
||||||
|
|
||||||
// Expose all APIs
|
// Expose all APIs
|
||||||
export * from './balance'
|
export * from './balance'
|
||||||
|
@ -29,5 +27,3 @@ export * from './non-moe'
|
||||||
export * from './ranking'
|
export * from './ranking'
|
||||||
export * from './topic'
|
export * from './topic'
|
||||||
export * from './update-log'
|
export * from './update-log'
|
||||||
export * from './pool'
|
|
||||||
export * from './technique'
|
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { fetchGet } from '@/utils/request'
|
|
||||||
import objectToQueryParams from '@/utils/objectToQueryParams'
|
|
||||||
import type * as Pool from './types/pool'
|
|
||||||
|
|
||||||
const poolURLs = {
|
|
||||||
topic: `/pool/topic`,
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getPoolTopicApi(
|
|
||||||
requestData: Pool.PoolTopicsRequestData
|
|
||||||
): Promise<Pool.PoolTopicResponseData> {
|
|
||||||
const queryParams = objectToQueryParams(requestData)
|
|
||||||
|
|
||||||
const response = await fetchGet<Pool.PoolTopicResponseData>(
|
|
||||||
`${poolURLs.topic}?${queryParams}`
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
export interface PoolTopic {
|
|
||||||
tid: number
|
|
||||||
title: string
|
|
||||||
views: number
|
|
||||||
likesCount: number
|
|
||||||
time: number
|
|
||||||
content: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PoolTopicsRequestData {
|
|
||||||
page: number
|
|
||||||
limit: number
|
|
||||||
sortField: string
|
|
||||||
sortOrder: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PoolTopicResponseData = KUNGalgameResponseData<PoolTopic[]>
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { fetchGet } from '@/utils/request'
|
|
||||||
import objectToQueryParams from '@/utils/objectToQueryParams'
|
|
||||||
import type * as Technique from './types/technique'
|
|
||||||
|
|
||||||
const techniqueURLs = {
|
|
||||||
topic: `/technique/topic`,
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getTechniqueTopicApi(
|
|
||||||
requestData: Technique.TechniqueTopicsRequestData
|
|
||||||
): Promise<Technique.TechniqueTopicResponseData> {
|
|
||||||
const queryParams = objectToQueryParams(requestData)
|
|
||||||
|
|
||||||
const response = await fetchGet<Technique.TechniqueTopicResponseData>(
|
|
||||||
`${techniqueURLs.topic}?${queryParams}`
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
export interface TechniqueTopic {
|
|
||||||
tid: number
|
|
||||||
title: string
|
|
||||||
views: number
|
|
||||||
likesCount: number
|
|
||||||
replyCount: number
|
|
||||||
content: string
|
|
||||||
tags: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TechniqueTopicsRequestData {
|
|
||||||
page: number
|
|
||||||
limit: number
|
|
||||||
sortField: string
|
|
||||||
sortOrder: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TechniqueTopicResponseData = KUNGalgameResponseData<
|
|
||||||
TechniqueTopic[]
|
|
||||||
>
|
|
|
@ -4,7 +4,7 @@ import objectToQueryParams from '@/utils/objectToQueryParams'
|
||||||
import * as Reply from './types/reply'
|
import * as Reply from './types/reply'
|
||||||
|
|
||||||
// Get topic replies by tid
|
// Get topic replies by tid
|
||||||
export async function getRepliesByTidApi(
|
export async function getRepliesByPidApi(
|
||||||
request: Reply.TopicReplyRequestData
|
request: Reply.TopicReplyRequestData
|
||||||
): Promise<Reply.TopicReplyResponseData> {
|
): Promise<Reply.TopicReplyResponseData> {
|
||||||
const queryParams = objectToQueryParams(request, 'tid')
|
const queryParams = objectToQueryParams(request, 'tid')
|
||||||
|
@ -16,7 +16,7 @@ export async function getRepliesByTidApi(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a reply by tid
|
// Create a reply by tid
|
||||||
export async function postReplyByTidApi(
|
export async function postReplyByPidApi(
|
||||||
request: Reply.TopicCreateReplyRequestData
|
request: Reply.TopicCreateReplyRequestData
|
||||||
): Promise<Reply.TopicCreateReplyResponseData> {
|
): Promise<Reply.TopicCreateReplyResponseData> {
|
||||||
const url = `/topics/${request.tid}/reply`
|
const url = `/topics/${request.tid}/reply`
|
||||||
|
|
|
@ -13,8 +13,6 @@ export interface TopicAsideOtherTagRequestData {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TopicAsideMasterRequestData {
|
export interface TopicAsideMasterRequestData {
|
||||||
// User uid
|
|
||||||
uid: number
|
|
||||||
// The tid of the current topic, as other topics under the same tag should not include the current one
|
// The tid of the current topic, as other topics under the same tag should not include the current one
|
||||||
tid: string
|
tid: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,6 @@ export interface TopicCreateReplyRequestData {
|
||||||
to_floor: number
|
to_floor: number
|
||||||
tags: string[]
|
tags: string[]
|
||||||
content: string
|
content: string
|
||||||
time: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upvote reply, the upvote operation is irreversible
|
// Upvote reply, the upvote operation is irreversible
|
||||||
|
@ -45,7 +44,6 @@ export interface TopicUpvoteReplyRequestData {
|
||||||
tid: number
|
tid: number
|
||||||
to_uid: number
|
to_uid: number
|
||||||
rid: number
|
rid: number
|
||||||
time: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Like reply
|
// Like reply
|
||||||
|
@ -70,7 +68,6 @@ export interface TopicUpdateReplyRequestData {
|
||||||
rid: number
|
rid: number
|
||||||
content: string
|
content: string
|
||||||
tags: string[]
|
tags: string[]
|
||||||
edited: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Response data format for a single topic reply, returning multiple reply data in an array
|
// Response data format for a single topic reply, returning multiple reply data in an array
|
||||||
|
|
|
@ -37,7 +37,6 @@ export interface TopicDetail {
|
||||||
export interface TopicUpvoteTopicRequestData {
|
export interface TopicUpvoteTopicRequestData {
|
||||||
tid: number
|
tid: number
|
||||||
to_uid: number
|
to_uid: number
|
||||||
time: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request data for liking a topic
|
// Request data for liking a topic
|
||||||
|
|
|
@ -1,802 +0,0 @@
|
||||||
[
|
|
||||||
{
|
|
||||||
"name": "汗",
|
|
||||||
"left": 287,
|
|
||||||
"top": 425,
|
|
||||||
"width": 12,
|
|
||||||
"height": 14,
|
|
||||||
"visible": 1,
|
|
||||||
"layer_id": 2961,
|
|
||||||
"group_layer_id": 2931
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "追真剣",
|
|
||||||
"left": 234,
|
|
||||||
"top": 383,
|
|
||||||
"width": 76,
|
|
||||||
"height": 27,
|
|
||||||
"visible": 1,
|
|
||||||
"layer_id": 3022,
|
|
||||||
"group_layer_id": 2923
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "笑1",
|
|
||||||
"left": 232,
|
|
||||||
"top": 380,
|
|
||||||
"width": 78,
|
|
||||||
"height": 29,
|
|
||||||
"visible": 1,
|
|
||||||
"layer_id": 2842,
|
|
||||||
"group_layer_id": 2923
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "笑2",
|
|
||||||
"left": 235,
|
|
||||||
"top": 385,
|
|
||||||
"width": 76,
|
|
||||||
"height": 32,
|
|
||||||
"visible": 1,
|
|
||||||
"layer_id": 2845,
|
|
||||||
"group_layer_id": 2923
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "怒1",
|
|
||||||
"left": 239,
|
|
||||||
"top": 382,
|
|
||||||
"width": 63,
|
|
||||||
"height": 28,
|
|
||||||
"visible": 1,
|
|
||||||
"layer_id": 2848,
|
|
||||||
"group_layer_id": 2923
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "怒2",
|
|
||||||
"left": 240,
|
|
||||||
"top": 382,
|
|
||||||
"width": 60,
|
|
||||||
"height": 26,
|
|
||||||
"visible": 1,
|
|
||||||
"layer_id": 2864,
|
|
||||||
"group_layer_id": 2923
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "悲1",
|
|
||||||
"left": 237,
|
|
||||||
"top": 386,
|
|
||||||
"width": 70,
|
|
||||||
"height": 28,
|
|
||||||
"visible": 1,
|
|
||||||
"layer_id": 2855,
|
|
||||||
"group_layer_id": 2923
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "悲2",
|
|
||||||
"left": 238,
|
|
||||||
"top": 391,
|
|
||||||
"width": 66,
|
|
||||||
"height": 28,
|
|
||||||
"visible": 1,
|
|
||||||
"layer_id": 2859,
|
|
||||||
"group_layer_id": 2923
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "泣1",
|
|
||||||
"left": 243,
|
|
||||||
"top": 388,
|
|
||||||
"width": 58,
|
|
||||||
"height": 30,
|
|
||||||
"visible": 1,
|
|
||||||
"layer_id": 2865,
|
|
||||||
"group_layer_id": 2923
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "泣2",
|
|
||||||
"left": 240,
|
|
||||||
"top": 386,
|
|
||||||
"width": 65,
|
|
||||||
"height": 31,
|
|
||||||
"visible": 1,
|
|
||||||
"layer_id": 2870,
|
|
||||||
"group_layer_id": 2923
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "驚1",
|
|
||||||
"left": 232,
|
|
||||||
"top": 379,
|
|
||||||
"width": 78,
|
|
||||||
"height": 31,
|
|
||||||
"visible": 1,
|
|
||||||
"layer_id": 2877,
|
|
||||||
"group_layer_id": 2923
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "驚2",
|
|
||||||
"left": 232,
|
|
||||||
"top": 379,
|
|
||||||
"width": 78,
|
|
||||||
"height": 33,
|
|
||||||
"visible": 1,
|
|
||||||
"layer_id": 2882,
|
|
||||||
"group_layer_id": 2923
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "照1",
|
|
||||||
"left": 234,
|
|
||||||
"top": 382,
|
|
||||||
"width": 77,
|
|
||||||
"height": 31,
|
|
||||||
"visible": 1,
|
|
||||||
"layer_id": 2887,
|
|
||||||
"group_layer_id": 2923
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "照2",
|
|
||||||
"left": 239,
|
|
||||||
"top": 385,
|
|
||||||
"width": 70,
|
|
||||||
"height": 31,
|
|
||||||
"visible": 1,
|
|
||||||
"layer_id": 2893,
|
|
||||||
"group_layer_id": 2923
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "えっへん",
|
|
||||||
"left": 235,
|
|
||||||
"top": 384,
|
|
||||||
"width": 72,
|
|
||||||
"height": 25,
|
|
||||||
"visible": 1,
|
|
||||||
"layer_id": 2899,
|
|
||||||
"group_layer_id": 2923
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ギャグ笑",
|
|
||||||
"left": 234,
|
|
||||||
"top": 383,
|
|
||||||
"width": 76,
|
|
||||||
"height": 33,
|
|
||||||
"visible": 1,
|
|
||||||
"layer_id": 2905,
|
|
||||||
"group_layer_id": 2923
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ギャグ怒",
|
|
||||||
"left": 239,
|
|
||||||
"top": 384,
|
|
||||||
"width": 61,
|
|
||||||
"height": 29,
|
|
||||||
"visible": 1,
|
|
||||||
"layer_id": 2910,
|
|
||||||
"group_layer_id": 2923
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ギャグ泣",
|
|
||||||
"left": 239,
|
|
||||||
"top": 389,
|
|
||||||
"width": 63,
|
|
||||||
"height": 26,
|
|
||||||
"visible": 1,
|
|
||||||
"layer_id": 2915,
|
|
||||||
"group_layer_id": 2923
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ギャグ驚",
|
|
||||||
"left": 235,
|
|
||||||
"top": 383,
|
|
||||||
"width": 66,
|
|
||||||
"height": 23,
|
|
||||||
"visible": 1,
|
|
||||||
"layer_id": 2918,
|
|
||||||
"group_layer_id": 2923
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "追真剣",
|
|
||||||
"left": 238,
|
|
||||||
"top": 392,
|
|
||||||
"width": 78,
|
|
||||||
"height": 41,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 3020,
|
|
||||||
"group_layer_id": 2925
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "笑1",
|
|
||||||
"left": 238,
|
|
||||||
"top": 390,
|
|
||||||
"width": 78,
|
|
||||||
"height": 43,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2843,
|
|
||||||
"group_layer_id": 2925
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "笑2",
|
|
||||||
"left": 244,
|
|
||||||
"top": 395,
|
|
||||||
"width": 69,
|
|
||||||
"height": 37,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2846,
|
|
||||||
"group_layer_id": 2925
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "怒1",
|
|
||||||
"left": 238,
|
|
||||||
"top": 391,
|
|
||||||
"width": 78,
|
|
||||||
"height": 42,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2850,
|
|
||||||
"group_layer_id": 2925
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "怒2",
|
|
||||||
"left": 238,
|
|
||||||
"top": 391,
|
|
||||||
"width": 78,
|
|
||||||
"height": 42,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2853,
|
|
||||||
"group_layer_id": 2925
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "悲1",
|
|
||||||
"left": 238,
|
|
||||||
"top": 392,
|
|
||||||
"width": 78,
|
|
||||||
"height": 41,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2856,
|
|
||||||
"group_layer_id": 2925
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "悲2",
|
|
||||||
"left": 240,
|
|
||||||
"top": 396,
|
|
||||||
"width": 75,
|
|
||||||
"height": 37,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2861,
|
|
||||||
"group_layer_id": 2925
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "泣1",
|
|
||||||
"left": 243,
|
|
||||||
"top": 396,
|
|
||||||
"width": 71,
|
|
||||||
"height": 37,
|
|
||||||
"visible": 1,
|
|
||||||
"layer_id": 3025,
|
|
||||||
"group_layer_id": 2925
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "泣2",
|
|
||||||
"left": 238,
|
|
||||||
"top": 394,
|
|
||||||
"width": 78,
|
|
||||||
"height": 60,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 3024,
|
|
||||||
"group_layer_id": 2925
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "驚1",
|
|
||||||
"left": 238,
|
|
||||||
"top": 390,
|
|
||||||
"width": 78,
|
|
||||||
"height": 43,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2880,
|
|
||||||
"group_layer_id": 2925
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "驚2",
|
|
||||||
"left": 238,
|
|
||||||
"top": 390,
|
|
||||||
"width": 78,
|
|
||||||
"height": 43,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2885,
|
|
||||||
"group_layer_id": 2925
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "照1",
|
|
||||||
"left": 238,
|
|
||||||
"top": 392,
|
|
||||||
"width": 78,
|
|
||||||
"height": 41,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2891,
|
|
||||||
"group_layer_id": 2925
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "照2",
|
|
||||||
"left": 244,
|
|
||||||
"top": 395,
|
|
||||||
"width": 69,
|
|
||||||
"height": 37,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2896,
|
|
||||||
"group_layer_id": 2925
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "えっへん",
|
|
||||||
"left": 244,
|
|
||||||
"top": 396,
|
|
||||||
"width": 68,
|
|
||||||
"height": 38,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2902,
|
|
||||||
"group_layer_id": 2925
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ギャグ笑",
|
|
||||||
"left": 244,
|
|
||||||
"top": 395,
|
|
||||||
"width": 69,
|
|
||||||
"height": 37,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2908,
|
|
||||||
"group_layer_id": 2925
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ギャグ怒",
|
|
||||||
"left": 244,
|
|
||||||
"top": 401,
|
|
||||||
"width": 66,
|
|
||||||
"height": 33,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2913,
|
|
||||||
"group_layer_id": 2925
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ギャグ泣",
|
|
||||||
"left": 244,
|
|
||||||
"top": 405,
|
|
||||||
"width": 66,
|
|
||||||
"height": 47,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2916,
|
|
||||||
"group_layer_id": 2925
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ギャグ驚",
|
|
||||||
"left": 248,
|
|
||||||
"top": 395,
|
|
||||||
"width": 57,
|
|
||||||
"height": 37,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2920,
|
|
||||||
"group_layer_id": 2925
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "追開け",
|
|
||||||
"left": 270,
|
|
||||||
"top": 430,
|
|
||||||
"width": 19,
|
|
||||||
"height": 16,
|
|
||||||
"visible": 1,
|
|
||||||
"layer_id": 2980,
|
|
||||||
"group_layer_id": 2927
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "追閉じ",
|
|
||||||
"left": 271,
|
|
||||||
"top": 430,
|
|
||||||
"width": 18,
|
|
||||||
"height": 14,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2969,
|
|
||||||
"group_layer_id": 2927
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "追真剣",
|
|
||||||
"left": 273,
|
|
||||||
"top": 433,
|
|
||||||
"width": 12,
|
|
||||||
"height": 13,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 3023,
|
|
||||||
"group_layer_id": 2927
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "笑1",
|
|
||||||
"left": 269,
|
|
||||||
"top": 429,
|
|
||||||
"width": 21,
|
|
||||||
"height": 18,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2844,
|
|
||||||
"group_layer_id": 2927
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "笑2",
|
|
||||||
"left": 268,
|
|
||||||
"top": 428,
|
|
||||||
"width": 24,
|
|
||||||
"height": 22,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2847,
|
|
||||||
"group_layer_id": 2927
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "怒1",
|
|
||||||
"left": 272,
|
|
||||||
"top": 433,
|
|
||||||
"width": 15,
|
|
||||||
"height": 12,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2851,
|
|
||||||
"group_layer_id": 2927
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "怒2",
|
|
||||||
"left": 268,
|
|
||||||
"top": 428,
|
|
||||||
"width": 25,
|
|
||||||
"height": 23,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2854,
|
|
||||||
"group_layer_id": 2927
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "悲1",
|
|
||||||
"left": 272,
|
|
||||||
"top": 434,
|
|
||||||
"width": 14,
|
|
||||||
"height": 11,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2857,
|
|
||||||
"group_layer_id": 2927
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "悲2",
|
|
||||||
"left": 272,
|
|
||||||
"top": 435,
|
|
||||||
"width": 15,
|
|
||||||
"height": 12,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2862,
|
|
||||||
"group_layer_id": 2927
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "泣1",
|
|
||||||
"left": 270,
|
|
||||||
"top": 431,
|
|
||||||
"width": 21,
|
|
||||||
"height": 17,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2869,
|
|
||||||
"group_layer_id": 2927
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "泣2",
|
|
||||||
"left": 270,
|
|
||||||
"top": 433,
|
|
||||||
"width": 22,
|
|
||||||
"height": 13,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2876,
|
|
||||||
"group_layer_id": 2927
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "驚1",
|
|
||||||
"left": 272,
|
|
||||||
"top": 432,
|
|
||||||
"width": 14,
|
|
||||||
"height": 16,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2881,
|
|
||||||
"group_layer_id": 2927
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "驚2",
|
|
||||||
"left": 268,
|
|
||||||
"top": 427,
|
|
||||||
"width": 26,
|
|
||||||
"height": 25,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2886,
|
|
||||||
"group_layer_id": 2927
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "照1",
|
|
||||||
"left": 269,
|
|
||||||
"top": 431,
|
|
||||||
"width": 20,
|
|
||||||
"height": 15,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2892,
|
|
||||||
"group_layer_id": 2927
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "照2",
|
|
||||||
"left": 268,
|
|
||||||
"top": 430,
|
|
||||||
"width": 23,
|
|
||||||
"height": 19,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2897,
|
|
||||||
"group_layer_id": 2927
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "えっへん",
|
|
||||||
"left": 269,
|
|
||||||
"top": 429,
|
|
||||||
"width": 22,
|
|
||||||
"height": 20,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2903,
|
|
||||||
"group_layer_id": 2927
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ギャグ笑",
|
|
||||||
"left": 267,
|
|
||||||
"top": 427,
|
|
||||||
"width": 26,
|
|
||||||
"height": 24,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2909,
|
|
||||||
"group_layer_id": 2927
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ギャグ怒",
|
|
||||||
"left": 270,
|
|
||||||
"top": 430,
|
|
||||||
"width": 20,
|
|
||||||
"height": 16,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2914,
|
|
||||||
"group_layer_id": 2927
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ギャグ泣",
|
|
||||||
"left": 270,
|
|
||||||
"top": 434,
|
|
||||||
"width": 19,
|
|
||||||
"height": 11,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2917,
|
|
||||||
"group_layer_id": 2927
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ギャグ驚",
|
|
||||||
"left": 267,
|
|
||||||
"top": 427,
|
|
||||||
"width": 25,
|
|
||||||
"height": 24,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2921,
|
|
||||||
"group_layer_id": 2927
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "G頬",
|
|
||||||
"left": 249,
|
|
||||||
"top": 415,
|
|
||||||
"width": 59,
|
|
||||||
"height": 28,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 813,
|
|
||||||
"group_layer_id": 2314
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "頬1",
|
|
||||||
"left": 246,
|
|
||||||
"top": 410,
|
|
||||||
"width": 65,
|
|
||||||
"height": 35,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2369,
|
|
||||||
"group_layer_id": 2314
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "頬2",
|
|
||||||
"left": 245,
|
|
||||||
"top": 410,
|
|
||||||
"width": 66,
|
|
||||||
"height": 35,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2364,
|
|
||||||
"group_layer_id": 2314
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "頬3",
|
|
||||||
"left": 245,
|
|
||||||
"top": 408,
|
|
||||||
"width": 66,
|
|
||||||
"height": 38,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2360,
|
|
||||||
"group_layer_id": 2314
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "頬4",
|
|
||||||
"left": 242,
|
|
||||||
"top": 406,
|
|
||||||
"width": 69,
|
|
||||||
"height": 44,
|
|
||||||
"visible": 1,
|
|
||||||
"layer_id": 2356,
|
|
||||||
"group_layer_id": 2314
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "頬5",
|
|
||||||
"left": 245,
|
|
||||||
"top": 410,
|
|
||||||
"width": 66,
|
|
||||||
"height": 36,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2352,
|
|
||||||
"group_layer_id": 2314
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "斜め1",
|
|
||||||
"left": 148,
|
|
||||||
"top": 323,
|
|
||||||
"width": 309,
|
|
||||||
"height": 600,
|
|
||||||
"visible": 1,
|
|
||||||
"layer_id": 2346,
|
|
||||||
"group_layer_id": 2086
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "斜め2",
|
|
||||||
"left": 148,
|
|
||||||
"top": 323,
|
|
||||||
"width": 309,
|
|
||||||
"height": 600,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2345,
|
|
||||||
"group_layer_id": 2086
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "斜め1",
|
|
||||||
"left": 147,
|
|
||||||
"top": 323,
|
|
||||||
"width": 313,
|
|
||||||
"height": 600,
|
|
||||||
"visible": 1,
|
|
||||||
"layer_id": 2934,
|
|
||||||
"group_layer_id": 2090
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "斜め2",
|
|
||||||
"left": 147,
|
|
||||||
"top": 323,
|
|
||||||
"width": 313,
|
|
||||||
"height": 600,
|
|
||||||
"visible": 1,
|
|
||||||
"layer_id": 2933,
|
|
||||||
"group_layer_id": 2090
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "斜め1",
|
|
||||||
"left": 137,
|
|
||||||
"top": 323,
|
|
||||||
"width": 367,
|
|
||||||
"height": 602,
|
|
||||||
"visible": 1,
|
|
||||||
"layer_id": 2340,
|
|
||||||
"group_layer_id": 2094
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "斜め2",
|
|
||||||
"left": 137,
|
|
||||||
"top": 323,
|
|
||||||
"width": 367,
|
|
||||||
"height": 602,
|
|
||||||
"visible": 1,
|
|
||||||
"layer_id": 2339,
|
|
||||||
"group_layer_id": 2094
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "斜め1",
|
|
||||||
"left": 148,
|
|
||||||
"top": 323,
|
|
||||||
"width": 294,
|
|
||||||
"height": 599,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2338,
|
|
||||||
"group_layer_id": 2098
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "斜め2",
|
|
||||||
"left": 148,
|
|
||||||
"top": 323,
|
|
||||||
"width": 290,
|
|
||||||
"height": 599,
|
|
||||||
"visible": 1,
|
|
||||||
"layer_id": 2337,
|
|
||||||
"group_layer_id": 2098
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "汗",
|
|
||||||
"left": 0,
|
|
||||||
"top": 0,
|
|
||||||
"width": 0,
|
|
||||||
"height": 0,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2931,
|
|
||||||
"group_layer_id": 974
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "眉",
|
|
||||||
"left": 0,
|
|
||||||
"top": 0,
|
|
||||||
"width": 0,
|
|
||||||
"height": 0,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2923,
|
|
||||||
"group_layer_id": 974
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "目",
|
|
||||||
"left": 0,
|
|
||||||
"top": 0,
|
|
||||||
"width": 0,
|
|
||||||
"height": 0,
|
|
||||||
"visible": 1,
|
|
||||||
"layer_id": 2925,
|
|
||||||
"group_layer_id": 974
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "口",
|
|
||||||
"left": 0,
|
|
||||||
"top": 0,
|
|
||||||
"width": 0,
|
|
||||||
"height": 0,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2927,
|
|
||||||
"group_layer_id": 974
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "頬",
|
|
||||||
"left": 0,
|
|
||||||
"top": 0,
|
|
||||||
"width": 0,
|
|
||||||
"height": 0,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2314,
|
|
||||||
"group_layer_id": 975
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "喫茶店服",
|
|
||||||
"left": 0,
|
|
||||||
"top": 0,
|
|
||||||
"width": 0,
|
|
||||||
"height": 0,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2086,
|
|
||||||
"group_layer_id": 975
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "パジャマ",
|
|
||||||
"left": 0,
|
|
||||||
"top": 0,
|
|
||||||
"width": 0,
|
|
||||||
"height": 0,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2090,
|
|
||||||
"group_layer_id": 975
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "私服2",
|
|
||||||
"left": 0,
|
|
||||||
"top": 0,
|
|
||||||
"width": 0,
|
|
||||||
"height": 0,
|
|
||||||
"visible": 0,
|
|
||||||
"layer_id": 2094,
|
|
||||||
"group_layer_id": 975
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "私服1",
|
|
||||||
"left": 0,
|
|
||||||
"top": 0,
|
|
||||||
"width": 0,
|
|
||||||
"height": 0,
|
|
||||||
"visible": 1,
|
|
||||||
"layer_id": 2098,
|
|
||||||
"group_layer_id": 975
|
|
||||||
}
|
|
||||||
]
|
|
|
@ -1,32 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { Icon } from '@iconify/vue'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<RouterLink to="/kun" class="return">
|
|
||||||
<Icon class="icon" icon="line-md:home-md-twotone" />
|
|
||||||
<span>{{ $tm('back.home') }}</span>
|
|
||||||
</RouterLink>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.return {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 1%;
|
|
||||||
right: 2%;
|
|
||||||
color: var(--kungalgame-font-color-0);
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
font-size: 18px;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.return:hover {
|
|
||||||
color: var(--kungalgame-blue-4);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -5,6 +5,7 @@ const router = useRouter()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<!-- Back to homepage -->
|
||||||
<div class="return" @click="router.back()">
|
<div class="return" @click="router.back()">
|
||||||
<span>{{ `< ${$tm('back.back')}` }}</span>
|
<span>{{ `< ${$tm('back.back')}` }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,27 +1,9 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts"></script>
|
||||||
import { Icon } from '@iconify/vue'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<div>{{ $tm('footer.copyright') }}</div>
|
<span>Copyright © 2023 KUNGalgame</span>
|
||||||
|
<span>All rights reserved | Version 1.0.0</span>
|
||||||
<div>
|
|
||||||
<span>{{ $tm('footer.openSource') }}</span>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="http://github.com/KUN1007/kun-galgame-vue"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
aria-label="KUN Visual Novel Open Source GitHub Repository. 鲲 Galgame 开源 GitHub 仓库."
|
|
||||||
>
|
|
||||||
<Icon icon="line-md:github-loop" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<span>{{ $tm('footer.reserved') }} 1.3.0</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -34,25 +16,5 @@ import { Icon } from '@iconify/vue'
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 14px;
|
|
||||||
|
|
||||||
div {
|
|
||||||
display: flex;
|
|
||||||
justify-self: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
a {
|
|
||||||
display: flex;
|
|
||||||
justify-self: center;
|
|
||||||
align-items: center;
|
|
||||||
margin-left: 5px;
|
|
||||||
color: var(--kungalgame-blue-5);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transition: all 0.2s;
|
|
||||||
transform: scale(1.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
309
src/components/KUNGalgameSearchBox.vue
Normal file
309
src/components/KUNGalgameSearchBox.vue
Normal file
|
@ -0,0 +1,309 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue'
|
||||||
|
// Import debounce function
|
||||||
|
import { debounce } from '@/utils/debounce'
|
||||||
|
import { ref, onBeforeMount } from 'vue'
|
||||||
|
// Import user store
|
||||||
|
import { useKUNGalgameHomeStore } from '@/store/modules/home'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
const { keywords, searchHistory, category } = storeToRefs(
|
||||||
|
useKUNGalgameHomeStore()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Value of the input field
|
||||||
|
const inputValue = ref('')
|
||||||
|
// Whether to show search history
|
||||||
|
const isShowSearchHistory = ref(false)
|
||||||
|
// Style when the input field is active
|
||||||
|
const inputActiveClass = ref({})
|
||||||
|
|
||||||
|
// Define props to tell the input field in which category to search for topics
|
||||||
|
const props = defineProps(['category'])
|
||||||
|
|
||||||
|
// Initialize the content of the search box to prevent content from persisting after page refresh
|
||||||
|
// Assign the topic category to be searched because the search box will be rendered on three pages
|
||||||
|
// , corresponding to three categories
|
||||||
|
onBeforeMount(() => {
|
||||||
|
keywords.value = ''
|
||||||
|
category.value = props.category
|
||||||
|
})
|
||||||
|
|
||||||
|
// Define the debounce handling function
|
||||||
|
const debouncedSearch = debounce((inputValue: string) => {
|
||||||
|
// Reset page status and loading state before searching
|
||||||
|
useKUNGalgameHomeStore().resetPageStatus()
|
||||||
|
keywords.value = inputValue
|
||||||
|
}, 300) // 300 milliseconds debounce delay
|
||||||
|
|
||||||
|
// When the search box is focused
|
||||||
|
const handleInputFocus = () => {
|
||||||
|
if (searchHistory.value.length !== 0) {
|
||||||
|
isShowSearchHistory.value = true
|
||||||
|
}
|
||||||
|
inputActiveClass.value = {
|
||||||
|
backgroundColor: 'var(--kungalgame-white)',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the search box is blurred
|
||||||
|
const handleInputBlur = () => {
|
||||||
|
// Delay hiding the search history so that clicking on the search history can trigger the fill event
|
||||||
|
setTimeout(() => {
|
||||||
|
isShowSearchHistory.value = false
|
||||||
|
inputActiveClass.value = {}
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search function logic
|
||||||
|
const search = () => {
|
||||||
|
debouncedSearch(inputValue.value)
|
||||||
|
if (!searchHistory.value.includes(inputValue.value)) {
|
||||||
|
// Push the element into the array only when there are no identical elements in the array
|
||||||
|
searchHistory.value.push(inputValue.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the user presses Enter
|
||||||
|
const handleClickEnter = (event: KeyboardEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
search()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clicking the search button triggers the search logic
|
||||||
|
const handleClickSearch = () => {
|
||||||
|
if (inputValue.value.trim()) {
|
||||||
|
search()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clicking on search history
|
||||||
|
const handleClickHistory = (index: number) => {
|
||||||
|
inputValue.value = searchHistory.value[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear search history
|
||||||
|
const clearSearchHistory = () => {
|
||||||
|
searchHistory.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete search history
|
||||||
|
const handleDeleteHistory = (historyIndex: number) => {
|
||||||
|
searchHistory.value.splice(historyIndex, 1)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- Interactive area search box -->
|
||||||
|
<div class="container">
|
||||||
|
<!-- Search box form -->
|
||||||
|
<form class="search-form">
|
||||||
|
<!-- Search box content -->
|
||||||
|
<div class="content">
|
||||||
|
<!-- Input field -->
|
||||||
|
<input
|
||||||
|
v-model="inputValue"
|
||||||
|
type="search"
|
||||||
|
class="input"
|
||||||
|
:style="inputActiveClass"
|
||||||
|
:placeholder="`${$tm('mainPage.header.search')}`"
|
||||||
|
@focus="handleInputFocus"
|
||||||
|
@blur="handleInputBlur"
|
||||||
|
@input="debouncedSearch(inputValue)"
|
||||||
|
@keydown.enter="handleClickEnter"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- Search box icon -->
|
||||||
|
<div class="search-btn" @click="handleClickSearch">
|
||||||
|
<Icon icon="line-md:search" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<!-- Search history container -->
|
||||||
|
<div v-if="isShowSearchHistory" class="history">
|
||||||
|
<!-- Search history title -->
|
||||||
|
<div class="title">
|
||||||
|
<span>{{ $tm('mainPage.header.history') }}</span>
|
||||||
|
<span @click="clearSearchHistory">
|
||||||
|
{{ $tm('mainPage.header.clear') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- Search history -->
|
||||||
|
<div class="history-container">
|
||||||
|
<div
|
||||||
|
class="single-history"
|
||||||
|
v-for="(history, index) in searchHistory"
|
||||||
|
:key="index"
|
||||||
|
@click="handleClickHistory(index)"
|
||||||
|
>
|
||||||
|
<span>{{ history }} </span>
|
||||||
|
<span @click="handleDeleteHistory(index)">
|
||||||
|
<Icon class="delete" icon="line-md:close-circle" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
/* Search topics */
|
||||||
|
.container {
|
||||||
|
height: 39px;
|
||||||
|
width: 1px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
/* Prevent line breaks when the page is resized */
|
||||||
|
white-space: nowrap;
|
||||||
|
background-color: var(--kungalgame-trans-blue-2);
|
||||||
|
border: 1px solid var(--kungalgame-blue-4);
|
||||||
|
flex-grow: 2;
|
||||||
|
/* Position relative to the secondary menu */
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
color: var(--kungalgame-font-color-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search box form */
|
||||||
|
.search-form {
|
||||||
|
display: flex;
|
||||||
|
height: 39px;
|
||||||
|
/* Grows with the page */
|
||||||
|
width: 1px;
|
||||||
|
flex-grow: 1;
|
||||||
|
/* Centered */
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search content area */
|
||||||
|
.content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input field */
|
||||||
|
.input {
|
||||||
|
padding: 0 15px;
|
||||||
|
height: 39px;
|
||||||
|
width: 100%;
|
||||||
|
/* Font size for input during search */
|
||||||
|
font-size: 16px;
|
||||||
|
border: none;
|
||||||
|
background-color: var(--kungalgame-trans-white-5);
|
||||||
|
color: var(--kungalgame-font-color-3);
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--kungalgame-font-color-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search button */
|
||||||
|
.search-btn {
|
||||||
|
/* Square shape, does not shrink */
|
||||||
|
height: 39px;
|
||||||
|
width: 39px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-left: 1px solid var(--kungalgame-trans-blue-4);
|
||||||
|
/* Center the search icon */
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--kungalgame-red-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: var(--kungalgame-red-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search history container */
|
||||||
|
.history {
|
||||||
|
width: 100%;
|
||||||
|
/* Absolute positioning relative to the search area in the nav */
|
||||||
|
position: absolute;
|
||||||
|
/* Tight positioning under the topic search area */
|
||||||
|
top: 39px;
|
||||||
|
left: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: var(--kungalgame-white);
|
||||||
|
color: var(--kungalgame-font-color-3);
|
||||||
|
border: 1px solid var(--kungalgame-red-1);
|
||||||
|
border-radius: 7px;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text for the search history title */
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
margin: 10px;
|
||||||
|
/* Distribute two hint texts left and right */
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 14px;
|
||||||
|
&:nth-child(2) {
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1.5px solid var(--kungalgame-trans-white-5);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-bottom: 1.5px solid var(--kungalgame-blue-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Container for storing search history tags */
|
||||||
|
.history-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
/* Font for individual search records */
|
||||||
|
font-size: 13px;
|
||||||
|
/* Blank space on the left and right sides of the search record */
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.single-history {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space between;
|
||||||
|
padding: 7px 3px;
|
||||||
|
margin: 2px 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--kungalgame-blue-4);
|
||||||
|
|
||||||
|
.delete {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span:nth-child(1) {
|
||||||
|
cursor: default;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
span:nth-child(2) {
|
||||||
|
width: 17px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Delete button */
|
||||||
|
.delete {
|
||||||
|
width: 30px;
|
||||||
|
right: 5px;
|
||||||
|
font-size: 17px;
|
||||||
|
position: absolute;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--kungalgame-font-color-0);
|
||||||
|
background-color: var(--kungalgame-white);
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,17 +1,19 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useTempMessageStore } from '@/store/temp/message'
|
import { useKUNGalgameMessageStore } from '@/store/modules/message'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
|
|
||||||
const { showAlert, alertMsg, isShowCancel } = storeToRefs(useTempMessageStore())
|
const { showAlert, alertMsg, isShowCancel } = storeToRefs(
|
||||||
|
useKUNGalgameMessageStore()
|
||||||
|
)
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
showAlert.value = false
|
showAlert.value = false
|
||||||
useTempMessageStore().handleClose()
|
useKUNGalgameMessageStore().handleClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleConfirm = () => {
|
const handleConfirm = () => {
|
||||||
showAlert.value = false
|
showAlert.value = false
|
||||||
useTempMessageStore().handleConfirm()
|
useKUNGalgameMessageStore().handleConfirm()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -46,7 +48,7 @@ const handleConfirm = () => {
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: var(--kungalgame-mask-color-0);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
display: flex;
|
display: flex;
|
||||||
transition: opacity 0.3s ease;
|
transition: opacity 0.3s ease;
|
||||||
color: var(--kungalgame-font-color-3);
|
color: var(--kungalgame-font-color-3);
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Icon } from '@iconify/vue'
|
import { Icon } from '@iconify/vue'
|
||||||
import { useTempMessageStore } from '@/store/temp/message'
|
import { useKUNGalgameMessageStore } from '@/store/modules/message'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import img from './loli'
|
import img from './loli'
|
||||||
import 'animate.css'
|
import 'animate.css'
|
||||||
|
|
||||||
const { showInfo, infoMsg } = storeToRefs(useTempMessageStore())
|
const { showInfo, infoMsg } = storeToRefs(useKUNGalgameMessageStore())
|
||||||
|
|
||||||
const { loli, name } = img
|
const { loli, name } = img
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ const handleClose = () => {
|
||||||
min-height: 120px;
|
min-height: 120px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
color: var(--kungalgame-font-color-3);
|
color: var(--kungalgame-font-color-3);
|
||||||
background-color: var(--kungalgame-trans-white-2);
|
background-color: var(--kungalgame-trans-white-5);
|
||||||
backdrop-filter: blur(2px);
|
backdrop-filter: blur(2px);
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
border-top: 1px solid var(--kungalgame-blue-1);
|
border-top: 1px solid var(--kungalgame-blue-1);
|
||||||
|
|
|
@ -1,17 +1,21 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
|
// Import questions
|
||||||
import { questionsEN, Question } from './questionsEN'
|
import { questionsEN, Question } from './questionsEN'
|
||||||
import { questionsCN } from './questionsCN'
|
import { questionsCN } from './questionsCN'
|
||||||
|
// Global message component (top)
|
||||||
import Message from '@/components/alert/Message'
|
import Message from '@/components/alert/Message'
|
||||||
|
// Import message store
|
||||||
import { useTempMessageStore } from '@/store/temp/message'
|
import { useKUNGalgameMessageStore } from '@/store/modules/message'
|
||||||
|
// Import settings component to get the language
|
||||||
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
|
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
|
|
||||||
|
// Use the settings store to get the language
|
||||||
const { showKUNGalgameLanguage } = storeToRefs(useKUNGalgameSettingsStore())
|
const { showKUNGalgameLanguage } = storeToRefs(useKUNGalgameSettingsStore())
|
||||||
|
// Variables from the message component
|
||||||
const { isShowCapture, isCaptureSuccessful } = storeToRefs(
|
const { isShowCapture, isCaptureSuccessful } = storeToRefs(
|
||||||
useTempMessageStore()
|
useKUNGalgameMessageStore()
|
||||||
)
|
)
|
||||||
// Current language
|
// Current language
|
||||||
const questions = ref<Question[]>([])
|
const questions = ref<Question[]>([])
|
||||||
|
@ -37,7 +41,9 @@ const userAnswers = ref('')
|
||||||
// Current question index
|
// Current question index
|
||||||
const currentQuestionIndex = ref(randomizeQuestion())
|
const currentQuestionIndex = ref(randomizeQuestion())
|
||||||
// Current question
|
// Current question
|
||||||
const currentQuestion = ref(questions.value[currentQuestionIndex.value])
|
const currentQuestion = computed(
|
||||||
|
() => questions.value[currentQuestionIndex.value]
|
||||||
|
)
|
||||||
// Error count
|
// Error count
|
||||||
const errorCounter = ref(0)
|
const errorCounter = ref(0)
|
||||||
const expectedKeys = ref(['k', 'u', 'n'])
|
const expectedKeys = ref(['k', 'u', 'n'])
|
||||||
|
@ -47,16 +53,6 @@ const isShowHint = ref(false)
|
||||||
// Whether to show the answer
|
// Whether to show the answer
|
||||||
const isShowAnswer = ref(false)
|
const isShowAnswer = ref(false)
|
||||||
|
|
||||||
const resetStatus = () => {
|
|
||||||
userAnswers.value = ''
|
|
||||||
currentQuestionIndex.value = randomizeQuestion()
|
|
||||||
currentQuestion.value = questions.value[currentQuestionIndex.value]
|
|
||||||
errorCounter.value = 0
|
|
||||||
currentIndex.value = 0
|
|
||||||
isShowHint.value = false
|
|
||||||
isShowAnswer.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listen to keyboard events
|
// Listen to keyboard events
|
||||||
const checkKeyPress = (event: KeyboardEvent) => {
|
const checkKeyPress = (event: KeyboardEvent) => {
|
||||||
const pressedKey = event.key
|
const pressedKey = event.key
|
||||||
|
@ -91,7 +87,6 @@ const submitAnswer = () => {
|
||||||
'人机身份验证通过 ~',
|
'人机身份验证通过 ~',
|
||||||
'success'
|
'success'
|
||||||
)
|
)
|
||||||
resetStatus()
|
|
||||||
} else {
|
} else {
|
||||||
// Wrong answer
|
// Wrong answer
|
||||||
errorCounter.value++
|
errorCounter.value++
|
||||||
|
@ -109,12 +104,6 @@ const submitAnswer = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close panel
|
|
||||||
const handleCloseCapture = () => {
|
|
||||||
isShowCapture.value = false
|
|
||||||
resetStatus()
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -152,7 +141,7 @@ const handleCloseCapture = () => {
|
||||||
<button @click="submitAnswer">
|
<button @click="submitAnswer">
|
||||||
{{ $tm('AlertInfo.capture.submit') }}
|
{{ $tm('AlertInfo.capture.submit') }}
|
||||||
</button>
|
</button>
|
||||||
<button @click="handleCloseCapture">
|
<button @click="isShowCapture = false">
|
||||||
{{ $tm('AlertInfo.capture.close') }}
|
{{ $tm('AlertInfo.capture.close') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -193,7 +182,7 @@ const handleCloseCapture = () => {
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: var(--kungalgame-mask-color-0);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
display: flex;
|
display: flex;
|
||||||
transition: opacity 0.3s ease;
|
transition: opacity 0.3s ease;
|
||||||
color: var(--kungalgame-font-color-3);
|
color: var(--kungalgame-font-color-3);
|
||||||
|
|
|
@ -1,108 +0,0 @@
|
||||||
<!-- Reference: https://loading.io/css/ -->
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const count = 7
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="loading">
|
|
||||||
<div v-for="(_, index) in count" :key="index"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.loading {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
width: 77px;
|
|
||||||
height: 77px;
|
|
||||||
|
|
||||||
div {
|
|
||||||
animation: roll 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
|
||||||
transform-origin: 40px 40px;
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
content: ' ';
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
width: 7px;
|
|
||||||
height: 7px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--kungalgame-blue-4);
|
|
||||||
margin: -4px 0 0 -4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(1) {
|
|
||||||
animation-delay: -0.036s;
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
top: 63px;
|
|
||||||
left: 63px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(2) {
|
|
||||||
animation-delay: -0.072s;
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
top: 68px;
|
|
||||||
left: 56px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(3) {
|
|
||||||
animation-delay: -0.108s;
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
top: 71px;
|
|
||||||
left: 48px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(4) {
|
|
||||||
animation-delay: -0.144s;
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
top: 72px;
|
|
||||||
left: 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(5) {
|
|
||||||
animation-delay: -0.18s;
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
top: 71px;
|
|
||||||
left: 32px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(6) {
|
|
||||||
animation-delay: -0.216s;
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
top: 68px;
|
|
||||||
left: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(7) {
|
|
||||||
animation-delay: -0.252s;
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
top: 63px;
|
|
||||||
left: 17px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes roll {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,71 +0,0 @@
|
||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<span class="loader"></span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.loader {
|
|
||||||
width: 0;
|
|
||||||
height: 4.8px;
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
background: var(--kungalgame-blue-4);
|
|
||||||
box-shadow: var(--kungalgame-shadow-0);
|
|
||||||
box-sizing: border-box;
|
|
||||||
animation: animFw 8s linear infinite;
|
|
||||||
}
|
|
||||||
.loader::after,
|
|
||||||
.loader::before {
|
|
||||||
content: '';
|
|
||||||
width: 10px;
|
|
||||||
height: 1px;
|
|
||||||
background: linear-gradient(
|
|
||||||
var(--kungalgame-trans-pink-1),
|
|
||||||
var(--kungalgame-trans-blue-1)
|
|
||||||
);
|
|
||||||
position: absolute;
|
|
||||||
top: 9px;
|
|
||||||
right: -2px;
|
|
||||||
opacity: 0;
|
|
||||||
transform: rotate(-45deg) translateX(0px);
|
|
||||||
box-sizing: border-box;
|
|
||||||
animation: coli1 0.3s linear infinite;
|
|
||||||
}
|
|
||||||
.loader::before {
|
|
||||||
top: -4px;
|
|
||||||
transform: rotate(45deg);
|
|
||||||
animation: coli2 0.3s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes animFw {
|
|
||||||
0% {
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes coli1 {
|
|
||||||
0% {
|
|
||||||
transform: rotate(-45deg) translateX(0px);
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(-45deg) translateX(-45px);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes coli2 {
|
|
||||||
0% {
|
|
||||||
transform: rotate(45deg) translateX(0px);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(45deg) translateX(-45px);
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,284 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, ref } from 'vue'
|
|
||||||
// KUN Visual Novel Menu
|
|
||||||
import MilkdownMenu from './plugins/MilkdownMenu.vue'
|
|
||||||
// Milkdown core
|
|
||||||
import { Editor, rootCtx, rootAttrsCtx, defaultValueCtx } from '@milkdown/core'
|
|
||||||
import { Milkdown, useEditor } from '@milkdown/vue'
|
|
||||||
import { commonmark } from '@milkdown/preset-commonmark'
|
|
||||||
import { gfm } from '@milkdown/preset-gfm'
|
|
||||||
// Milkdown Plugins
|
|
||||||
import { history } from '@milkdown/plugin-history'
|
|
||||||
import { prism, prismConfig } from '@milkdown/plugin-prism'
|
|
||||||
import { listener, listenerCtx } from '@milkdown/plugin-listener'
|
|
||||||
import { clipboard } from '@milkdown/plugin-clipboard'
|
|
||||||
import { indent } from '@milkdown/plugin-indent'
|
|
||||||
import { trailing } from '@milkdown/plugin-trailing'
|
|
||||||
import { usePluginViewFactory } from '@prosemirror-adapter/vue'
|
|
||||||
// KUN Visual Novel Custom tooltip
|
|
||||||
import { tooltipFactory } from '@milkdown/plugin-tooltip'
|
|
||||||
import Tooltip from './plugins/Tooltip.vue'
|
|
||||||
// Custom text size calculate
|
|
||||||
import Size from './plugins/Size.vue'
|
|
||||||
import { $prose } from '@milkdown/utils'
|
|
||||||
import { Plugin } from '@milkdown/prose/state'
|
|
||||||
|
|
||||||
// KUN Visual Novel style
|
|
||||||
import '@/styles/editor/index.scss'
|
|
||||||
|
|
||||||
// Syntax highlight
|
|
||||||
import c from 'refractor/lang/c'
|
|
||||||
import cpp from 'refractor/lang/cpp'
|
|
||||||
import csharp from 'refractor/lang/csharp'
|
|
||||||
import css from 'refractor/lang/css'
|
|
||||||
import go from 'refractor/lang/go'
|
|
||||||
import haskell from 'refractor/lang/haskell'
|
|
||||||
import python from 'refractor/lang/python'
|
|
||||||
import java from 'refractor/lang/java'
|
|
||||||
import javascript from 'refractor/lang/javascript'
|
|
||||||
import typescript from 'refractor/lang/typescript'
|
|
||||||
import jsx from 'refractor/lang/jsx'
|
|
||||||
import kotlin from 'refractor/lang/kotlin'
|
|
||||||
import r from 'refractor/lang/r'
|
|
||||||
import rust from 'refractor/lang/rust'
|
|
||||||
import scala from 'refractor/lang/scala'
|
|
||||||
import sql from 'refractor/lang/sql'
|
|
||||||
import tsx from 'refractor/lang/tsx'
|
|
||||||
import markdown from 'refractor/lang/markdown'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
valueMarkdown: string
|
|
||||||
editorHight: string
|
|
||||||
isShowMenu: boolean
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emits = defineEmits<{
|
|
||||||
saveMarkdown: [editorMarkdown: string]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const editorHight = computed(() => props.editorHight + 'px')
|
|
||||||
const valueMarkdown = computed(() => props.valueMarkdown)
|
|
||||||
const isShowMenu = computed(() => props.isShowMenu)
|
|
||||||
|
|
||||||
const tooltip = tooltipFactory('Text')
|
|
||||||
const pluginViewFactory = usePluginViewFactory()
|
|
||||||
const container = ref<HTMLElement | null>(null)
|
|
||||||
const isEditorFocus = ref(false)
|
|
||||||
const editorContent = ref('')
|
|
||||||
|
|
||||||
const editorInfo = useEditor((root) =>
|
|
||||||
Editor.make()
|
|
||||||
.config((ctx) => {
|
|
||||||
ctx.set(rootCtx, root)
|
|
||||||
ctx.set(rootAttrsCtx, {
|
|
||||||
roles: 'kun-galgame-milkdown-editor',
|
|
||||||
'aria-label': 'kun-galgame-milkdown-editor',
|
|
||||||
})
|
|
||||||
ctx.set(defaultValueCtx, valueMarkdown.value)
|
|
||||||
|
|
||||||
const listener = ctx.get(listenerCtx)
|
|
||||||
listener.markdownUpdated((ctx, markdown, prevMarkdown) => {
|
|
||||||
if (markdown !== prevMarkdown) {
|
|
||||||
editorContent.value = markdown
|
|
||||||
emits('saveMarkdown', markdown)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
listener.blur(() => {
|
|
||||||
isEditorFocus.value = false
|
|
||||||
})
|
|
||||||
listener.focus(() => {
|
|
||||||
isEditorFocus.value = true
|
|
||||||
})
|
|
||||||
|
|
||||||
ctx.set(prismConfig.key, {
|
|
||||||
configureRefractor: (refractor) => {
|
|
||||||
refractor.register(c)
|
|
||||||
refractor.register(cpp)
|
|
||||||
refractor.register(csharp)
|
|
||||||
refractor.register(css)
|
|
||||||
refractor.register(go)
|
|
||||||
refractor.register(haskell)
|
|
||||||
refractor.register(python)
|
|
||||||
refractor.register(markdown)
|
|
||||||
refractor.register(java)
|
|
||||||
refractor.register(javascript)
|
|
||||||
refractor.register(typescript)
|
|
||||||
refractor.register(jsx)
|
|
||||||
refractor.register(kotlin)
|
|
||||||
refractor.register(r)
|
|
||||||
refractor.register(rust)
|
|
||||||
refractor.register(scala)
|
|
||||||
refractor.register(sql)
|
|
||||||
refractor.register(tsx)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
ctx.set(tooltip.key, {
|
|
||||||
view: pluginViewFactory({
|
|
||||||
component: Tooltip,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.use(history)
|
|
||||||
.use(commonmark)
|
|
||||||
.use(gfm)
|
|
||||||
.use(prism)
|
|
||||||
.use(listener)
|
|
||||||
.use(clipboard)
|
|
||||||
.use(indent)
|
|
||||||
.use(trailing)
|
|
||||||
.use(tooltip)
|
|
||||||
// Add custom plugin view, calculate markdown text size
|
|
||||||
.use(
|
|
||||||
$prose(
|
|
||||||
() =>
|
|
||||||
new Plugin({
|
|
||||||
view: pluginViewFactory({
|
|
||||||
component: Size,
|
|
||||||
root: () => (container.value ? container.value : root),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- MilkdownEditor.vue -->
|
|
||||||
<template>
|
|
||||||
<div ref="container" class="editor-container">
|
|
||||||
<MilkdownMenu v-if="isShowMenu" :editorInfo="editorInfo" />
|
|
||||||
<Milkdown
|
|
||||||
class="editor"
|
|
||||||
:class="isEditorFocus || editorContent ? 'active' : ''"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.editor {
|
|
||||||
position: relative;
|
|
||||||
&::before {
|
|
||||||
position: absolute;
|
|
||||||
padding: 27px 10px;
|
|
||||||
content: 'Moe Moe Moe!';
|
|
||||||
font-style: oblique;
|
|
||||||
color: var(--kungalgame-blue-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.milkdown) {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
|
|
||||||
/* Silence css check */
|
|
||||||
* {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > div:nth-child(1) {
|
|
||||||
transition: all 0.2s;
|
|
||||||
margin: 0 auto;
|
|
||||||
min-height: v-bind(editorHight);
|
|
||||||
overflow-y: scroll;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
display: inline;
|
|
||||||
width: 7px;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
cursor: default;
|
|
||||||
background: var(--kungalgame-blue-4);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Compatible with Firefox */
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: var(--kungalgame-blue-4) var(--kungalgame-blue-1); /* Firefox 64+ */
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
del {
|
|
||||||
text-decoration: line-through;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 17px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote {
|
|
||||||
margin: 17px 0;
|
|
||||||
padding: 10px;
|
|
||||||
font-size: 18px;
|
|
||||||
border-left: 4px solid var(--kungalgame-blue-4);
|
|
||||||
background-color: var(--kungalgame-trans-blue-0);
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
margin: 17px 0;
|
|
||||||
border: 1px solid var(--kungalgame-blue-4);
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 17px;
|
|
||||||
background-color: var(--kungalgame-trans-white-2);
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
code {
|
|
||||||
font-size: 15px;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
cursor: pointer;
|
|
||||||
font-style: oblique;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--kungalgame-blue-4);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
border: 1px solid var(--kungalgame-blue-4);
|
|
||||||
border-radius: 5px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
th,
|
|
||||||
td {
|
|
||||||
border: 1px solid var(--kungalgame-blue-4);
|
|
||||||
padding: 3px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr:nth-child(even) {
|
|
||||||
background-color: var(--kungalgame-trans-blue-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
ul li,
|
|
||||||
ol li {
|
|
||||||
color: var(--kungalgame-blue-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tableWrapper {
|
|
||||||
color: var(--kungalgame-font-color-3);
|
|
||||||
position: relative;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.active {
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,123 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, ref, onBeforeMount } from 'vue'
|
|
||||||
import { useRoute } from 'vue-router'
|
|
||||||
import { debounce } from '@/utils/debounce'
|
|
||||||
// Milkdown
|
|
||||||
import { MilkdownProvider } from '@milkdown/vue'
|
|
||||||
import { ProsemirrorAdapterProvider } from '@prosemirror-adapter/vue'
|
|
||||||
import MilkdownEditor from './MilkdownEditor.vue'
|
|
||||||
|
|
||||||
// KUN Visual Novel store
|
|
||||||
import { useTempEditStore } from '@/store/temp/edit'
|
|
||||||
import { useKUNGalgameEditStore } from '@/store/modules/edit'
|
|
||||||
import { useTempReplyStore } from '@/store/temp/topic/reply'
|
|
||||||
import { usePersistKUNGalgameReplyStore } from '@/store/modules/topic/reply'
|
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
|
|
||||||
const { content: rewriteContent, isTopicRewriting } = storeToRefs(
|
|
||||||
useTempEditStore()
|
|
||||||
)
|
|
||||||
const {
|
|
||||||
editorHeight: editEditorHeight,
|
|
||||||
isSaveTopic,
|
|
||||||
content: editContent,
|
|
||||||
} = storeToRefs(useKUNGalgameEditStore())
|
|
||||||
const { isReplyRewriting, replyRewrite } = storeToRefs(useTempReplyStore())
|
|
||||||
const {
|
|
||||||
editorHeight: replyEditorHeight,
|
|
||||||
isSaveReply,
|
|
||||||
replyDraft,
|
|
||||||
} = storeToRefs(usePersistKUNGalgameReplyStore())
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
isShowMenu: boolean
|
|
||||||
}>()
|
|
||||||
const isShowMenu = computed(() => props.isShowMenu)
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
// Current page route name
|
|
||||||
const routeName = computed(() => route.name as string)
|
|
||||||
const valueMarkdown = ref('')
|
|
||||||
|
|
||||||
// Editor height, determined by the route name
|
|
||||||
const editorHeightStyle = computed(() =>
|
|
||||||
routeName.value === 'Edit' ? editEditorHeight.value : replyEditorHeight.value
|
|
||||||
)
|
|
||||||
|
|
||||||
onBeforeMount(() => {
|
|
||||||
/**
|
|
||||||
* Editor is in the edit mode
|
|
||||||
*/
|
|
||||||
// Load topic data before mounting if not saved (and must be on the Edit page)
|
|
||||||
if (isSaveTopic.value && routeName.value === 'Edit') {
|
|
||||||
valueMarkdown.value = editContent.value
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Editor is in the re-editing edit mode
|
|
||||||
*/
|
|
||||||
// Load data for re-editing a topic before mounting
|
|
||||||
if (isTopicRewriting.value && routeName.value === 'Edit') {
|
|
||||||
valueMarkdown.value = rewriteContent.value
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Editor is in the reply mode
|
|
||||||
*/
|
|
||||||
// Load reply data before mounting if not saved (and must be on the Topic page)
|
|
||||||
if (isSaveReply.value && routeName.value === 'Topic') {
|
|
||||||
valueMarkdown.value = replyDraft.value.content
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Editor is in the re-editing reply mode
|
|
||||||
*/
|
|
||||||
if (isReplyRewriting.value && routeName.value === 'Topic') {
|
|
||||||
valueMarkdown.value = replyRewrite.value.content
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const saveMarkdown = (editorMarkdown: string) =>
|
|
||||||
debounce(() => {
|
|
||||||
/**
|
|
||||||
* Editor is in edit mode
|
|
||||||
*/
|
|
||||||
// Save to the edit store if not in topic re-edit mode
|
|
||||||
if (!isTopicRewriting.value && routeName.value === 'Edit') {
|
|
||||||
editContent.value = editorMarkdown
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Editor is in re-editing edit mode
|
|
||||||
*/
|
|
||||||
// Load data for re-editing a topic before mounting
|
|
||||||
if (isTopicRewriting.value && routeName.value === 'Edit') {
|
|
||||||
rewriteContent.value = editorMarkdown
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Editor is in reply mode
|
|
||||||
*/
|
|
||||||
// Save to the reply store if not in reply re-edit mode
|
|
||||||
if (!isReplyRewriting.value && routeName.value === 'Topic') {
|
|
||||||
replyDraft.value.content = editorMarkdown
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Editor is in re-editing reply mode
|
|
||||||
*/
|
|
||||||
if (isReplyRewriting.value && routeName.value === 'Topic') {
|
|
||||||
replyRewrite.value.content = editorMarkdown
|
|
||||||
}
|
|
||||||
}, 1007)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- MilkdownEditorWrapper.vue -->
|
|
||||||
<template>
|
|
||||||
<div class="editor">
|
|
||||||
<MilkdownProvider>
|
|
||||||
<ProsemirrorAdapterProvider>
|
|
||||||
<MilkdownEditor
|
|
||||||
@save-markdown="saveMarkdown"
|
|
||||||
:value-markdown="valueMarkdown"
|
|
||||||
:editor-hight="editorHeightStyle.toString()"
|
|
||||||
:is-show-menu="isShowMenu"
|
|
||||||
/>
|
|
||||||
</ProsemirrorAdapterProvider>
|
|
||||||
</MilkdownProvider>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
|
@ -1,216 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, onMounted, watch } from 'vue'
|
|
||||||
|
|
||||||
import {
|
|
||||||
Editor,
|
|
||||||
rootCtx,
|
|
||||||
rootAttrsCtx,
|
|
||||||
defaultValueCtx,
|
|
||||||
editorViewOptionsCtx,
|
|
||||||
} from '@milkdown/core'
|
|
||||||
import { Milkdown, useEditor } from '@milkdown/vue'
|
|
||||||
import { commonmark } from '@milkdown/preset-commonmark'
|
|
||||||
import { gfm } from '@milkdown/preset-gfm'
|
|
||||||
|
|
||||||
import { prism, prismConfig } from '@milkdown/plugin-prism'
|
|
||||||
import { replaceAll } from '@milkdown/utils'
|
|
||||||
|
|
||||||
import '@/styles/editor/index.scss'
|
|
||||||
|
|
||||||
// Syntax highlight
|
|
||||||
import c from 'refractor/lang/c'
|
|
||||||
import cpp from 'refractor/lang/cpp'
|
|
||||||
import csharp from 'refractor/lang/csharp'
|
|
||||||
import css from 'refractor/lang/css'
|
|
||||||
import go from 'refractor/lang/go'
|
|
||||||
import haskell from 'refractor/lang/haskell'
|
|
||||||
import python from 'refractor/lang/python'
|
|
||||||
import java from 'refractor/lang/java'
|
|
||||||
import javascript from 'refractor/lang/javascript'
|
|
||||||
import typescript from 'refractor/lang/typescript'
|
|
||||||
import jsx from 'refractor/lang/jsx'
|
|
||||||
import kotlin from 'refractor/lang/kotlin'
|
|
||||||
import r from 'refractor/lang/r'
|
|
||||||
import rust from 'refractor/lang/rust'
|
|
||||||
import scala from 'refractor/lang/scala'
|
|
||||||
import sql from 'refractor/lang/sql'
|
|
||||||
import tsx from 'refractor/lang/tsx'
|
|
||||||
import markdown from 'refractor/lang/markdown'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
isReadonly: boolean
|
|
||||||
valueMarkdown: string
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const valueMarkdown = computed(() => props.valueMarkdown)
|
|
||||||
|
|
||||||
const editable = () => !props.isReadonly
|
|
||||||
|
|
||||||
const editor = useEditor((root) =>
|
|
||||||
Editor.make()
|
|
||||||
.config((ctx) => {
|
|
||||||
ctx.set(rootCtx, root)
|
|
||||||
ctx.set(rootAttrsCtx, {
|
|
||||||
roles: 'kun-galgame-milkdown-editor',
|
|
||||||
'aria-label': 'kun-galgame-milkdown-editor',
|
|
||||||
})
|
|
||||||
ctx.set(defaultValueCtx, valueMarkdown.value)
|
|
||||||
|
|
||||||
ctx.update(editorViewOptionsCtx, (prev) => ({
|
|
||||||
...prev,
|
|
||||||
editable,
|
|
||||||
}))
|
|
||||||
|
|
||||||
ctx.set(prismConfig.key, {
|
|
||||||
configureRefractor: (refractor) => {
|
|
||||||
refractor.register(c)
|
|
||||||
refractor.register(cpp)
|
|
||||||
refractor.register(csharp)
|
|
||||||
refractor.register(css)
|
|
||||||
refractor.register(go)
|
|
||||||
refractor.register(haskell)
|
|
||||||
refractor.register(python)
|
|
||||||
refractor.register(markdown)
|
|
||||||
refractor.register(java)
|
|
||||||
refractor.register(javascript)
|
|
||||||
refractor.register(typescript)
|
|
||||||
refractor.register(jsx)
|
|
||||||
refractor.register(kotlin)
|
|
||||||
refractor.register(r)
|
|
||||||
refractor.register(rust)
|
|
||||||
refractor.register(scala)
|
|
||||||
refractor.register(sql)
|
|
||||||
refractor.register(tsx)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.use(commonmark)
|
|
||||||
.use(gfm)
|
|
||||||
.use(prism)
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => valueMarkdown.value,
|
|
||||||
() => {
|
|
||||||
editor.get()?.action(replaceAll(valueMarkdown.value))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- MilkdownEditor.vue -->
|
|
||||||
<template>
|
|
||||||
<Milkdown class="editor" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.editor {
|
|
||||||
:deep(.milkdown) {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
|
|
||||||
/* Silence css check */
|
|
||||||
* {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > div:nth-child(1) {
|
|
||||||
transition: all 0.2s;
|
|
||||||
margin: 0 auto;
|
|
||||||
overflow-y: scroll;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
display: inline;
|
|
||||||
width: 7px;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
cursor: default;
|
|
||||||
background: var(--kungalgame-blue-4);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Compatible with Firefox */
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: var(--kungalgame-blue-4) var(--kungalgame-blue-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
del {
|
|
||||||
text-decoration: line-through;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 17px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote {
|
|
||||||
margin: 17px 0;
|
|
||||||
padding: 10px;
|
|
||||||
font-size: 18px;
|
|
||||||
border-left: 4px solid var(--kungalgame-blue-4);
|
|
||||||
background-color: var(--kungalgame-trans-blue-0);
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
margin: 17px 0;
|
|
||||||
border: 1px solid var(--kungalgame-blue-4);
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 17px;
|
|
||||||
background-color: var(--kungalgame-trans-white-5);
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
code {
|
|
||||||
font-size: 15px;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
cursor: pointer;
|
|
||||||
font-style: oblique;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--kungalgame-blue-4);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
border: 1px solid var(--kungalgame-blue-4);
|
|
||||||
border-radius: 5px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
th,
|
|
||||||
td {
|
|
||||||
border: 1px solid var(--kungalgame-blue-4);
|
|
||||||
padding: 3px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr:nth-child(even) {
|
|
||||||
background-color: var(--kungalgame-trans-blue-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
ul li,
|
|
||||||
ol li {
|
|
||||||
color: var(--kungalgame-blue-5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tableWrapper {
|
|
||||||
color: var(--kungalgame-font-color-3);
|
|
||||||
position: relative;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,151 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { Icon } from '@iconify/vue'
|
|
||||||
import { UseEditorReturn } from '@milkdown/vue'
|
|
||||||
import type { CmdKey } from '@milkdown/core'
|
|
||||||
import { callCommand } from '@milkdown/utils'
|
|
||||||
import {
|
|
||||||
createCodeBlockCommand,
|
|
||||||
updateCodeBlockLanguageCommand,
|
|
||||||
toggleEmphasisCommand,
|
|
||||||
toggleStrongCommand,
|
|
||||||
wrapInBlockquoteCommand,
|
|
||||||
wrapInBulletListCommand,
|
|
||||||
wrapInOrderedListCommand,
|
|
||||||
insertHrCommand,
|
|
||||||
toggleInlineCodeCommand,
|
|
||||||
toggleLinkCommand,
|
|
||||||
} from '@milkdown/preset-commonmark'
|
|
||||||
import {
|
|
||||||
insertTableCommand,
|
|
||||||
toggleStrikethroughCommand,
|
|
||||||
} from '@milkdown/preset-gfm'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
editorInfo: UseEditorReturn
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const { get, loading } = props.editorInfo
|
|
||||||
|
|
||||||
const call = <T>(command: CmdKey<T>, payload?: T) => {
|
|
||||||
return get()?.action(callCommand(command, payload))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select a language TODO:
|
|
||||||
const selectLanguage = () => {}
|
|
||||||
|
|
||||||
// Create code block
|
|
||||||
const handleClickCodeBlock = () => {
|
|
||||||
call(createCodeBlockCommand.key, 'javascript')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="menu">
|
|
||||||
<!-- Mark Group -->
|
|
||||||
<button
|
|
||||||
aria-label="kun-galgame-bold"
|
|
||||||
@click="call(toggleStrongCommand.key)"
|
|
||||||
>
|
|
||||||
<Icon icon="material-symbols:format-bold-rounded" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
aria-label="kun-galgame-italic"
|
|
||||||
@click="call(toggleEmphasisCommand.key)"
|
|
||||||
>
|
|
||||||
<Icon icon="material-symbols:format-italic-rounded" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
aria-label="kun-galgame-italic"
|
|
||||||
@click="call(toggleStrikethroughCommand.key)"
|
|
||||||
>
|
|
||||||
<Icon icon="material-symbols:strikethrough-s-rounded" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
aria-label="kun-galgame-table"
|
|
||||||
@click="call(insertTableCommand.key)"
|
|
||||||
>
|
|
||||||
<Icon icon="material-symbols:table" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
aria-label="kun-galgame-list-bulleted"
|
|
||||||
@click="call(wrapInBulletListCommand.key)"
|
|
||||||
>
|
|
||||||
<Icon icon="material-symbols:format-list-bulleted-rounded" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
aria-label="kun-galgame-list-numbered"
|
|
||||||
@click="call(wrapInOrderedListCommand.key)"
|
|
||||||
>
|
|
||||||
<Icon icon="material-symbols:format-list-numbered-rounded" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
aria-label="kun-galgame-quote"
|
|
||||||
@click="call(wrapInBlockquoteCommand.key)"
|
|
||||||
>
|
|
||||||
<Icon icon="material-symbols:format-quote-rounded" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
aria-label="kun-galgame-horizontal"
|
|
||||||
@click="call(insertHrCommand.key)"
|
|
||||||
>
|
|
||||||
<Icon icon="material-symbols:horizontal-rule-rounded" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
aria-label="kun-galgame-italic"
|
|
||||||
@click="call(toggleLinkCommand.key)"
|
|
||||||
>
|
|
||||||
<Icon icon="material-symbols:link-rounded" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button aria-label="kun-galgame-italic" @click="handleClickCodeBlock">
|
|
||||||
<Icon icon="material-symbols:code-blocks-outline-rounded" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
aria-label="kun-galgame-italic"
|
|
||||||
@click="call(toggleInlineCodeCommand.key)"
|
|
||||||
>
|
|
||||||
<Icon icon="material-symbols:code-rounded" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.menu {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
width: 100%;
|
|
||||||
background-color: var(--kungalgame-trans-blue-1);
|
|
||||||
border-bottom: 1px solid var(--kungalgame-blue-1);
|
|
||||||
border-top: 1px solid var(--kungalgame-blue-1);
|
|
||||||
|
|
||||||
button {
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 5px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin: 5px;
|
|
||||||
font-size: 22px;
|
|
||||||
color: var(--kungalgame-font-color-3);
|
|
||||||
background-color: var(--kungalgame-trans-white-9);
|
|
||||||
border: 1px solid var(--kungalgame-trans-white-9);
|
|
||||||
transition: all 0.2s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border: 1px solid var(--kungalgame-blue-4);
|
|
||||||
color: var(--kungalgame-blue-4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,80 +0,0 @@
|
||||||
<!-- Custom plugins, calculate text size -->
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, onMounted, watch } from 'vue'
|
|
||||||
import { useRoute } from 'vue-router'
|
|
||||||
import Settings from '../components/Settings.vue'
|
|
||||||
import { usePluginViewContext } from '@prosemirror-adapter/vue'
|
|
||||||
|
|
||||||
import { useTempEditStore } from '@/store/temp/edit'
|
|
||||||
import { useKUNGalgameEditStore } from '@/store/modules/edit'
|
|
||||||
import { useTempReplyStore } from '@/store/temp/topic/reply'
|
|
||||||
import { usePersistKUNGalgameReplyStore } from '@/store/modules/topic/reply'
|
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
|
|
||||||
const { textCount: textCountEditRewrite, isTopicRewriting } = storeToRefs(
|
|
||||||
useTempEditStore()
|
|
||||||
)
|
|
||||||
const { textCount: textCountEdit } = storeToRefs(useKUNGalgameEditStore())
|
|
||||||
const { textCount: textCountReplyRewrite, isReplyRewriting } = storeToRefs(
|
|
||||||
useTempReplyStore()
|
|
||||||
)
|
|
||||||
const { textCount: textCountReply } = storeToRefs(
|
|
||||||
usePersistKUNGalgameReplyStore()
|
|
||||||
)
|
|
||||||
|
|
||||||
const { view } = usePluginViewContext()
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const routeName = computed(() => route.name as string)
|
|
||||||
|
|
||||||
const size = computed(() => {
|
|
||||||
return view.value.state.doc.textContent.length
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => size.value,
|
|
||||||
() => {
|
|
||||||
if (routeName.value === 'Edit' && isTopicRewriting.value) {
|
|
||||||
textCountEditRewrite.value = size.value
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (routeName.value === 'Topic' && isReplyRewriting.value) {
|
|
||||||
textCountReplyRewrite.value = size.value
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (routeName.value === 'Edit') {
|
|
||||||
textCountEdit.value = size.value
|
|
||||||
}
|
|
||||||
if (routeName.value === 'Topic') {
|
|
||||||
textCountReply.value = size.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (routeName.value === 'Edit' && isTopicRewriting.value) {
|
|
||||||
textCountEditRewrite.value = size.value
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (routeName.value === 'Topic' && isReplyRewriting.value) {
|
|
||||||
textCountReplyRewrite.value = size.value
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="footer">
|
|
||||||
<Settings />
|
|
||||||
<span> {{ size + ` ${$tm('edit.word')}` }} </span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.footer {
|
|
||||||
padding: 10px 17px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,93 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { Icon } from '@iconify/vue'
|
|
||||||
import type { CmdKey } from '@milkdown/core'
|
|
||||||
import { TooltipProvider } from '@milkdown/plugin-tooltip'
|
|
||||||
import {
|
|
||||||
toggleStrongCommand,
|
|
||||||
toggleEmphasisCommand,
|
|
||||||
toggleInlineCodeCommand,
|
|
||||||
} from '@milkdown/preset-commonmark'
|
|
||||||
import { toggleStrikethroughCommand } from '@milkdown/preset-gfm'
|
|
||||||
import { callCommand } from '@milkdown/utils'
|
|
||||||
import { useInstance } from '@milkdown/vue'
|
|
||||||
import { usePluginViewContext } from '@prosemirror-adapter/vue'
|
|
||||||
import { onMounted, onUnmounted, ref, VNodeRef, watch } from 'vue'
|
|
||||||
|
|
||||||
const { view, prevState } = usePluginViewContext()
|
|
||||||
const [loading, get] = useInstance()
|
|
||||||
|
|
||||||
const divRef = ref<VNodeRef>()
|
|
||||||
|
|
||||||
let tooltipProvider: TooltipProvider
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
tooltipProvider = new TooltipProvider({
|
|
||||||
content: divRef.value as any,
|
|
||||||
})
|
|
||||||
|
|
||||||
tooltipProvider.update(view.value, prevState.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
watch([view, prevState], () => {
|
|
||||||
tooltipProvider?.update(view.value, prevState.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
tooltipProvider.destroy()
|
|
||||||
})
|
|
||||||
|
|
||||||
const call = <T>(command: CmdKey<T>, payload?: T) => {
|
|
||||||
return get()?.action(callCommand(command, payload))
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div v-if="loading" class="tooltip" ref="divRef">
|
|
||||||
<button @click="call(toggleStrongCommand.key)">
|
|
||||||
<Icon icon="material-symbols:format-bold-rounded" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button @click="call(toggleEmphasisCommand.key)">
|
|
||||||
<Icon icon="material-symbols:format-italic-rounded" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button @click="call(toggleStrikethroughCommand.key)">
|
|
||||||
<Icon icon="material-symbols:strikethrough-s-rounded" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button @click="call(toggleInlineCodeCommand.key)">
|
|
||||||
<Icon icon="material-symbols:code-rounded" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.tooltip {
|
|
||||||
display: flex;
|
|
||||||
background-color: var(--kungalgame-trans-white-2);
|
|
||||||
border: 1px solid var(--kungalgame-blue-4);
|
|
||||||
border-radius: 5px;
|
|
||||||
backdrop-filter: blur(5px);
|
|
||||||
|
|
||||||
button {
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 5px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin: 5px;
|
|
||||||
font-size: 22px;
|
|
||||||
color: var(--kungalgame-font-color-3);
|
|
||||||
background-color: var(--kungalgame-trans-white-9);
|
|
||||||
border: 1px solid var(--kungalgame-trans-white-9);
|
|
||||||
transition: all 0.2s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border: 1px solid var(--kungalgame-blue-4);
|
|
||||||
color: var(--kungalgame-blue-4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,9 +1,11 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Icon } from '@iconify/vue'
|
|
||||||
import { computed, defineAsyncComponent, ref } from 'vue'
|
import { computed, defineAsyncComponent, ref } from 'vue'
|
||||||
// Import the router
|
// Import the router
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
// Import icon font
|
||||||
|
import { Icon } from '@iconify/vue'
|
||||||
|
|
||||||
// Asynchronously import the editor settings menu
|
// Asynchronously import the editor settings menu
|
||||||
const EditorSettingsMenu = defineAsyncComponent(
|
const EditorSettingsMenu = defineAsyncComponent(
|
||||||
() => import('./EditorSettingsMenu.vue')
|
() => import('./EditorSettingsMenu.vue')
|
||||||
|
@ -11,17 +13,25 @@ const EditorSettingsMenu = defineAsyncComponent(
|
||||||
// Import CSS animations
|
// Import CSS animations
|
||||||
import 'animate.css'
|
import 'animate.css'
|
||||||
|
|
||||||
import { usePersistKUNGalgameReplyStore } from '@/store/modules/topic/reply'
|
// Import the topic editing store
|
||||||
|
import { useKUNGalgameEditStore } from '@/store/modules/edit'
|
||||||
|
import { useKUNGalgameTopicStore } from '@/store/modules/topic'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
|
|
||||||
|
// Topic editing page store
|
||||||
|
const { textCount } = storeToRefs(useKUNGalgameEditStore())
|
||||||
// Topic page store for replies and adjusting reply panel width
|
// Topic page store for replies and adjusting reply panel width
|
||||||
const { replyPanelWidth } = storeToRefs(usePersistKUNGalgameReplyStore())
|
const { replyDraft, replyPanelWidth } = storeToRefs(useKUNGalgameTopicStore())
|
||||||
|
|
||||||
// Current route
|
// Current route
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
// Name of the current page route
|
// Name of the current page route
|
||||||
const routeName = computed(() => route.name as string)
|
const routeName = computed(() => route.name as string)
|
||||||
|
|
||||||
|
const textCountNumber = computed(() =>
|
||||||
|
routeName.value === 'Edit' ? textCount.value : replyDraft.value.textCount
|
||||||
|
)
|
||||||
|
|
||||||
// Whether to display the editor settings panel
|
// Whether to display the editor settings panel
|
||||||
const isShowSettingsMenu = ref(false)
|
const isShowSettingsMenu = ref(false)
|
||||||
// Style when the settings panel is activated
|
// Style when the settings panel is activated
|
||||||
|
@ -45,7 +55,7 @@ const handelCloseSettingsMenu = () => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="footer">
|
||||||
<!-- Display the settings button -->
|
<!-- Display the settings button -->
|
||||||
<div class="settings">
|
<div class="settings">
|
||||||
<span
|
<span
|
||||||
|
@ -69,6 +79,9 @@ const handelCloseSettingsMenu = () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Word count -->
|
||||||
|
<span class="count">{{ textCountNumber + ` ${$tm('edit.word')}` }}</span>
|
||||||
|
|
||||||
<!-- Settings panel -->
|
<!-- Settings panel -->
|
||||||
<EditorSettingsMenu
|
<EditorSettingsMenu
|
||||||
@close="handelCloseSettingsMenu"
|
@close="handelCloseSettingsMenu"
|
||||||
|
@ -78,7 +91,9 @@ const handelCloseSettingsMenu = () => {
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.container {
|
.footer {
|
||||||
|
padding: 10px 17px;
|
||||||
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
@ -118,10 +133,6 @@ const handelCloseSettingsMenu = () => {
|
||||||
margin-bottom: 100px;
|
margin-bottom: 100px;
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
text-decoration: none;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: var(--kungalgame-white);
|
background-color: var(--kungalgame-white);
|
||||||
|
@ -155,6 +166,11 @@ const handelCloseSettingsMenu = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
color: var(--kungalgame-font-color-0);
|
||||||
|
background-color: var(--kungalgame-trans-white-9);
|
||||||
|
}
|
||||||
|
|
||||||
// Keep the settings button rotating when activated.
|
// Keep the settings button rotating when activated.
|
||||||
.settings-icon-active {
|
.settings-icon-active {
|
||||||
color: var(--kungalgame-blue-4);
|
color: var(--kungalgame-blue-4);
|
|
@ -1,36 +1,53 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Icon } from '@iconify/vue'
|
import { ref, watch, computed } from 'vue'
|
||||||
import { computed } from 'vue'
|
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
// Import the icon font
|
||||||
|
import { Icon } from '@iconify/vue'
|
||||||
|
// Import CSS animations
|
||||||
import 'animate.css'
|
import 'animate.css'
|
||||||
|
|
||||||
|
// Import the topic editing store
|
||||||
import { useKUNGalgameEditStore } from '@/store/modules/edit'
|
import { useKUNGalgameEditStore } from '@/store/modules/edit'
|
||||||
import { usePersistKUNGalgameReplyStore } from '@/store/modules/topic/reply'
|
// Import the reply store
|
||||||
|
import { useKUNGalgameTopicStore } from '@/store/modules/topic'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
|
|
||||||
|
// Import the keyword display toggle button
|
||||||
import SwitchButton from './SwitchButton.vue'
|
import SwitchButton from './SwitchButton.vue'
|
||||||
|
|
||||||
const { editorHeight: editEditorHeight } = storeToRefs(useKUNGalgameEditStore())
|
// Topic editing page store
|
||||||
const { editorHeight: replyEditorHeight } = storeToRefs(
|
const { editorHeight, mode } = storeToRefs(useKUNGalgameEditStore())
|
||||||
usePersistKUNGalgameReplyStore()
|
// Topic page store for replies
|
||||||
)
|
const { replyDraft } = storeToRefs(useKUNGalgameTopicStore())
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
isShowSettingsMenu: boolean
|
isShowSettingsMenu: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
// Define emits to close the settings panel
|
||||||
const emits = defineEmits<{
|
const emits = defineEmits<{
|
||||||
close: [isShowSettingsMenu: boolean]
|
close: [isShowSettingsMenu: boolean]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
// Current route
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
// Name of the current page route
|
||||||
const routeName = computed(() => route.name as string)
|
const routeName = computed(() => route.name as string)
|
||||||
const editorHeight = computed(() => {
|
|
||||||
return routeName.value === 'Edit'
|
|
||||||
? `${editEditorHeight.value}px`
|
|
||||||
: `${replyEditorHeight.value}px`
|
|
||||||
})
|
|
||||||
|
|
||||||
|
// Whether to refresh the page when clicking advanced options
|
||||||
|
const isRefreshPage = ref(false)
|
||||||
|
|
||||||
|
// Remind the user to refresh the page when clicking on advanced options
|
||||||
|
watch(
|
||||||
|
() => [replyDraft.value.mode, mode.value],
|
||||||
|
() => {
|
||||||
|
isRefreshPage.value = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleRefreshPage = () => location.reload()
|
||||||
|
|
||||||
|
// Close the settings panel
|
||||||
const handelCloseSettingsPanel = () => {
|
const handelCloseSettingsPanel = () => {
|
||||||
emits('close', false)
|
emits('close', false)
|
||||||
}
|
}
|
||||||
|
@ -47,18 +64,18 @@ const handelCloseSettingsPanel = () => {
|
||||||
<!-- Editor height settings -->
|
<!-- Editor height settings -->
|
||||||
<div class="editor-height-title">
|
<div class="editor-height-title">
|
||||||
<span> {{ $tm('edit.editorHeight') }} </span>
|
<span> {{ $tm('edit.editorHeight') }} </span>
|
||||||
<span>{{ editorHeight }} </span>
|
<span>{{ editorHeight }} px</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Editor page -->
|
<!-- Editor page -->
|
||||||
<div v-if="routeName === 'Edit'" class="editor-height">
|
<div v-if="routeName === 'Edit'" class="editor-height">
|
||||||
<span>200 px</span>
|
<span>300 px</span>
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min="200"
|
min="300"
|
||||||
max="500"
|
max="500"
|
||||||
step="1"
|
step="1"
|
||||||
v-model="editEditorHeight"
|
v-model="editorHeight"
|
||||||
/>
|
/>
|
||||||
<span>500 px</span>
|
<span>500 px</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -71,11 +88,46 @@ const handelCloseSettingsPanel = () => {
|
||||||
min="100"
|
min="100"
|
||||||
max="500"
|
max="500"
|
||||||
step="1"
|
step="1"
|
||||||
v-model="replyEditorHeight"
|
v-model="replyDraft.editorHeight"
|
||||||
/>
|
/>
|
||||||
<span>500 px</span>
|
<span>500 px</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Whether to display editor advanced options -->
|
||||||
|
<div class="editor-advance">
|
||||||
|
<div class="editor-advance-title">
|
||||||
|
<Transition mode="out-in" name="slide-up">
|
||||||
|
<span v-if="!isRefreshPage"> {{ $tm('edit.editorMode') }} </span>
|
||||||
|
<span
|
||||||
|
@click="handleRefreshPage"
|
||||||
|
class="refresh"
|
||||||
|
v-else-if="isRefreshPage"
|
||||||
|
>
|
||||||
|
{{ $tm('edit.refresh') }}
|
||||||
|
</span>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Editor page switch button -->
|
||||||
|
<select class="select" v-if="routeName === 'Edit'" v-model="mode">
|
||||||
|
<option value="minimal">{{ $tm('edit.minimal') }}</option>
|
||||||
|
<option value="">{{ $tm('edit.default') }}</option>
|
||||||
|
<option value="essential">{{ $tm('edit.essential') }}</option>
|
||||||
|
<option value="full">{{ $tm('edit.full') }}</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- Reply panel switch button -->
|
||||||
|
<select
|
||||||
|
class="select"
|
||||||
|
v-if="routeName === 'Topic'"
|
||||||
|
v-model="replyDraft.mode"
|
||||||
|
>
|
||||||
|
<option value="minimal">{{ $tm('edit.minimal') }}</option>
|
||||||
|
<option value="">{{ $tm('edit.default') }}</option>
|
||||||
|
<option value="essential">{{ $tm('edit.essential') }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Whether to display popular keywords -->
|
<!-- Whether to display popular keywords -->
|
||||||
<div class="keywords">
|
<div class="keywords">
|
||||||
<div class="keywords-title">{{ $tm('edit.tagsHint') }}</div>
|
<div class="keywords-title">{{ $tm('edit.tagsHint') }}</div>
|
||||||
|
@ -134,6 +186,35 @@ const handelCloseSettingsPanel = () => {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-advance-title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
.refresh {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 17px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--kungalgame-blue-4);
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Editor mode selection box
|
||||||
|
.select {
|
||||||
|
width: 100px;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-left: 20px;
|
||||||
|
color: var(--kungalgame-font-color-3);
|
||||||
|
border: 1px solid var(--kungalgame-blue-4);
|
||||||
|
background-color: var(--kungalgame-trans-white-9);
|
||||||
|
|
||||||
|
option {
|
||||||
|
background-color: var(--kungalgame-white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Close settings
|
// Close settings
|
||||||
.close {
|
.close {
|
||||||
font-size: 25px;
|
font-size: 25px;
|
137
src/components/quill-editor/Help.vue
Normal file
137
src/components/quill-editor/Help.vue
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
// Global message component (top)
|
||||||
|
import Message from '@/components/alert/Message'
|
||||||
|
|
||||||
|
// Import the router
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
// Import the icon font
|
||||||
|
import { Icon } from '@iconify/vue'
|
||||||
|
|
||||||
|
// Import CSS animations
|
||||||
|
import 'animate.css'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
// Current route
|
||||||
|
const route = useRoute()
|
||||||
|
// Name of the current page route
|
||||||
|
const routeName = computed(() => route.name as string)
|
||||||
|
|
||||||
|
// Adjust the background color based on the mouse coordinates
|
||||||
|
const x = ref(0)
|
||||||
|
// Whether to display information prompts
|
||||||
|
const isShowInfo = ref(false)
|
||||||
|
|
||||||
|
// When the mouse moves
|
||||||
|
const onMousemove = (e: MouseEvent) => {
|
||||||
|
x.value = e.clientX
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click the help button
|
||||||
|
const handleClickHelp = () => {
|
||||||
|
if (routeName.value === 'Edit') {
|
||||||
|
isShowInfo.value = true
|
||||||
|
} else {
|
||||||
|
const helpHtmlEN = `<p>You can click on the left settings to adjust the editor's mode.</p>
|
||||||
|
<p>We recommend finishing your text before formatting.</p>
|
||||||
|
<p>The website's code is handwritten, and errors are inevitable.</p>
|
||||||
|
<p>If you encounter any errors, please <a style="color: var(--kungalgame-blue-4); border-bottom: 2px solid var(--kungalgame-blue-4);" href="/contact">Contact Us</a>.</p>`
|
||||||
|
const helpHtmlCN = `<p>您可以点击左侧的设置调整编辑器的模式</p>
|
||||||
|
<p>我们建议您写完文本再进行格式化</p>
|
||||||
|
<p>网站的代码是手写的,错误在所难免</p>
|
||||||
|
<p>如果您遇到错误,请<a style="color: var(--kungalgame-blue-4); border-bottom: 2px solid var(--kungalgame-blue-4);" href="/contact">联系我们</a></p>`
|
||||||
|
|
||||||
|
Message(helpHtmlEN, helpHtmlCN, 'info', 5000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="help">
|
||||||
|
<div class="title" @click="handleClickHelp">
|
||||||
|
<span><Icon icon="line-md:question-circle" /></span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="isShowInfo"
|
||||||
|
@mousemove="onMousemove"
|
||||||
|
@mouseleave="isShowInfo = false"
|
||||||
|
class="info"
|
||||||
|
:style="{ backgroundColor: `hsl(${x}, 77%, 77%)` }"
|
||||||
|
>
|
||||||
|
<ul>
|
||||||
|
<li>{{ $tm('edit.help1') }}</li>
|
||||||
|
<li>{{ $tm('edit.help2') }}</li>
|
||||||
|
<li>{{ $tm('edit.help3') }}</li>
|
||||||
|
<li>{{ $tm('edit.help4') }}</li>
|
||||||
|
<li>
|
||||||
|
{{ $tm('edit.help5') }}
|
||||||
|
<span @click="router.push('/contact')">
|
||||||
|
{{ $tm('edit.contact') }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.help {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 20px;
|
||||||
|
color: var(--kungalgame-font-color-1);
|
||||||
|
font-size: 23px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
padding: 3px;
|
||||||
|
color: var(--kungalgame-font-color-2);
|
||||||
|
position: absolute;
|
||||||
|
left: 200px;
|
||||||
|
transition: 0.3s background-color ease;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 100px;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: var(--kungalgame-white);
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
li {
|
||||||
|
&::before {
|
||||||
|
content: '❆ ';
|
||||||
|
color: var(--kungalgame-pink-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor: default;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 27px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--kungalgame-blue-4);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
303
src/components/quill-editor/QuillEditor.vue
Normal file
303
src/components/quill-editor/QuillEditor.vue
Normal file
|
@ -0,0 +1,303 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineAsyncComponent, computed, ref, onBeforeMount } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
// Import the editor
|
||||||
|
import { QuillEditor } from '@vueup/vue-quill'
|
||||||
|
// Import editor Modules
|
||||||
|
import { modules } from './modules'
|
||||||
|
|
||||||
|
// Custom Quill themes, the second theme is not currently in use
|
||||||
|
import '@/styles/editor/editor.snow.scss'
|
||||||
|
// import '@vueup/vue-quill/dist/vue-quill.bubble.css'
|
||||||
|
|
||||||
|
// Import Title component
|
||||||
|
const Title = defineAsyncComponent(
|
||||||
|
() => import('@/components/quill-editor/Title.vue')
|
||||||
|
)
|
||||||
|
|
||||||
|
// Import EditorFooter
|
||||||
|
import EditorFooter from './EditorFooter.vue'
|
||||||
|
// Footer slot
|
||||||
|
import Help from './Help.vue'
|
||||||
|
|
||||||
|
// Import the store for editing topics
|
||||||
|
import { useKUNGalgameEditStore } from '@/store/modules/edit'
|
||||||
|
import { useKUNGalgameTopicStore } from '@/store/modules/topic'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
|
||||||
|
// Import XSS filtering tool
|
||||||
|
import DOMPurify from 'dompurify'
|
||||||
|
// Import debounce function
|
||||||
|
import { debounce } from '@/utils/debounce'
|
||||||
|
|
||||||
|
// Topic editing page store
|
||||||
|
const {
|
||||||
|
editorHeight,
|
||||||
|
mode,
|
||||||
|
theme,
|
||||||
|
textCount,
|
||||||
|
isSaveTopic,
|
||||||
|
content,
|
||||||
|
topicRewrite,
|
||||||
|
} = storeToRefs(useKUNGalgameEditStore())
|
||||||
|
// Store for topic page used for replies
|
||||||
|
const { replyDraft, replyRewrite } = storeToRefs(useKUNGalgameTopicStore())
|
||||||
|
|
||||||
|
// Current route
|
||||||
|
const route = useRoute()
|
||||||
|
// Current page route name
|
||||||
|
const routeName = computed(() => route.name as string)
|
||||||
|
|
||||||
|
// Define props passed from the parent component
|
||||||
|
/**
|
||||||
|
* @param {boolean} isShowToolbar - Whether to display the toolbar
|
||||||
|
* @param {boolean} isShowTitle - Whether to display the title
|
||||||
|
*/
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
isShowToolbar: boolean
|
||||||
|
isShowTitle: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// Editor instance
|
||||||
|
const editorRef = ref<typeof QuillEditor>()
|
||||||
|
// Content inside the editor
|
||||||
|
const valueHtml = ref('')
|
||||||
|
// Editor-related configuration
|
||||||
|
const editorOptions = {
|
||||||
|
placeholder: 'Moe Moe Moe!',
|
||||||
|
}
|
||||||
|
|
||||||
|
// Editor height, determined by the route name
|
||||||
|
const editorHeightStyle = computed(
|
||||||
|
() =>
|
||||||
|
`height: ${
|
||||||
|
routeName.value === 'Edit'
|
||||||
|
? editorHeight.value
|
||||||
|
: replyDraft.value.editorHeight
|
||||||
|
}px`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Editor mode, determined by the route name
|
||||||
|
const editorMode = computed(() =>
|
||||||
|
routeName.value === 'Edit' ? mode.value : replyDraft.value.mode
|
||||||
|
)
|
||||||
|
|
||||||
|
// Whether to show the editor toolbar
|
||||||
|
const isShowEditorToolbar = computed(() =>
|
||||||
|
props.isShowToolbar ? 'block' : 'none'
|
||||||
|
)
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
/**
|
||||||
|
* Editor is in the edit mode
|
||||||
|
*/
|
||||||
|
// Load topic data before mounting if not saved (and must be on the Edit page)
|
||||||
|
if (isSaveTopic.value && routeName.value === 'Edit') {
|
||||||
|
valueHtml.value = content.value
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Editor is in the re-editing edit mode
|
||||||
|
*/
|
||||||
|
// Load data for re-editing a topic before mounting
|
||||||
|
if (topicRewrite.value.isTopicRewriting && routeName.value === 'Edit') {
|
||||||
|
valueHtml.value = topicRewrite.value.content
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Editor is in the reply mode
|
||||||
|
*/
|
||||||
|
// Load reply data before mounting if not saved (and must be on the Topic page)
|
||||||
|
if (replyDraft.value.isSaveReply && routeName.value === 'Topic') {
|
||||||
|
valueHtml.value = replyDraft.value.content
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Editor is in the re-editing reply mode
|
||||||
|
*/
|
||||||
|
if (replyRewrite.value.isReplyRewriting && routeName.value === 'Topic') {
|
||||||
|
valueHtml.value = replyRewrite.value.content
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Automatically save data when the editor text changes
|
||||||
|
const handleTextChange = async () => {
|
||||||
|
// Filter out XSS
|
||||||
|
const purifiedHtml = DOMPurify.sanitize(editorRef.value?.getHTML())
|
||||||
|
// Create a debounce function
|
||||||
|
const debouncedUpdateContent = debounce(() => {
|
||||||
|
/**
|
||||||
|
* Editor is in edit mode
|
||||||
|
*/
|
||||||
|
// Save to the edit store if not in topic re-edit mode
|
||||||
|
if (!topicRewrite.value.isTopicRewriting && routeName.value === 'Edit') {
|
||||||
|
content.value = purifiedHtml
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Editor is in re-editing edit mode
|
||||||
|
*/
|
||||||
|
// Load data for re-editing a topic before mounting
|
||||||
|
if (topicRewrite.value.isTopicRewriting && routeName.value === 'Edit') {
|
||||||
|
topicRewrite.value.content = purifiedHtml
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Editor is in reply mode
|
||||||
|
*/
|
||||||
|
// Save to the reply store if not in reply re-edit mode
|
||||||
|
if (!replyRewrite.value.isReplyRewriting && routeName.value === 'Topic') {
|
||||||
|
replyDraft.value.content = purifiedHtml
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Editor is in re-editing reply mode
|
||||||
|
*/
|
||||||
|
if (replyRewrite.value.isReplyRewriting && routeName.value === 'Topic') {
|
||||||
|
replyRewrite.value.content = purifiedHtml
|
||||||
|
}
|
||||||
|
}, 1007)
|
||||||
|
|
||||||
|
// Call the debounce function, which will execute the update operation only once within the delay time
|
||||||
|
debouncedUpdateContent()
|
||||||
|
|
||||||
|
// Calculate how many characters the user has entered
|
||||||
|
const length = computed(() => editorRef.value?.getText().trim().length)
|
||||||
|
|
||||||
|
// Save the count based on the page's route name
|
||||||
|
if (routeName.value === 'Edit') {
|
||||||
|
textCount.value = length.value
|
||||||
|
}
|
||||||
|
if (routeName.value === 'Topic') {
|
||||||
|
replyDraft.value.textCount = length.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="editor">
|
||||||
|
<!-- Topic title -->
|
||||||
|
<Title v-if="isShowTitle" />
|
||||||
|
|
||||||
|
<!-- Editor body -->
|
||||||
|
<QuillEditor
|
||||||
|
ref="editorRef"
|
||||||
|
contentType="html"
|
||||||
|
:content="valueHtml"
|
||||||
|
:style="editorHeightStyle"
|
||||||
|
:theme="theme"
|
||||||
|
:modules="modules"
|
||||||
|
:toolbar="editorMode"
|
||||||
|
:options="editorOptions"
|
||||||
|
@textChange="handleTextChange"
|
||||||
|
@click.prevent
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Editor footer -->
|
||||||
|
<EditorFooter>
|
||||||
|
<template #help>
|
||||||
|
<Help />
|
||||||
|
</template>
|
||||||
|
</EditorFooter>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
/*
|
||||||
|
* Resolve style issues
|
||||||
|
* These styles are written based on the compiled CSS, it's a bit weird, blame the author www
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Style of the toolbar */
|
||||||
|
:deep(.ql-toolbar) {
|
||||||
|
border-top: 1px solid var(--kungalgame-blue-1);
|
||||||
|
border-bottom: 1px solid var(--kungalgame-blue-1);
|
||||||
|
background-color: var(--kungalgame-trans-blue-0);
|
||||||
|
/* Shadow below the header */
|
||||||
|
box-shadow: 0 2px 4px 0 var(--kungalgame-trans-blue-1);
|
||||||
|
display: v-bind(isShowEditorToolbar);
|
||||||
|
/* Do not display video insertion, this feature has too many bugs */
|
||||||
|
|
||||||
|
.ql-video {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style of the editor body */
|
||||||
|
:deep(.ql-container) {
|
||||||
|
transition: all 0.2s;
|
||||||
|
width: 80%;
|
||||||
|
max-width: 1080px;
|
||||||
|
border: none;
|
||||||
|
margin: 0 auto;
|
||||||
|
font-size: 17px;
|
||||||
|
margin-top: 40px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '∟';
|
||||||
|
position: absolute;
|
||||||
|
font-size: 40px;
|
||||||
|
transform: translateX(-20px) translateY(-20px) rotate(90deg);
|
||||||
|
color: var(--kungalgame-blue-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '∟';
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
font-size: 40px;
|
||||||
|
transform: translateX(20px) translateY(-20px) rotate(-90deg);
|
||||||
|
color: var(--kungalgame-blue-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ql-editor {
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: inline;
|
||||||
|
width: 7px;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
cursor: default;
|
||||||
|
background: var(--kungalgame-blue-4);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compatible with Firefox */
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--kungalgame-blue-4) var(--kungalgame-blue-1); /* Firefox 64+ */
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '♡ Yuki Yuki';
|
||||||
|
font-size: 22px;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
transform: translateX(-20px) translateY(27px);
|
||||||
|
color: var(--kungalgame-trans-white-5);
|
||||||
|
text-shadow: 1px 1px 1px var(--kungalgame-pink-3);
|
||||||
|
font-style: oblique;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style of BlotFormatter plugin, important here */
|
||||||
|
.blot-formatter__toolbar-button {
|
||||||
|
margin: 0 5px;
|
||||||
|
border: none !important;
|
||||||
|
background: var(--kungalgame-trans-white-9) !important;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
border: 1px solid var(--kungalgame-blue-4) !important;
|
||||||
|
background: var(--kungalgame-trans-white-2) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-selected {
|
||||||
|
svg {
|
||||||
|
background: var(--kungalgame-trans-blue-1) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -3,9 +3,10 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { watch, computed } from 'vue'
|
import { watch, computed } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
// Import the store for the editing page
|
||||||
import { useKUNGalgameEditStore } from '@/store/modules/edit'
|
import { useKUNGalgameEditStore } from '@/store/modules/edit'
|
||||||
import { usePersistKUNGalgameReplyStore } from '@/store/modules/topic/reply'
|
// Import the store for replies
|
||||||
|
import { useKUNGalgameTopicStore } from '@/store/modules/topic'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
|
|
||||||
// Current page's route
|
// Current page's route
|
||||||
|
@ -14,20 +15,16 @@ const route = useRoute()
|
||||||
const routeName = computed(() => route.name as string)
|
const routeName = computed(() => route.name as string)
|
||||||
|
|
||||||
// Use the store for the editing page
|
// Use the store for the editing page
|
||||||
const { isShowHotKeywords: isShowEditHotKeywords } = storeToRefs(
|
const { isShowHotKeywords } = storeToRefs(useKUNGalgameEditStore())
|
||||||
useKUNGalgameEditStore()
|
|
||||||
)
|
|
||||||
// Store for the topic page, used for replies
|
// Store for the topic page, used for replies
|
||||||
const { isShowHotKeywords: isShowReplyHotKeywords, replyDraft } = storeToRefs(
|
const { replyDraft } = storeToRefs(useKUNGalgameTopicStore())
|
||||||
usePersistKUNGalgameReplyStore()
|
|
||||||
)
|
|
||||||
|
|
||||||
// Watch for changes in store states to keep button states in sync with the store
|
// Watch for changes in store states to keep button states in sync with the store
|
||||||
watch(
|
watch(
|
||||||
() => [isShowEditHotKeywords.value, isShowReplyHotKeywords.value],
|
() => [isShowHotKeywords.value, replyDraft.value.isShowHotKeywords],
|
||||||
([newValue1, newValue2]) => {
|
([newValue1, newValue2]) => {
|
||||||
isShowEditHotKeywords.value = newValue1
|
isShowHotKeywords.value = newValue1
|
||||||
isShowReplyHotKeywords.value = newValue2
|
replyDraft.value.isShowHotKeywords = newValue2
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
@ -38,14 +35,13 @@ watch(
|
||||||
v-if="routeName === 'Edit'"
|
v-if="routeName === 'Edit'"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="switch"
|
id="switch"
|
||||||
v-model="isShowEditHotKeywords"
|
v-model="isShowHotKeywords"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
v-if="routeName === 'Topic'"
|
v-if="routeName === 'Topic'"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="switch"
|
id="switch"
|
||||||
v-model="isShowReplyHotKeywords"
|
v-model="replyDraft.isShowHotKeywords"
|
||||||
/>
|
/>
|
||||||
<label for="switch"></label>
|
<label for="switch"></label>
|
||||||
</template>
|
</template>
|
|
@ -1,16 +1,16 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onBeforeMount, ref } from 'vue'
|
import { onBeforeMount, ref } from 'vue'
|
||||||
|
|
||||||
import { useTempEditStore } from '@/store/temp/edit'
|
// Import the store for editing topics
|
||||||
import { useKUNGalgameEditStore } from '@/store/modules/edit'
|
import { useKUNGalgameEditStore } from '@/store/modules/edit'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
|
|
||||||
|
// Import debounce function
|
||||||
import { debounce } from '@/utils/debounce'
|
import { debounce } from '@/utils/debounce'
|
||||||
|
|
||||||
const { title: rewriteTitle, isTopicRewriting } = storeToRefs(
|
const { isSaveTopic, title, topicRewrite } = storeToRefs(
|
||||||
useTempEditStore()
|
useKUNGalgameEditStore()
|
||||||
)
|
)
|
||||||
const { isSaveTopic, title: editTitle } = storeToRefs(useKUNGalgameEditStore())
|
|
||||||
|
|
||||||
// Topic title text
|
// Topic title text
|
||||||
const topicTitle = ref('')
|
const topicTitle = ref('')
|
||||||
|
@ -22,14 +22,14 @@ onBeforeMount(() => {
|
||||||
* Editor is in edit mode
|
* Editor is in edit mode
|
||||||
*/
|
*/
|
||||||
if (isSaveTopic.value) {
|
if (isSaveTopic.value) {
|
||||||
topicTitle.value = editTitle.value
|
topicTitle.value = title.value
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Editor is in re-editing edit mode
|
* Editor is in re-editing edit mode
|
||||||
*/
|
*/
|
||||||
// Load data for re-editing a topic before mounting
|
// Load data for re-editing a topic before mounting
|
||||||
if (isTopicRewriting.value) {
|
if (topicRewrite.value.isTopicRewriting) {
|
||||||
topicTitle.value = rewriteTitle.value
|
topicTitle.value = topicRewrite.value.title
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -40,28 +40,33 @@ const handleInput = () => {
|
||||||
topicTitle.value = topicTitle.value.slice(0, maxInputLength)
|
topicTitle.value = topicTitle.value.slice(0, maxInputLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// User input is pure whitespace
|
||||||
if (topicTitle.value.trim() === '') {
|
if (topicTitle.value.trim() === '') {
|
||||||
rewriteTitle.value = ''
|
title.value = ''
|
||||||
editTitle.value = ''
|
topicRewrite.value.title = ''
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return debounce(() => {
|
// Create a debounce handling function
|
||||||
|
const debouncedInput = debounce(() => {
|
||||||
/**
|
/**
|
||||||
* Editor is in reply mode
|
* Editor is in reply mode
|
||||||
*/
|
*/
|
||||||
// Save to the edit store if not in topic re-edit mode
|
// Save to the edit store if not in topic re-edit mode
|
||||||
if (!isTopicRewriting.value) {
|
if (!topicRewrite.value.isTopicRewriting) {
|
||||||
editTitle.value = topicTitle.value
|
title.value = topicTitle.value
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Editor is in re-editing edit mode
|
* Editor is in re-editing edit mode
|
||||||
*/
|
*/
|
||||||
// Save to the re-editing page's store if in re-edit mode
|
// Save to the re-editing page's store if in re-edit mode
|
||||||
if (isTopicRewriting.value) {
|
if (topicRewrite.value.isTopicRewriting) {
|
||||||
rewriteTitle.value = topicTitle.value
|
topicRewrite.value.title = topicTitle.value
|
||||||
}
|
}
|
||||||
}, 300)
|
}, 300)
|
||||||
|
|
||||||
|
// Call the debounce handling function, which will execute the update operation only once within the delay time
|
||||||
|
debouncedInput()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
88
src/components/quill-editor/modules.ts
Normal file
88
src/components/quill-editor/modules.ts
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
/**
|
||||||
|
* This file contains various modules for Quill. Large modules like markdown and emoji are not used here.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Import the editor
|
||||||
|
// import { QuillEditor } from '@vueup/vue-quill'
|
||||||
|
// Import Quill module for resizing and realigning images and iframe video
|
||||||
|
// It must be imported this way, otherwise it will throw errors after bundling
|
||||||
|
// import BlotFormatter from 'quill-blot-formatter'
|
||||||
|
// import BlotFormatter from 'quill-blot-formatter/dist/BlotFormatter'
|
||||||
|
// Import module for automatic recognition of URLs and email addresses
|
||||||
|
import MagicUrl from 'quill-magic-url'
|
||||||
|
import '@/styles/editor/editor.snow.scss'
|
||||||
|
// Import module for image compression and uploading (very useful)
|
||||||
|
import ImageCompress from 'quill-image-compress'
|
||||||
|
import Message from '../alert/Message'
|
||||||
|
|
||||||
|
// Editor modules
|
||||||
|
export const modules = [
|
||||||
|
// BlotFormatter
|
||||||
|
// {
|
||||||
|
// name: 'blotFormatter',
|
||||||
|
// module: BlotFormatter,
|
||||||
|
// // see: https://github.com/Fandom-OSS/quill-blot-formatter/blob/master/src/Options.js
|
||||||
|
// options: {
|
||||||
|
// overlay: {
|
||||||
|
// style: {
|
||||||
|
// border: '2px solid var(--kungalgame-blue-3)',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// MagicUrl
|
||||||
|
{
|
||||||
|
name: 'magicUrl',
|
||||||
|
module: MagicUrl,
|
||||||
|
options: {
|
||||||
|
// Regex used to check URLs during typing
|
||||||
|
urlRegularExpression:
|
||||||
|
/(?:https?:\/\/)?(?:www\.)?[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}(?:\/[^\s]*)?/,
|
||||||
|
// Regex used to check URLs on paste
|
||||||
|
globalRegularExpression: /(https?:\/\/|www\.|tel:)[\S]+/g,
|
||||||
|
mailRegularExpression: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
|
||||||
|
globalMailRegularExpression:
|
||||||
|
/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// ImageCompress
|
||||||
|
{
|
||||||
|
name: 'imageCompress',
|
||||||
|
module: ImageCompress,
|
||||||
|
options: {
|
||||||
|
quality: 0.77,
|
||||||
|
maxWidth: 1007,
|
||||||
|
maxHeight: 1007,
|
||||||
|
imageType: 'image/webp',
|
||||||
|
insertIntoEditor: () => {
|
||||||
|
Message(
|
||||||
|
'The image upload API is under development',
|
||||||
|
'图片上传接口正在开发中',
|
||||||
|
'warn'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
// insertIntoEditor: (
|
||||||
|
// imageBase64URL: string,
|
||||||
|
// imageBlob: Blob,
|
||||||
|
// editor: typeof QuillEditor
|
||||||
|
// ) => {
|
||||||
|
// const formData = new FormData()
|
||||||
|
// formData.append('file', imageBlob)
|
||||||
|
|
||||||
|
// /* TODO: Change this to a backend API */
|
||||||
|
|
||||||
|
// fetch('127.0.0.1:10008/upload', { method: 'POST', body: formData })
|
||||||
|
// .then((response) => response.text())
|
||||||
|
// .then((result) => {
|
||||||
|
// const range = editor.getSelection()
|
||||||
|
// editor.insertEmbed(range.index, 'image', `${result}`, 'user')
|
||||||
|
// })
|
||||||
|
// .catch((error) => {
|
||||||
|
// console.error(error)
|
||||||
|
// })
|
||||||
|
// },
|
||||||
|
// Temporarily enable console debugging
|
||||||
|
debug: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
|
@ -1,164 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { onBeforeUnmount, ref, watch } from 'vue'
|
|
||||||
import SearchBox from './SearchBox.vue'
|
|
||||||
import SearchHistory from './SearchHistory.vue'
|
|
||||||
import SearchResult from './SearchResult.vue'
|
|
||||||
|
|
||||||
import { useTempHomeStore } from '@/store/temp/home'
|
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
|
|
||||||
import { HomeSearchTopic } from '@/api'
|
|
||||||
|
|
||||||
const { search, isShowSearch } = storeToRefs(useTempHomeStore())
|
|
||||||
|
|
||||||
const topics = ref<HomeSearchTopic[]>([])
|
|
||||||
const container = ref<HTMLElement>()
|
|
||||||
|
|
||||||
const searchTopics = async () => {
|
|
||||||
return (await useTempHomeStore().searchTopic()).data
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => search.value.keywords,
|
|
||||||
async () => {
|
|
||||||
if (search.value.keywords) {
|
|
||||||
topics.value = await searchTopics()
|
|
||||||
} else {
|
|
||||||
topics.value = []
|
|
||||||
useTempHomeStore().resetSearchStatus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => container.value,
|
|
||||||
() => {
|
|
||||||
const element = container.value
|
|
||||||
if (element) {
|
|
||||||
element.addEventListener('scroll', scrollHandler)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const scrollHandler = async () => {
|
|
||||||
if (isScrollAtBottom() && search.value.isLoading && search.value.keywords) {
|
|
||||||
search.value.page++
|
|
||||||
|
|
||||||
const lazyLoadTopics = await searchTopics()
|
|
||||||
|
|
||||||
if (!lazyLoadTopics.length) {
|
|
||||||
search.value.isLoading = false
|
|
||||||
}
|
|
||||||
|
|
||||||
topics.value = [...topics.value, ...lazyLoadTopics]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isScrollAtBottom = () => {
|
|
||||||
if (container.value) {
|
|
||||||
const scrollHeight = container.value.scrollHeight
|
|
||||||
const scrollTop = container.value.scrollTop
|
|
||||||
const clientHeight = container.value.clientHeight
|
|
||||||
|
|
||||||
const errorMargin = 1.007
|
|
||||||
return Math.abs(scrollHeight - scrollTop - clientHeight) < errorMargin
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
const element = container.value
|
|
||||||
if (element) {
|
|
||||||
element.removeEventListener('scroll', scrollHandler)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Teleport to="body" :disabled="isShowSearch">
|
|
||||||
<Transition name="search">
|
|
||||||
<div class="mask" v-if="isShowSearch" @click="isShowSearch = false">
|
|
||||||
<div ref="container" class="container" @click.stop>
|
|
||||||
<SearchBox />
|
|
||||||
|
|
||||||
<SearchHistory v-if="!search.keywords" />
|
|
||||||
|
|
||||||
<SearchResult :topics="topics" v-if="topics.length" />
|
|
||||||
|
|
||||||
<span class="empty" v-if="!topics.length && search.keywords">
|
|
||||||
{{ $tm('mainPage.header.emptyResult') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</Teleport>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.mask {
|
|
||||||
position: fixed;
|
|
||||||
z-index: 9999;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: var(--kungalgame-mask-color-0);
|
|
||||||
display: flex;
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--kungalgame-font-color-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
position: relative;
|
|
||||||
color: var(--kungalgame-font-color-3);
|
|
||||||
background-color: var(--kungalgame-trans-white-2);
|
|
||||||
box-shadow: var(--kungalgame-shadow-0);
|
|
||||||
border-radius: 17px;
|
|
||||||
padding: 10px;
|
|
||||||
width: 40vw;
|
|
||||||
max-width: 500px;
|
|
||||||
min-height: 200px;
|
|
||||||
max-height: 600px;
|
|
||||||
overflow-y: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
color: var(--kungalgame-blue-2);
|
|
||||||
font-style: oblique;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-enter-from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-enter-from .container,
|
|
||||||
.search-leave-to .container {
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
-webkit-transform: scale(1.1);
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1000px) {
|
|
||||||
.container {
|
|
||||||
width: 60vw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
|
||||||
.container {
|
|
||||||
width: 90vw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,90 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, onBeforeMount, onMounted, watch } from 'vue'
|
|
||||||
|
|
||||||
import { debounce } from '@/utils/debounce'
|
|
||||||
|
|
||||||
import { useTempHomeStore } from '@/store/temp/home'
|
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
|
|
||||||
const { search } = storeToRefs(useTempHomeStore())
|
|
||||||
|
|
||||||
const input = ref<HTMLElement | null>(null)
|
|
||||||
const inputValue = ref('')
|
|
||||||
|
|
||||||
onBeforeMount(() => {
|
|
||||||
search.value.keywords = ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const debouncedSearch = debounce((inputValue: string) => {
|
|
||||||
if (inputValue.trim()) {
|
|
||||||
search.value.keywords = inputValue
|
|
||||||
} else {
|
|
||||||
search.value.keywords = ''
|
|
||||||
}
|
|
||||||
}, 300)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => search.value.keywords,
|
|
||||||
() => {
|
|
||||||
if (!inputValue.value) {
|
|
||||||
inputValue.value = search.value.keywords
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (input) {
|
|
||||||
input.value?.focus()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="search-form">
|
|
||||||
<input
|
|
||||||
ref="input"
|
|
||||||
v-model="inputValue"
|
|
||||||
type="search"
|
|
||||||
class="input"
|
|
||||||
:placeholder="`${$tm('mainPage.header.search')}`"
|
|
||||||
@input="debouncedSearch(inputValue)"
|
|
||||||
@keydown.enter="debouncedSearch(inputValue)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.search-form {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 777px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: 17px;
|
|
||||||
background-color: var(--kungalgame-trans-white-2);
|
|
||||||
backdrop-filter: blur(5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.input {
|
|
||||||
padding: 0 15px;
|
|
||||||
height: 40px;
|
|
||||||
width: 100%;
|
|
||||||
font-size: 16px;
|
|
||||||
border: none;
|
|
||||||
background-color: var(--kungalgame-trans-white-9);
|
|
||||||
border: 2px solid var(--kungalgame-blue-4);
|
|
||||||
border-radius: 17px;
|
|
||||||
color: var(--kungalgame-font-color-3);
|
|
||||||
transition: all 0.2s;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
border: 2px solid var(--kungalgame-pink-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: var(--kungalgame-font-color-1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,136 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { Icon } from '@iconify/vue'
|
|
||||||
|
|
||||||
import { usePersistKUNGalgameHomeStore } from '@/store/modules/home'
|
|
||||||
import { useTempHomeStore } from '@/store/temp/home'
|
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
|
|
||||||
const { searchHistory } = storeToRefs(usePersistKUNGalgameHomeStore())
|
|
||||||
const { search } = storeToRefs(useTempHomeStore())
|
|
||||||
|
|
||||||
const handleClickHistory = (index: number) => {
|
|
||||||
search.value.keywords = searchHistory.value[index]
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearSearchHistory = () => {
|
|
||||||
searchHistory.value = []
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDeleteHistory = (historyIndex: number) => {
|
|
||||||
searchHistory.value.splice(historyIndex, 1)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="history">
|
|
||||||
<div class="title">
|
|
||||||
<span>{{ $tm('mainPage.header.history') }}</span>
|
|
||||||
<span @click="clearSearchHistory">
|
|
||||||
{{ $tm('mainPage.header.clear') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="history-container" v-if="searchHistory.length">
|
|
||||||
<div
|
|
||||||
class="single-history"
|
|
||||||
v-for="(history, index) in searchHistory"
|
|
||||||
:key="index"
|
|
||||||
@click="handleClickHistory(index)"
|
|
||||||
>
|
|
||||||
<span>{{ history }} </span>
|
|
||||||
<span>
|
|
||||||
<Icon
|
|
||||||
@click="handleDeleteHistory(index)"
|
|
||||||
class="delete"
|
|
||||||
icon="line-md:close-circle"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span class="empty" v-if="!searchHistory.length">
|
|
||||||
{{ $tm('mainPage.header.emptyHistory') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.history {
|
|
||||||
width: 100%;
|
|
||||||
top: 70px;
|
|
||||||
left: 0;
|
|
||||||
flex-direction: column;
|
|
||||||
color: var(--kungalgame-font-color-3);
|
|
||||||
border-radius: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
display: flex;
|
|
||||||
margin: 10px;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-size: 14px;
|
|
||||||
&:nth-child(2) {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--kungalgame-blue-4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
font-size: 13px;
|
|
||||||
margin: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.single-history {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space between;
|
|
||||||
padding: 7px 3px;
|
|
||||||
margin: 2px 0;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--kungalgame-blue-4);
|
|
||||||
|
|
||||||
.delete {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
span:nth-child(1) {
|
|
||||||
cursor: default;
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
span:nth-child(2) {
|
|
||||||
width: 17px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete {
|
|
||||||
width: 30px;
|
|
||||||
right: 5px;
|
|
||||||
font-size: 17px;
|
|
||||||
position: absolute;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--kungalgame-font-color-0);
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
color: var(--kungalgame-blue-2);
|
|
||||||
font-style: oblique;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,94 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { HomeSearchTopic } from '@/api'
|
|
||||||
|
|
||||||
import { usePersistKUNGalgameHomeStore } from '@/store/modules/home'
|
|
||||||
|
|
||||||
const { searchHistory } = storeToRefs(usePersistKUNGalgameHomeStore())
|
|
||||||
import { useTempHomeStore } from '@/store/temp/home'
|
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const { search, isShowSearch } = storeToRefs(useTempHomeStore())
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
topics: HomeSearchTopic[]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const topics = computed(() => props.topics)
|
|
||||||
|
|
||||||
const handleClickTopic = (tid: number) => {
|
|
||||||
router.push(`/topic/${tid}`)
|
|
||||||
if (!searchHistory.value.includes(search.value.keywords)) {
|
|
||||||
searchHistory.value.push(search.value.keywords)
|
|
||||||
}
|
|
||||||
isShowSearch.value = false
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="result">
|
|
||||||
<div
|
|
||||||
v-for="(topic, index) in topics"
|
|
||||||
:key="index"
|
|
||||||
:to="`/topic/${topic.tid}`"
|
|
||||||
class="topic"
|
|
||||||
@click="handleClickTopic(topic.tid)"
|
|
||||||
>
|
|
||||||
<span class="title">{{ topic.title }}</span>
|
|
||||||
<span class="content">{{ topic.content }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.result {
|
|
||||||
width: 100%;
|
|
||||||
top: 70px;
|
|
||||||
left: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.topic {
|
|
||||||
color: var(--kungalgame-font-color-3);
|
|
||||||
border: 1px solid var(--kungalgame-blue-4);
|
|
||||||
background-color: var(--kungalgame-trans-blue-0);
|
|
||||||
padding: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
border-radius: 17px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transition: all 0.2s;
|
|
||||||
background-color: var(--kungalgame-white);
|
|
||||||
box-shadow: var(--kungalgame-shadow-1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
&:nth-child(1) {
|
|
||||||
white-space: wrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
color: var(--kungalgame-blue-5);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(2) {
|
|
||||||
white-space: wrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
overflow: hidden;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,33 +1,45 @@
|
||||||
|
<!-- Settings panel component, displaying the entire forum's settings panel -->
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
// Import icon font
|
||||||
import { Icon } from '@iconify/vue'
|
import { Icon } from '@iconify/vue'
|
||||||
|
// Import mascot component
|
||||||
import Loli from './components/Loli.vue'
|
import Loli from './components/Loli.vue'
|
||||||
|
// Import mode switch component
|
||||||
import Mode from './components/Mode.vue'
|
import Mode from './components/Mode.vue'
|
||||||
|
// Import language switch component
|
||||||
import SwitchLanguage from './components/SwitchLanguage.vue'
|
import SwitchLanguage from './components/SwitchLanguage.vue'
|
||||||
|
// Page width adjustment component
|
||||||
import PageWidth from './components/PageWidth.vue'
|
import PageWidth from './components/PageWidth.vue'
|
||||||
|
// Font settings component
|
||||||
import Font from './components/Font.vue'
|
import Font from './components/Font.vue'
|
||||||
|
// Import background settings component
|
||||||
import Background from './components/Background.vue'
|
import Background from './components/Background.vue'
|
||||||
|
// Import settings store
|
||||||
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
|
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
|
|
||||||
|
// Use the settings store
|
||||||
const settingsStore = useKUNGalgameSettingsStore()
|
const settingsStore = useKUNGalgameSettingsStore()
|
||||||
const { isShowPageWidth } = storeToRefs(settingsStore)
|
const { isShowPageWidth } = storeToRefs(settingsStore)
|
||||||
|
|
||||||
|
// Define emits to close the settings panel
|
||||||
const emits = defineEmits<{
|
const emits = defineEmits<{
|
||||||
close: [showKUNGalgamePanel: boolean]
|
close: [showKUNGalgamePanel: boolean]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
// Restore all settings to default
|
||||||
const handleRecover = () => {
|
const handleRecover = () => {
|
||||||
settingsStore.setKUNGalgameSettingsRecover()
|
settingsStore.setKUNGalgameSettingsRecover()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close the settings panel
|
||||||
const handelCloseSettingsPanel = () => {
|
const handelCloseSettingsPanel = () => {
|
||||||
emits('close', false)
|
emits('close', false)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<!-- Root element -->
|
||||||
<div class="root">
|
<div class="root">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
|
@ -35,8 +47,10 @@ const handelCloseSettingsPanel = () => {
|
||||||
<span><Icon class="settings-icon" icon="uiw:setting-o" /></span>
|
<span><Icon class="settings-icon" icon="uiw:setting-o" /></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Mode switch component -->
|
||||||
<Mode />
|
<Mode />
|
||||||
|
|
||||||
|
<!-- Language switch component -->
|
||||||
<SwitchLanguage />
|
<SwitchLanguage />
|
||||||
|
|
||||||
<div class="switch">
|
<div class="switch">
|
||||||
|
@ -57,15 +71,18 @@ const handelCloseSettingsPanel = () => {
|
||||||
|
|
||||||
<TransitionGroup name="item" tag="div">
|
<TransitionGroup name="item" tag="div">
|
||||||
<div class="item" v-if="isShowPageWidth">
|
<div class="item" v-if="isShowPageWidth">
|
||||||
|
<!-- Page width adjustment component -->
|
||||||
<PageWidth />
|
<PageWidth />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="item" v-else-if="!isShowPageWidth">
|
<div class="item" v-else-if="!isShowPageWidth">
|
||||||
|
<!-- Set the page font -->
|
||||||
<Font />
|
<Font />
|
||||||
</div>
|
</div>
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Background settings component -->
|
||||||
<Background />
|
<Background />
|
||||||
|
|
||||||
<button class="reset" @click="handleRecover">
|
<button class="reset" @click="handleRecover">
|
||||||
|
@ -73,15 +90,19 @@ const handelCloseSettingsPanel = () => {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Mascot component -->
|
||||||
<Loli class="loli" />
|
<Loli class="loli" />
|
||||||
|
|
||||||
|
<!-- Close panel -->
|
||||||
<div class="close">
|
<div class="close">
|
||||||
|
<!-- showKUNGalgamePanel exists in the settings, false to close the settings panel -->
|
||||||
<Icon @click="handelCloseSettingsPanel" icon="line-md:close" />
|
<Icon @click="handelCloseSettingsPanel" icon="line-md:close" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
/* Root container */
|
||||||
.root {
|
.root {
|
||||||
top: 65px;
|
top: 65px;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
@ -113,6 +134,7 @@ const handelCloseSettingsPanel = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep the settings button rotating
|
||||||
.settings-icon {
|
.settings-icon {
|
||||||
animation: settings 3s linear infinite;
|
animation: settings 3s linear infinite;
|
||||||
}
|
}
|
||||||
|
@ -126,6 +148,7 @@ const handelCloseSettingsPanel = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Menu for switching settings options */
|
||||||
.switch {
|
.switch {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -167,9 +190,9 @@ const handelCloseSettingsPanel = () => {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
color: var(--kungalgame-red-4);
|
color: var(--kungalgame-font-color-3);
|
||||||
border: 1px solid var(--kungalgame-red-4);
|
border: 1px solid var(--kungalgame-red-4);
|
||||||
background-color: var(--kungalgame-trans-white-9);
|
background-color: var(--kungalgame-trans-red-1);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
@ -189,7 +212,7 @@ const handelCloseSettingsPanel = () => {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-move,
|
.item-move, /* Transition applied to moving elements */
|
||||||
.item-enter-active,
|
.item-enter-active,
|
||||||
.item-leave-active {
|
.item-leave-active {
|
||||||
transition: all 0.5s ease;
|
transition: all 0.5s ease;
|
||||||
|
@ -201,6 +224,8 @@ const handelCloseSettingsPanel = () => {
|
||||||
transform: translateY(77px);
|
transform: translateY(77px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ensure the leaving element is removed from the layout flow
|
||||||
|
to correctly calculate the animated movement. */
|
||||||
.item-leave-active {
|
.item-leave-active {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,46 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
// Import Vue functions
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import CustomBackground from './CustomBackground.vue'
|
|
||||||
import BackgroundImageSkeleton from '@/components/skeleton/settings-panel/BackgroundImageSkeleton.vue'
|
|
||||||
|
|
||||||
|
// Import the settings panel store
|
||||||
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
|
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
|
|
||||||
|
// Global message component (top)
|
||||||
|
import Message from '@/components/alert/Message'
|
||||||
|
|
||||||
import { backgroundImages } from './background'
|
import { backgroundImages } from './background'
|
||||||
import { getBackgroundURL } from '@/hooks/useBackgroundPicture'
|
import { getBackgroundURL } from '@/hooks/useBackgroundPicture'
|
||||||
import { restoreBackground } from '@/hooks/useBackgroundPicture'
|
import { restoreBackground } from '@/hooks/useBackgroundPicture'
|
||||||
|
|
||||||
const imageArray = ref<string[]>([])
|
const imageArray = ref<string[]>([])
|
||||||
const { showKUNGalgameBackground } = storeToRefs(useKUNGalgameSettingsStore())
|
// Use the settings panel store
|
||||||
|
const { showKUNGalgameBackground, showKUNGalgameCustomBackground } =
|
||||||
|
storeToRefs(useKUNGalgameSettingsStore())
|
||||||
|
|
||||||
|
// Get background image thumbnails
|
||||||
const getBackground = async (imageNumber: number) => {
|
const getBackground = async (imageNumber: number) => {
|
||||||
return await getBackgroundURL(`bg${imageNumber}-m`)
|
return await getBackgroundURL(`bg${imageNumber}-m`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Change the background image
|
||||||
const handleChangeImage = (index: number) => {
|
const handleChangeImage = (index: number) => {
|
||||||
showKUNGalgameBackground.value = `bg${index}`
|
showKUNGalgameBackground.value = `bg${index}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Custom background
|
||||||
|
const url = ref('')
|
||||||
|
|
||||||
|
const handleCustomBackground = () => {
|
||||||
|
if (url.value) {
|
||||||
|
showKUNGalgameCustomBackground.value = url.value
|
||||||
|
showKUNGalgameBackground.value = 'bg1007'
|
||||||
|
url.value = ''
|
||||||
|
} else {
|
||||||
|
Message('Please input a valid image URL', '请输入合法的图片链接', 'warn')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
for (const background of backgroundImages) {
|
for (const background of backgroundImages) {
|
||||||
const backgroundURL = await getBackground(background.index)
|
const backgroundURL = await getBackground(background.index)
|
||||||
|
@ -31,32 +51,45 @@ onMounted(async () => {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="kungalgame-background">
|
<div class="kungalgame-background">
|
||||||
<div class="bg-settings">
|
<div class="bg-settings">{{ $tm('header.settings.background') }}</div>
|
||||||
{{ $tm('header.settings.background') }}
|
|
||||||
</div>
|
|
||||||
<ul class="kungalgame-background-container">
|
<ul class="kungalgame-background-container">
|
||||||
<li>
|
<li>
|
||||||
<span>{{ $tm('header.settings.preset') }}</span>
|
<span>{{ $tm('header.settings.preset') }}</span>
|
||||||
|
<!-- Preset background collection -->
|
||||||
<ul class="kungalgame-restore-bg">
|
<ul class="kungalgame-restore-bg">
|
||||||
<li
|
<li v-for="kun in backgroundImages" :key="kun.index">
|
||||||
v-for="kun in backgroundImages"
|
|
||||||
:key="kun.index"
|
|
||||||
v-tooltip="{ message: kun.message, position: 'bottom' }"
|
|
||||||
>
|
|
||||||
<img
|
<img
|
||||||
v-if="kun"
|
|
||||||
:src="imageArray[kun.index - 1]"
|
:src="imageArray[kun.index - 1]"
|
||||||
|
:alt="kun.alt"
|
||||||
@click="handleChangeImage(kun.index)"
|
@click="handleChangeImage(kun.index)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BackgroundImageSkeleton v-if="!imageArray[kun.index - 1]" />
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<!-- User-customized background -->
|
||||||
<li>
|
<li>
|
||||||
<CustomBackground />
|
<!-- Title -->
|
||||||
|
<span>{{ $tm('header.settings.custom') }}</span>
|
||||||
|
|
||||||
|
<!-- Input field -->
|
||||||
|
<div class="kungalgamer-bg">
|
||||||
|
<div class="bg-url-input">
|
||||||
|
<input
|
||||||
|
:placeholder="`${$tm('header.settings.url')}`"
|
||||||
|
type="text"
|
||||||
|
v-model="url"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Confirm background URL -->
|
||||||
|
<button @click="handleCustomBackground">
|
||||||
|
{{ $tm('header.settings.confirm') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Reset blank background -->
|
||||||
<button class="restore-bg" @click="restoreBackground">
|
<button class="restore-bg" @click="restoreBackground">
|
||||||
{{ $tm('header.settings.restore') }}
|
{{ $tm('header.settings.restore') }}
|
||||||
</button>
|
</button>
|
||||||
|
@ -66,60 +99,102 @@ onMounted(async () => {
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
/* Background settings */
|
||||||
.kungalgame-background-container {
|
.kungalgame-background-container {
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
text-decoration: none;
|
|
||||||
display: block;
|
display: block;
|
||||||
|
/* Height of the background menu */
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
color: var(--kungalgame-font-color-3);
|
color: var(--kungalgame-font-color-3);
|
||||||
|
|
||||||
|
/* Font for the title of the background container */
|
||||||
span {
|
span {
|
||||||
height: 30px;
|
height: 30px;
|
||||||
|
/* Centered */
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-settings {
|
.bg-settings {
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
|
/* Grid of background image thumbnails, three rows and three columns */
|
||||||
.kungalgame-restore-bg {
|
.kungalgame-restore-bg {
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
text-decoration: none;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
grid-template-columns: repeat(3, 80px);
|
grid-template-columns: repeat(3, 80px);
|
||||||
grid-template-rows: repeat(3, 50px);
|
grid-template-rows: repeat(3, 50px);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
/* Distance from the lower area */
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
/* Center individual images */
|
||||||
li {
|
li {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
/* Spacing for individual images */
|
||||||
img {
|
img {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 70px;
|
width: 70px;
|
||||||
position: relative;
|
|
||||||
|
|
||||||
|
/* Image hover effect */
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: scale(3);
|
transform: scale(3);
|
||||||
transition: 0.2s;
|
transition: 0.2s;
|
||||||
z-index: 7;
|
z-index: 7;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.kungalgamer-bg {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
/* URL input box */
|
||||||
|
.bg-url-input {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--kungalgame-font-color-3);
|
||||||
|
|
||||||
.image-detail {
|
input {
|
||||||
position: absolute;
|
width: 100%;
|
||||||
|
padding-left: 5px;
|
||||||
|
height: 25px;
|
||||||
|
border: 1px solid var(--kungalgame-blue-4);
|
||||||
|
background-color: var(--kungalgame-trans-white-9);
|
||||||
|
color: var(--kungalgame-font-color-3);
|
||||||
|
|
||||||
|
/* Focus on the input box */
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
background-color: var(--kungalgame-trans-blue-0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Confirm button */
|
||||||
|
button {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0 10px;
|
||||||
|
height: 25px;
|
||||||
|
width: 70px;
|
||||||
|
color: var(--kungalgame-font-color-3);
|
||||||
|
border: 1px solid var(--kungalgame-blue-4);
|
||||||
|
border-left: none;
|
||||||
|
background-color: var(--kungalgame-trans-white-5);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
/* Confirm button hover effect */
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--kungalgame-trans-red-1);
|
||||||
|
|
||||||
|
/* Confirm button active effect */
|
||||||
|
&:active {
|
||||||
|
background-color: var(--kungalgame-trans-red-3);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -132,13 +207,11 @@ onMounted(async () => {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
color: var(--kungalgame-font-color-3);
|
color: var(--kungalgame-font-color-3);
|
||||||
border: 1px solid var(--kungalgame-blue-4);
|
border: 1px solid var(--kungalgame-blue-4);
|
||||||
background-color: var(--kungalgame-trans-white-9);
|
background-color: var(--kungalgame-trans-blue-1);
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
color: var(--kungalgame-blue-4);
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--kungalgame-white);
|
background-color: var(--kungalgame-trans-blue-2);
|
||||||
background-color: var(--kungalgame-blue-4);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,88 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import Message from '@/components/alert/Message'
|
|
||||||
|
|
||||||
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
|
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
|
|
||||||
import { saveImage, getImage } from '@/hooks/useLocalforage'
|
|
||||||
|
|
||||||
const { showKUNGalgameBackground, showKUNGalgameCustomBackground } =
|
|
||||||
storeToRefs(useKUNGalgameSettingsStore())
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
isMobile?: boolean
|
|
||||||
}>()
|
|
||||||
const input = ref<HTMLElement>()
|
|
||||||
|
|
||||||
const handleCustomBackground = () => {
|
|
||||||
input.value?.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFileChange = async (event: Event) => {
|
|
||||||
const input = event.target as HTMLInputElement
|
|
||||||
|
|
||||||
if (!input.files || !input.files[0]) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = input.files[0]
|
|
||||||
await saveImage(file, 'kun-galgame-custom-bg')
|
|
||||||
const backgroundImageBlobData = await getImage('kun-galgame-custom-bg')
|
|
||||||
|
|
||||||
if (backgroundImageBlobData) {
|
|
||||||
showKUNGalgameBackground.value = 'bg1007'
|
|
||||||
showKUNGalgameCustomBackground.value = URL.createObjectURL(
|
|
||||||
backgroundImageBlobData
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Message('Upload image failed!', '上传图片错误!', 'error')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="kungalgamer-bg">
|
|
||||||
<input
|
|
||||||
ref="input"
|
|
||||||
hidden
|
|
||||||
type="file"
|
|
||||||
accept=".jpg, .jpeg, .png"
|
|
||||||
@change="handleFileChange($event)"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
:class="props.isMobile ? 'mobile' : ''"
|
|
||||||
@click="handleCustomBackground"
|
|
||||||
>
|
|
||||||
{{ $tm('header.settings.custom') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.kungalgamer-bg {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
button {
|
|
||||||
font-size: 15px;
|
|
||||||
cursor: pointer;
|
|
||||||
height: 30px;
|
|
||||||
width: 100%;
|
|
||||||
color: var(--kungalgame-font-color-3);
|
|
||||||
border: 1px solid var(--kungalgame-blue-4);
|
|
||||||
background-color: var(--kungalgame-trans-white-9);
|
|
||||||
transition: all 0.2s;
|
|
||||||
color: var(--kungalgame-blue-4);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--kungalgame-white);
|
|
||||||
background-color: var(--kungalgame-blue-4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile {
|
|
||||||
border-radius: 14px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,5 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
// Global message component (top)
|
||||||
import Message from '@/components/alert/Message'
|
import Message from '@/components/alert/Message'
|
||||||
|
|
||||||
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
|
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
|
||||||
|
@ -21,6 +23,7 @@ const setFont = () => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<!-- Set the width of certain pages -->
|
||||||
<div class="font">
|
<div class="font">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<span>{{ $tm('header.settings.font') }}</span>
|
<span>{{ $tm('header.settings.font') }}</span>
|
||||||
|
@ -31,7 +34,6 @@ const setFont = () => {
|
||||||
{{ showKUNGalgameFontStyle }}
|
{{ showKUNGalgameFontStyle }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="font-input">
|
<div class="font-input">
|
||||||
<input
|
<input
|
||||||
:placeholder="`${$tm('header.settings.fontInput')}`"
|
:placeholder="`${$tm('header.settings.fontInput')}`"
|
||||||
|
@ -53,6 +55,7 @@ const setFont = () => {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* URL input box */
|
||||||
.font-input {
|
.font-input {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -68,12 +71,13 @@ const setFont = () => {
|
||||||
background-color: var(--kungalgame-trans-white-9);
|
background-color: var(--kungalgame-trans-white-9);
|
||||||
color: var(--kungalgame-font-color-3);
|
color: var(--kungalgame-font-color-3);
|
||||||
|
|
||||||
|
/* Focus on the input box */
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
background-color: var(--kungalgame-trans-blue-0);
|
background-color: var(--kungalgame-trans-blue-0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* Confirm button */
|
||||||
button {
|
button {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
|
@ -85,9 +89,11 @@ const setFont = () => {
|
||||||
background-color: var(--kungalgame-trans-white-5);
|
background-color: var(--kungalgame-trans-white-5);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
/* Confirm button hover effect */
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--kungalgame-trans-red-1);
|
background-color: var(--kungalgame-trans-red-1);
|
||||||
|
|
||||||
|
/* Confirm button active effect */
|
||||||
&:active {
|
&:active {
|
||||||
background-color: var(--kungalgame-trans-red-3);
|
background-color: var(--kungalgame-trans-red-3);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useLoliDataURL } from '@/hooks/useLoli'
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import { getLoli } from './loli'
|
|
||||||
|
|
||||||
import LoliSkeleton from '@/components/skeleton/settings-panel/LoliSkeleton.vue'
|
|
||||||
import KUNGalgameLoading from '@/components/loading/KUNGalgameLoading.vue'
|
|
||||||
|
|
||||||
const loliData = ref({
|
const loliData = ref({
|
||||||
loliBodyLeft: '',
|
loliBodyLeft: '',
|
||||||
|
@ -22,71 +19,57 @@ const loliData = ref({
|
||||||
mouth: '',
|
mouth: '',
|
||||||
face: '',
|
face: '',
|
||||||
})
|
})
|
||||||
const isShowLoading = ref(false)
|
|
||||||
|
|
||||||
const reGetLoli = async () => {
|
const reGetLoli = async () => (loliData.value = await useLoliDataURL())
|
||||||
isShowLoading.value = true
|
|
||||||
loliData.value = await getLoli()
|
|
||||||
isShowLoading.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await reGetLoli()
|
loliData.value = await useLoliDataURL()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="loli-container">
|
<div class="loli" @click="reGetLoli">
|
||||||
<div class="loli" @click="reGetLoli" v-if="loliData.body">
|
<img
|
||||||
<img
|
class="body"
|
||||||
class="body"
|
:src="loliData.body"
|
||||||
:src="loliData.body"
|
alt="ren"
|
||||||
alt="ren"
|
:style="{ left: loliData.loliBodyLeft, top: loliData.loliBodyTop }"
|
||||||
:style="{ left: loliData.loliBodyLeft, top: loliData.loliBodyTop }"
|
/>
|
||||||
/>
|
<img
|
||||||
<img
|
class="eye"
|
||||||
class="eye"
|
:src="loliData.eye"
|
||||||
:src="loliData.eye"
|
alt="ren"
|
||||||
alt="ren"
|
:style="{ left: loliData.loliEyeLeft, top: loliData.loliEyeTop }"
|
||||||
:style="{ left: loliData.loliEyeLeft, top: loliData.loliEyeTop }"
|
/>
|
||||||
/>
|
<img
|
||||||
<img
|
class="brow"
|
||||||
class="brow"
|
:src="loliData.brow"
|
||||||
:src="loliData.brow"
|
alt="ren"
|
||||||
alt="ren"
|
:style="{ left: loliData.loliBrowLeft, top: loliData.loliBrowTop }"
|
||||||
:style="{ left: loliData.loliBrowLeft, top: loliData.loliBrowTop }"
|
/>
|
||||||
/>
|
<img
|
||||||
<img
|
class="mouth"
|
||||||
class="mouth"
|
:src="loliData.mouth"
|
||||||
:src="loliData.mouth"
|
alt="ren"
|
||||||
alt="ren"
|
:style="{ left: loliData.loliMouthLeft, top: loliData.loliMouthTop }"
|
||||||
:style="{ left: loliData.loliMouthLeft, top: loliData.loliMouthTop }"
|
/>
|
||||||
/>
|
<img
|
||||||
<img
|
class="face"
|
||||||
class="face"
|
:src="loliData.face"
|
||||||
:src="loliData.face"
|
alt="ren"
|
||||||
alt="ren"
|
:style="{ left: loliData.loliFaceLeft, top: loliData.loliFaceTop }"
|
||||||
:style="{ left: loliData.loliFaceLeft, top: loliData.loliFaceTop }"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<LoliSkeleton v-if="!loliData.body" />
|
|
||||||
|
|
||||||
<KUNGalgameLoading v-if="isShowLoading" style="top: 310px; left: 140px" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.loli-container {
|
|
||||||
top: -270px;
|
|
||||||
left: 130px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loli {
|
.loli {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 0;
|
width: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
|
top: -270px;
|
||||||
|
left: 130px;
|
||||||
}
|
}
|
||||||
.body {
|
.body {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -1,25 +1,30 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
// Import the icon font
|
||||||
import { Icon } from '@iconify/vue'
|
import { Icon } from '@iconify/vue'
|
||||||
|
// Import the settings store
|
||||||
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
|
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
|
||||||
|
|
||||||
|
// Use the settings store
|
||||||
|
const settingsStore = useKUNGalgameSettingsStore()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="mode">
|
<div class="mode">
|
||||||
|
<!-- Day / Night mode toggle -->
|
||||||
<span>{{ $tm('header.settings.mode') }}</span>
|
<span>{{ $tm('header.settings.mode') }}</span>
|
||||||
<div class="mode-container">
|
<div class="mode-container">
|
||||||
<span>
|
<span>
|
||||||
<Icon
|
<Icon
|
||||||
class="sun"
|
class="sun"
|
||||||
icon="line-md:moon-filled-alt-to-sunny-filled-loop-transition"
|
icon="line-md:moon-filled-alt-to-sunny-filled-loop-transition"
|
||||||
@click="useKUNGalgameSettingsStore().setKUNGalgameTheme('')"
|
@click="settingsStore.setKUNGalgameTheme('')"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
<Icon
|
<Icon
|
||||||
class="moon"
|
class="moon"
|
||||||
icon="line-md:sunny-outline-to-moon-loop-transition"
|
icon="line-md:sunny-outline-to-moon-loop-transition"
|
||||||
@click="useKUNGalgameSettingsStore().setKUNGalgameTheme('dark')"
|
@click="settingsStore.setKUNGalgameTheme('dark')"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -33,7 +38,6 @@ import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-container {
|
.mode-container {
|
||||||
font-size: 25px;
|
font-size: 25px;
|
||||||
width: 60%;
|
width: 60%;
|
||||||
|
|
|
@ -9,10 +9,14 @@ const route = useRoute()
|
||||||
const settingsStore = useKUNGalgameSettingsStore()
|
const settingsStore = useKUNGalgameSettingsStore()
|
||||||
const { showKUNGalgamePageWidth } = storeToRefs(settingsStore)
|
const { showKUNGalgamePageWidth } = storeToRefs(settingsStore)
|
||||||
|
|
||||||
|
// Page width
|
||||||
const pageWidth = ref(0)
|
const pageWidth = ref(0)
|
||||||
|
// Current route name
|
||||||
const routeName = computed(() => route.name as string)
|
const routeName = computed(() => route.name as string)
|
||||||
|
// Whether page width adjustment is disabled
|
||||||
const isDisabled = ref(false)
|
const isDisabled = ref(false)
|
||||||
|
|
||||||
|
// Pages where width adjustment is allowed
|
||||||
const pageNameArray = [
|
const pageNameArray = [
|
||||||
'KUN',
|
'KUN',
|
||||||
'Topic',
|
'Topic',
|
||||||
|
@ -24,9 +28,12 @@ const pageNameArray = [
|
||||||
'ThanksList',
|
'ThanksList',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Initialize page width
|
||||||
const initPageWidth = () => {
|
const initPageWidth = () => {
|
||||||
if (pageNameArray.includes(routeName.value)) {
|
if (pageNameArray.includes(routeName.value)) {
|
||||||
|
// Page width value equals store width value
|
||||||
pageWidth.value = showKUNGalgamePageWidth.value[routeName.value]
|
pageWidth.value = showKUNGalgamePageWidth.value[routeName.value]
|
||||||
|
// Enable input
|
||||||
isDisabled.value = false
|
isDisabled.value = false
|
||||||
} else {
|
} else {
|
||||||
isDisabled.value = true
|
isDisabled.value = true
|
||||||
|
@ -35,6 +42,7 @@ const initPageWidth = () => {
|
||||||
|
|
||||||
watch(pageWidth, () => {
|
watch(pageWidth, () => {
|
||||||
if (pageNameArray.includes(routeName.value)) {
|
if (pageNameArray.includes(routeName.value)) {
|
||||||
|
// Store user-input width
|
||||||
showKUNGalgamePageWidth.value[routeName.value] = pageWidth.value
|
showKUNGalgamePageWidth.value[routeName.value] = pageWidth.value
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -49,6 +57,7 @@ onActivated(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<!-- Set the width for specific pages -->
|
||||||
<div
|
<div
|
||||||
class="width"
|
class="width"
|
||||||
:class="isDisabled ? 'disabled' : ''"
|
:class="isDisabled ? 'disabled' : ''"
|
||||||
|
@ -58,7 +67,6 @@ onActivated(() => {
|
||||||
<span>{{ $tm('header.settings.width') }}</span>
|
<span>{{ $tm('header.settings.width') }}</span>
|
||||||
<span>{{ pageWidth }}%</span>
|
<span>{{ pageWidth }}%</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="page-width">
|
<div class="page-width">
|
||||||
<span>50%</span>
|
<span>50%</span>
|
||||||
<input
|
<input
|
||||||
|
@ -85,7 +93,7 @@ onActivated(() => {
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* Main page width slider */
|
||||||
.main {
|
.main {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
|
@ -97,6 +105,7 @@ onActivated(() => {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Styles when page width adjustment is disabled */
|
||||||
.disabled {
|
.disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
color: var(--kungalgame-font-color-0);
|
color: var(--kungalgame-font-color-0);
|
||||||
|
|
|
@ -1,11 +1,18 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { watch } from 'vue'
|
// Import the settings store
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
|
|
||||||
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
|
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { watch } from 'vue'
|
||||||
|
// Import i18n
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
const { showKUNGalgameLanguage } = storeToRefs(useKUNGalgameSettingsStore())
|
// Use the settings store
|
||||||
|
const settingsStore = useKUNGalgameSettingsStore()
|
||||||
|
const { showKUNGalgameLanguage } = storeToRefs(settingsStore)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Website language settings
|
||||||
|
*/
|
||||||
const { locale } = useI18n({ useScope: 'global' })
|
const { locale } = useI18n({ useScope: 'global' })
|
||||||
|
|
||||||
watch(showKUNGalgameLanguage, () => {
|
watch(showKUNGalgameLanguage, () => {
|
||||||
|
@ -24,12 +31,13 @@ watch(showKUNGalgameLanguage, () => {
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
// Language settings
|
||||||
.set-lang {
|
.set-lang {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
// Language selection box
|
||||||
.select {
|
.select {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|
|
@ -11,78 +11,46 @@
|
||||||
https://s3.bmp.ovh/imgs/2023/05/30/7aa57120cc6977a1.png
|
https://s3.bmp.ovh/imgs/2023/05/30/7aa57120cc6977a1.png
|
||||||
*/
|
*/
|
||||||
|
|
||||||
interface Background {
|
interface background {
|
||||||
index: number
|
index: number
|
||||||
message: BackgroundDetail
|
alt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BackgroundDetail {
|
export const backgroundImages: background[] = [
|
||||||
en: string
|
|
||||||
zh: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const backgroundImages: Background[] = [
|
|
||||||
{
|
{
|
||||||
index: 1,
|
index: 1,
|
||||||
message: {
|
alt: 'Akai Hitomi ni Utsuru Sekai 紅い瞳に映るセカイ 红瞳映入的世界',
|
||||||
en: 'Akai Hitomi ni Utsuru Sekai 紅い瞳に映るセカイ',
|
|
||||||
zh: '紅い瞳に映るセカイ 红瞳映入的世界',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
index: 2,
|
index: 2,
|
||||||
message: {
|
alt: 'Shugaten! しゅがてん! 糖调',
|
||||||
en: 'Shugaten! しゅがてん!',
|
|
||||||
zh: 'しゅがてん! 糖调',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
index: 3,
|
index: 3,
|
||||||
message: {
|
alt: 'Amayui Castle Meister 天結いキャッスルマイスター 天结神缘',
|
||||||
en: 'Amayui Castle Meister 天結いキャッスルマイスター',
|
|
||||||
zh: '天結いキャッスルマイスター 天结神缘',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
index: 4,
|
index: 4,
|
||||||
message: {
|
alt: 'Pieces Wataridori no Somnium 渡り鳥のソムニウム 渡鸟的梦',
|
||||||
en: 'Pieces Wataridori no Somnium 渡り鳥のソムニウム',
|
|
||||||
zh: '渡り鳥のソムニウム 渡鸟的梦',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
index: 5,
|
index: 5,
|
||||||
message: {
|
alt: 'Karenai Sekai to Owaru Hana 枯れない世界と終わる花 不败世界与终焉之花',
|
||||||
en: 'Karenai Sekai to Owaru Hana 枯れない世界と終わる花',
|
|
||||||
zh: '枯れない世界と終わる花 不败世界与终焉之花',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
index: 6,
|
index: 6,
|
||||||
message: {
|
alt: 'NEKOPARA ネコぱら 猫娘乐园',
|
||||||
en: 'NEKOPARA ネコぱら',
|
|
||||||
zh: 'ネコぱら 猫娘乐园',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
index: 7,
|
index: 7,
|
||||||
message: {
|
alt: 'Sakura no Uta サクラノ詩 樱之诗',
|
||||||
en: 'Sakura no Uta サクラノ詩',
|
|
||||||
zh: 'サクラノ詩 樱之诗',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
index: 8,
|
index: 8,
|
||||||
message: {
|
alt: 'Hokenshitsu no Sensei to Shabondama Chuudoku no Joshu 保健室のセンセーとシャボン玉中毒の助手 保健室的老师与肥皂泡中毒的助手',
|
||||||
en: 'Hokenshitsu no Sensei to Shabondama Chuudoku no Joshu 保健室のセンセーとシャボン玉中毒の助手',
|
|
||||||
zh: '保健室のセンセーとシャボン玉中毒の助手 保健室的老师与肥皂泡中毒的助手',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
index: 9,
|
index: 9,
|
||||||
message: {
|
alt: 'Senren * Banka 千恋*万花 千恋*万花',
|
||||||
en: 'Senren * Banka 千戀*萬花',
|
|
||||||
zh: '千戀*萬花 千恋*万花',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,72 +0,0 @@
|
||||||
import loliData from '@/assets/ren/ren.json'
|
|
||||||
import { randomNum } from '@/utils/random'
|
|
||||||
|
|
||||||
export const getLoli = async () => {
|
|
||||||
const linkPreset = `https://cdn.jsdelivr.net/gh/kun-moe/kun-image@main/ren/`
|
|
||||||
const getAssetsFile = (name: number) => `${linkPreset}${name}.webp`
|
|
||||||
|
|
||||||
const randomBrow = randomNum(1, 18)
|
|
||||||
const randomEye = randomNum(19, 36)
|
|
||||||
const randomMouth = randomNum(37, 56)
|
|
||||||
const randomFace = randomNum(57, 62)
|
|
||||||
const randomSkirt = randomNum(63, 70)
|
|
||||||
|
|
||||||
const loli = {
|
|
||||||
lass: loliData[randomSkirt],
|
|
||||||
eye: loliData[randomEye],
|
|
||||||
brow: loliData[randomBrow],
|
|
||||||
mouth: loliData[randomMouth],
|
|
||||||
face: loliData[randomFace],
|
|
||||||
}
|
|
||||||
|
|
||||||
const loliBodyLeft = `${loli.lass.left}px`
|
|
||||||
const loliBodyTop = `${loli.lass.top}px`
|
|
||||||
|
|
||||||
const loliEyeLeft = `${loli.eye.left}px`
|
|
||||||
const loliEyeTop = `${loli.eye.top}px`
|
|
||||||
|
|
||||||
const loliBrowLeft = `${loli.brow.left}px`
|
|
||||||
const loliBrowTop = `${loli.brow.top}px`
|
|
||||||
|
|
||||||
const loliMouthLeft = `${loli.mouth.left}px`
|
|
||||||
const loliMouthTop = `${loli.mouth.top}px`
|
|
||||||
|
|
||||||
const loliFaceLeft = `${loli.face.left}px`
|
|
||||||
const loliFaceTop = `${loli.face.top}px`
|
|
||||||
|
|
||||||
const promises = [
|
|
||||||
getAssetsFile(loli.lass.layer_id),
|
|
||||||
getAssetsFile(loli.eye.layer_id),
|
|
||||||
getAssetsFile(loli.brow.layer_id),
|
|
||||||
getAssetsFile(loli.mouth.layer_id),
|
|
||||||
getAssetsFile(loli.face.layer_id),
|
|
||||||
].map((url) => fetch(url))
|
|
||||||
|
|
||||||
const responses = await Promise.all(promises)
|
|
||||||
const results = await Promise.all(
|
|
||||||
responses.map((response) => response.blob())
|
|
||||||
)
|
|
||||||
|
|
||||||
const [body, eye, brow, mouth, face] = results.map((blob) =>
|
|
||||||
URL.createObjectURL(blob)
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
loliBodyLeft,
|
|
||||||
loliBodyTop,
|
|
||||||
loliEyeLeft,
|
|
||||||
loliEyeTop,
|
|
||||||
loliBrowLeft,
|
|
||||||
loliBrowTop,
|
|
||||||
loliMouthLeft,
|
|
||||||
loliMouthTop,
|
|
||||||
loliFaceLeft,
|
|
||||||
loliFaceTop,
|
|
||||||
|
|
||||||
body,
|
|
||||||
eye,
|
|
||||||
brow,
|
|
||||||
mouth,
|
|
||||||
face,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
const count = 10
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="skeleton">
|
|
||||||
<ul>
|
|
||||||
<li v-for="(_, index) in count" :key="index"></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.skeleton {
|
|
||||||
width: 100%;
|
|
||||||
height: 430px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
background-color: var(--kungalgame-trans-white-5);
|
|
||||||
border-radius: 3px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 10px;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
li {
|
|
||||||
background-image: linear-gradient(
|
|
||||||
90deg,
|
|
||||||
var(--kungalgame-trans-blue-2) 25%,
|
|
||||||
var(--kungalgame-pink-0) 37%,
|
|
||||||
var(--kungalgame-trans-blue-2) 63%
|
|
||||||
);
|
|
||||||
border-radius: 3px;
|
|
||||||
width: 100%;
|
|
||||||
height: 30px;
|
|
||||||
list-style: none;
|
|
||||||
background-size: 400% 100%;
|
|
||||||
margin-top: 10px;
|
|
||||||
background-position: 100% 50%;
|
|
||||||
animation: skeleton 1.7s ease infinite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes skeleton {
|
|
||||||
0% {
|
|
||||||
background-position: 100% 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
background-position: 0 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,92 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
count?: number
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const count = computed(() => (props.count ? props.count : 1))
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div v-for="(_, index) in count" :key="index" class="skeleton">
|
|
||||||
<div class="container">
|
|
||||||
<span></span>
|
|
||||||
<ul>
|
|
||||||
<li></li>
|
|
||||||
<li></li>
|
|
||||||
<li></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.skeleton {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
background-color: var(--kungalgame-trans-white-5);
|
|
||||||
border-radius: 3px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 12px;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
span {
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
background-color: var(--kungalgame-trans-blue-2);
|
|
||||||
margin-right: 10px;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
li {
|
|
||||||
background-image: linear-gradient(
|
|
||||||
90deg,
|
|
||||||
var(--kungalgame-trans-blue-2) 25%,
|
|
||||||
var(--kungalgame-pink-0) 37%,
|
|
||||||
var(--kungalgame-trans-blue-2) 63%
|
|
||||||
);
|
|
||||||
border-radius: 3px;
|
|
||||||
width: 100%;
|
|
||||||
height: 10px;
|
|
||||||
list-style: none;
|
|
||||||
background-size: 400% 100%;
|
|
||||||
margin-top: 10px;
|
|
||||||
background-position: 100% 50%;
|
|
||||||
animation: skeleton 1.7s ease infinite;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-top: 0;
|
|
||||||
width: 23%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
width: 77%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes skeleton {
|
|
||||||
0% {
|
|
||||||
background-position: 100% 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
background-position: 0 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,70 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
const count = 10
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div v-for="(_, index) in count" :key="index" class="skeleton">
|
|
||||||
<ul>
|
|
||||||
<li></li>
|
|
||||||
<li></li>
|
|
||||||
<li></li>
|
|
||||||
<li></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.skeleton {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: var(--kungalgame-trans-white-5);
|
|
||||||
border-radius: 3px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
li {
|
|
||||||
background-image: linear-gradient(
|
|
||||||
90deg,
|
|
||||||
var(--kungalgame-trans-blue-2) 25%,
|
|
||||||
var(--kungalgame-pink-0) 37%,
|
|
||||||
var(--kungalgame-trans-blue-2) 63%
|
|
||||||
);
|
|
||||||
border-radius: 3px;
|
|
||||||
width: 100%;
|
|
||||||
height: 10px;
|
|
||||||
list-style: none;
|
|
||||||
background-size: 400% 100%;
|
|
||||||
margin-top: 10px;
|
|
||||||
background-position: 100% 50%;
|
|
||||||
animation: skeleton 1.7s ease infinite;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-top: 0;
|
|
||||||
width: 50%;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
width: 77%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes skeleton {
|
|
||||||
0% {
|
|
||||||
background-position: 100% 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
background-position: 0 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,52 +0,0 @@
|
||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="skeleton">
|
|
||||||
<ul>
|
|
||||||
<li></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.skeleton {
|
|
||||||
position: absolute;
|
|
||||||
width: 70px;
|
|
||||||
height: 40px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
background-color: var(--kungalgame-trans-white-5);
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
li {
|
|
||||||
background-image: linear-gradient(
|
|
||||||
90deg,
|
|
||||||
var(--kungalgame-trans-blue-2) 25%,
|
|
||||||
var(--kungalgame-pink-0) 37%,
|
|
||||||
var(--kungalgame-trans-blue-2) 63%
|
|
||||||
);
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
list-style: none;
|
|
||||||
background-size: 400% 100%;
|
|
||||||
background-position: 100% 50%;
|
|
||||||
animation: skeleton 1.7s ease infinite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes skeleton {
|
|
||||||
0% {
|
|
||||||
background-position: 100% 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
background-position: 0 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,40 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { Icon } from '@iconify/vue'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="skeleton">
|
|
||||||
<ul>
|
|
||||||
<li><Icon icon="line-md:image" /></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.skeleton {
|
|
||||||
position: absolute;
|
|
||||||
top: 320px;
|
|
||||||
left: 140px;
|
|
||||||
width: 309px;
|
|
||||||
height: 600px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
li {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 77px;
|
|
||||||
color: var(--kungalgame-blue-4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,85 +0,0 @@
|
||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="skeleton">
|
|
||||||
<div class="container">
|
|
||||||
<span></span>
|
|
||||||
<ul>
|
|
||||||
<li></li>
|
|
||||||
<li></li>
|
|
||||||
<li></li>
|
|
||||||
<li></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.skeleton {
|
|
||||||
margin-top: 30px;
|
|
||||||
width: 100%;
|
|
||||||
height: 150px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
background-color: var(--kungalgame-trans-white-5);
|
|
||||||
border-radius: 3px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
span {
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
background-color: var(--kungalgame-trans-blue-2);
|
|
||||||
margin-right: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
li {
|
|
||||||
background-image: linear-gradient(
|
|
||||||
90deg,
|
|
||||||
var(--kungalgame-trans-blue-2) 25%,
|
|
||||||
var(--kungalgame-pink-0) 37%,
|
|
||||||
var(--kungalgame-trans-blue-2) 63%
|
|
||||||
);
|
|
||||||
border-radius: 3px;
|
|
||||||
width: 100%;
|
|
||||||
height: 17px;
|
|
||||||
list-style: none;
|
|
||||||
background-size: 400% 100%;
|
|
||||||
margin-top: 10px;
|
|
||||||
background-position: 100% 50%;
|
|
||||||
animation: skeleton 1.7s ease infinite;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-top: 0;
|
|
||||||
height: 30px;
|
|
||||||
width: 23%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
width: 77%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes skeleton {
|
|
||||||
0% {
|
|
||||||
background-position: 100% 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
background-position: 0 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,65 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
const count = 5
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div v-for="(_, index) in count" :key="index" class="skeleton">
|
|
||||||
<ul>
|
|
||||||
<li></li>
|
|
||||||
<li></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.skeleton {
|
|
||||||
width: 100%;
|
|
||||||
height: 60px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 7px;
|
|
||||||
background-color: var(--kungalgame-trans-white-5);
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 12px;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-radius: 0 0 5px 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
li {
|
|
||||||
background-image: linear-gradient(
|
|
||||||
90deg,
|
|
||||||
var(--kungalgame-trans-blue-2) 25%,
|
|
||||||
var(--kungalgame-pink-0) 37%,
|
|
||||||
var(--kungalgame-trans-blue-2) 63%
|
|
||||||
);
|
|
||||||
border-radius: 3px;
|
|
||||||
width: 100%;
|
|
||||||
height: 16px;
|
|
||||||
list-style: none;
|
|
||||||
background-size: 400% 100%;
|
|
||||||
background-position: 100% 50%;
|
|
||||||
animation: skeleton 1.7s ease infinite;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes skeleton {
|
|
||||||
0% {
|
|
||||||
background-position: 100% 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
background-position: 0 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
94
src/components/tooltip/KUNGalgameTooltip.vue
Normal file
94
src/components/tooltip/KUNGalgameTooltip.vue
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, defineProps } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
content: String,
|
||||||
|
position: String, // 'top', 'bottom', 'left', 'right'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="tooltip">
|
||||||
|
<div class="tooltip-content">
|
||||||
|
{{ content }}
|
||||||
|
</div>
|
||||||
|
<div class="tooltip-arrow"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.tooltip {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-content {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
background-color: #333;
|
||||||
|
color: #fff;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-arrow {
|
||||||
|
position: absolute;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-width: 6px;
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip.top .tooltip-content {
|
||||||
|
bottom: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip.top .tooltip-arrow {
|
||||||
|
top: 100%;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -6px;
|
||||||
|
border-color: transparent transparent #333 transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip.bottom .tooltip-content {
|
||||||
|
top: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip.bottom .tooltip-arrow {
|
||||||
|
bottom: 100%;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -6px;
|
||||||
|
border-color: #333 transparent transparent transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip.left .tooltip-content {
|
||||||
|
top: 50%;
|
||||||
|
right: 100%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip.left .tooltip-arrow {
|
||||||
|
top: 50%;
|
||||||
|
right: 100%;
|
||||||
|
margin-top: -6px;
|
||||||
|
border-color: transparent #333 transparent transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip.right .tooltip-content {
|
||||||
|
top: 50%;
|
||||||
|
left: 100%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip.right .tooltip-arrow {
|
||||||
|
top: 50%;
|
||||||
|
left: 100%;
|
||||||
|
margin-top: -6px;
|
||||||
|
border-color: transparent transparent transparent #333;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,73 +1,65 @@
|
||||||
|
<!-- This file is for adapting the top navigation bar for mobile devices -->
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue'
|
||||||
|
// Import mode switch component
|
||||||
import Mode from '../setting-panel/components/Mode.vue'
|
import Mode from '../setting-panel/components/Mode.vue'
|
||||||
|
// Import language switch component
|
||||||
import SwitchLanguage from '../setting-panel/components/SwitchLanguage.vue'
|
import SwitchLanguage from '../setting-panel/components/SwitchLanguage.vue'
|
||||||
import CustomBackground from '../setting-panel/components/CustomBackground.vue'
|
// Import top navigation bar items
|
||||||
import { hamburgerItem } from './hamburgerItem'
|
import { topBarItem } from './topBarItem'
|
||||||
|
// Send a close command to the parent element
|
||||||
defineEmits(['showKUNGalgameHamburger'])
|
defineEmits(['showKUNGalgameHamburger'])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="root" @click="$emit('showKUNGalgameHamburger', false)">
|
<!-- Main container -->
|
||||||
<Transition
|
<div class="container">
|
||||||
enter-active-class="animate__animated animate__fadeInLeft animate__faster"
|
<div class="kungalgame">
|
||||||
appear
|
<img src="@/assets/images/favicon.webp" alt="KUNGalgame" />
|
||||||
>
|
<span>{{ $tm('header.name') }}</span>
|
||||||
<div class="container" @click.stop>
|
</div>
|
||||||
<div class="kungalgame">
|
<!-- Interactive items -->
|
||||||
<img src="@/assets/images/favicon.webp" alt="KUNGalgame" />
|
<div class="item" style="font-size: 17px">
|
||||||
<span>{{ $tm('header.name') }}</span>
|
<span v-for="kun in topBarItem" :key="kun.index">
|
||||||
</div>
|
<RouterLink :to="kun.router">{{
|
||||||
<!-- Interactive items -->
|
$tm(`header['${kun.name}']`)
|
||||||
<div class="item" style="font-size: 17px">
|
}}</RouterLink>
|
||||||
<span v-for="kun in hamburgerItem" :key="kun.index">
|
</span>
|
||||||
<RouterLink :to="kun.router">
|
</div>
|
||||||
{{ $tm(`header.hamburger.${kun.name}`) }}
|
|
||||||
</RouterLink>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Mode style="font-size: 15px" />
|
<!-- Day and night mode switch component -->
|
||||||
|
<Mode style="font-size: 20px" />
|
||||||
|
|
||||||
<SwitchLanguage style="font-size: 15px" />
|
<!-- Language switch component -->
|
||||||
|
<SwitchLanguage style="font-size: 20px; margin-bottom: 40px" />
|
||||||
<CustomBackground :is-mobile="true" />
|
<!-- Close button -->
|
||||||
|
<div class="close">
|
||||||
<div class="home">
|
<Icon
|
||||||
<RouterLink to="/kun">{{ $tm('header.hamburger.home') }}</RouterLink>
|
icon="line-md:menu-fold-left"
|
||||||
</div>
|
@click="$emit('showKUNGalgameHamburger', false)"
|
||||||
</div>
|
/>
|
||||||
</Transition>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.root {
|
.container {
|
||||||
height: 100vh;
|
height: 400px;
|
||||||
width: 100vw;
|
width: 277px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
padding: 10px;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
color: var(--kungalgame-font-color-3);
|
|
||||||
font-size: 25px;
|
|
||||||
background-color: var(--kungalgame-mask-color-0);
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
height: 100vh;
|
|
||||||
position: absolute;
|
|
||||||
width: 247px;
|
|
||||||
padding: 10px;
|
|
||||||
background-color: var(--kungalgame-trans-white-2);
|
background-color: var(--kungalgame-trans-white-2);
|
||||||
border: 1px solid var(--kungalgame-blue-1);
|
border: 1px solid var(--kungalgame-blue-1);
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
border-left: none;
|
border-left: none;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
border-radius: 0 5px 5px 5px;
|
border-radius: 0 5px 5px 5px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
color: var(--kungalgame-font-color-3);
|
||||||
|
font-size: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
|
@ -94,20 +86,9 @@ defineEmits(['showKUNGalgameHamburger'])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.home {
|
.close {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 50px;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
a {
|
|
||||||
padding: 5px 10px;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: 17px;
|
|
||||||
font-size: 17px;
|
|
||||||
border: 1px solid var(--kungalgame-blue-4);
|
|
||||||
color: var(--kungalgame-blue-4);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -4,29 +4,33 @@ import { Icon } from '@iconify/vue'
|
||||||
import 'animate.css'
|
import 'animate.css'
|
||||||
import { topBarItem } from './topBarItem'
|
import { topBarItem } from './topBarItem'
|
||||||
import { onBeforeRouteLeave } from 'vue-router'
|
import { onBeforeRouteLeave } from 'vue-router'
|
||||||
|
// Mobile version hamburger
|
||||||
const Hamburger = defineAsyncComponent(() => import('./Hamburger.vue'))
|
const Hamburger = defineAsyncComponent(() => import('./Hamburger.vue'))
|
||||||
|
// Settings panel
|
||||||
const KUNGalgameSettingsPanel = defineAsyncComponent(
|
const KUNGalgameSettingsPanel = defineAsyncComponent(
|
||||||
() => import('../setting-panel/KUNGalgameSettingPanel.vue')
|
() => import('../setting-panel/KUNGalgameSettingPanel.vue')
|
||||||
)
|
)
|
||||||
|
// Panel when clicking on the user's avatar
|
||||||
const KUNGalgameUserInfo = defineAsyncComponent(
|
const KUNGalgameUserInfo = defineAsyncComponent(
|
||||||
() => import('./KUNGalgameUserInfo.vue')
|
() => import('./KUNGalgameUserInfo.vue')
|
||||||
)
|
)
|
||||||
|
|
||||||
import { useTempHomeStore } from '@/store/temp/home'
|
|
||||||
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
|
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
|
|
||||||
const { isShowSearch } = storeToRefs(useTempHomeStore())
|
|
||||||
const { name, avatarMin } = storeToRefs(useKUNGalgameUserStore())
|
const { name, avatarMin } = storeToRefs(useKUNGalgameUserStore())
|
||||||
|
|
||||||
const showKUNGalgameHamburger = ref(false)
|
// Show settings panel state
|
||||||
const showKUNGalgamePanel = ref(false)
|
const showKUNGalgamePanel = ref(false)
|
||||||
|
// Show mobile mode hamburger state
|
||||||
|
const showKUNGalgameHamburger = ref(false)
|
||||||
|
// Show user panel when clicking on the user's avatar
|
||||||
const showKUNGalgameUserPanel = ref(false)
|
const showKUNGalgameUserPanel = ref(false)
|
||||||
|
|
||||||
|
// Set the navigation bar width based on the number of navigation items
|
||||||
const navItemNum = topBarItem.length
|
const navItemNum = topBarItem.length
|
||||||
const navItemLength = `${navItemNum}00px`
|
const navItemLength = `${navItemNum}00px`
|
||||||
|
|
||||||
|
// Destroy the SettingsPanel and Hamburger before leaving the route
|
||||||
onBeforeRouteLeave(() => {
|
onBeforeRouteLeave(() => {
|
||||||
showKUNGalgamePanel.value = false
|
showKUNGalgamePanel.value = false
|
||||||
showKUNGalgameHamburger.value = false
|
showKUNGalgameHamburger.value = false
|
||||||
|
@ -35,6 +39,7 @@ onBeforeRouteLeave(() => {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
|
<!-- Top left interactive bar -->
|
||||||
<div class="nav-top">
|
<div class="nav-top">
|
||||||
<div class="hamburger">
|
<div class="hamburger">
|
||||||
<Icon
|
<Icon
|
||||||
|
@ -42,40 +47,45 @@ onBeforeRouteLeave(() => {
|
||||||
v-if="!showKUNGalgameHamburger"
|
v-if="!showKUNGalgameHamburger"
|
||||||
@click="showKUNGalgameHamburger = !showKUNGalgameHamburger"
|
@click="showKUNGalgameHamburger = !showKUNGalgameHamburger"
|
||||||
/>
|
/>
|
||||||
<Transition name="hamburger">
|
<transition
|
||||||
<Hamburger
|
enter-active-class="animate__animated animate__fadeInLeft animate__faster"
|
||||||
v-if="showKUNGalgameHamburger"
|
leave-active-class="animate__animated animate__fadeOutLeft animate__faster"
|
||||||
@showKUNGalgameHamburger="showKUNGalgameHamburger = false"
|
>
|
||||||
/>
|
<KeepAlive>
|
||||||
</Transition>
|
<Hamburger
|
||||||
|
v-if="showKUNGalgameHamburger"
|
||||||
|
@showKUNGalgameHamburger="showKUNGalgameHamburger = false"
|
||||||
|
/>
|
||||||
|
</KeepAlive>
|
||||||
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Website name and logo -->
|
||||||
<div class="kungalgame">
|
<div class="kungalgame">
|
||||||
<RouterLink to="/kun">
|
<RouterLink to="/kun">
|
||||||
<img
|
<img
|
||||||
src="@/assets/images/favicon.webp"
|
src="@/assets/images/favicon.webp"
|
||||||
alt="KUN Visual Novel | 鲲 Galgame"
|
alt="KUN Visual Novel 鲲 Galgame"
|
||||||
/>
|
/>
|
||||||
<span>{{ $tm('header.name') }}</span>
|
<span>{{ $tm('header.name') }}</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Navigation bar -->
|
||||||
<div class="top-bar">
|
<div class="top-bar">
|
||||||
|
<!-- Top individual sections -->
|
||||||
<span v-for="kun in topBarItem" :key="kun.index">
|
<span v-for="kun in topBarItem" :key="kun.index">
|
||||||
<RouterLink :to="{ path: kun.router }">
|
<RouterLink :to="{ path: kun.router }">
|
||||||
{{ $tm(`header.${kun.name}`) }}
|
{{ $tm(`header['${kun.name}']`) }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</span>
|
</span>
|
||||||
|
<!-- Hover effect under the top section -->
|
||||||
<div class="box"></div>
|
<div class="box"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="kungalgamer-info">
|
<div class="kungalgamer-info">
|
||||||
<span class="search" @click="isShowSearch = true">
|
<!-- showKUNGalgamePanel is a boolean value in the store, true/false controls the display and close of the settings panel -->
|
||||||
<Icon icon="line-md:search" />
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class="settings"
|
class="settings"
|
||||||
@click="showKUNGalgamePanel = !showKUNGalgamePanel"
|
@click="showKUNGalgamePanel = !showKUNGalgamePanel"
|
||||||
|
@ -125,6 +135,7 @@ onBeforeRouteLeave(() => {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
/* 相对于设置面板定位 */
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
margin-bottom: 7px;
|
margin-bottom: 7px;
|
||||||
|
@ -236,16 +247,6 @@ $navNumber: v-bind(navItemNum);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-right: 50px;
|
margin-right: 50px;
|
||||||
|
|
||||||
.search {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--kungalgame-font-color-2);
|
|
||||||
font-size: 25px;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-right: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings {
|
.settings {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -288,20 +289,6 @@ $navNumber: v-bind(navItemNum);
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hamburger-enter-from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hamburger-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hamburger-enter-from .container,
|
|
||||||
.hamburger-leave-to .container {
|
|
||||||
-webkit-transform: scale(1.1);
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1000px) {
|
@media (max-width: 1000px) {
|
||||||
.kungalgame {
|
.kungalgame {
|
||||||
span {
|
span {
|
||||||
|
@ -311,13 +298,12 @@ $navNumber: v-bind(navItemNum);
|
||||||
margin-right: 0 !important;
|
margin-right: 0 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 700px) {
|
||||||
|
.settings {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
.top-bar {
|
.top-bar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -332,11 +318,5 @@ $navNumber: v-bind(navItemNum);
|
||||||
.kungalgamer-info {
|
.kungalgamer-info {
|
||||||
margin-right: 30px;
|
margin-right: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
|
||||||
img {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -5,11 +5,13 @@ import { useRouter } from 'vue-router'
|
||||||
// Global message component (top)
|
// Global message component (top)
|
||||||
import Message from '@/components/alert/Message'
|
import Message from '@/components/alert/Message'
|
||||||
// Global message component (bottom)
|
// Global message component (bottom)
|
||||||
import { useTempMessageStore } from '@/store/temp/message'
|
import { useKUNGalgameMessageStore } from '@/store/modules/message'
|
||||||
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
|
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
// Reset store
|
// Reset store
|
||||||
import { kungalgameStoreReset } from '@/store'
|
import { kungalgameStoreReset } from '@/store'
|
||||||
|
// Reset router
|
||||||
|
import { resetRouter } from '@/router'
|
||||||
|
|
||||||
const { uid, name, moemoepoint } = storeToRefs(useKUNGalgameUserStore())
|
const { uid, name, moemoepoint } = storeToRefs(useKUNGalgameUserStore())
|
||||||
|
|
||||||
|
@ -33,10 +35,14 @@ const handlePanelBlur = async () => {
|
||||||
// Log out - for simplicity, the code here does not communicate with the backend to remove the token from Redis.
|
// Log out - for simplicity, the code here does not communicate with the backend to remove the token from Redis.
|
||||||
const logOut = async () => {
|
const logOut = async () => {
|
||||||
// Get the user's response
|
// Get the user's response
|
||||||
const res = await useTempMessageStore().alert('AlertInfo.edit.logout', true)
|
const res = await useKUNGalgameMessageStore().alert(
|
||||||
|
'AlertInfo.edit.logout',
|
||||||
|
true
|
||||||
|
)
|
||||||
if (res) {
|
if (res) {
|
||||||
kungalgameStoreReset()
|
kungalgameStoreReset()
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
|
resetRouter()
|
||||||
Message('Logout successfully!', '登出成功', 'success')
|
Message('Logout successfully!', '登出成功', 'success')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
// Interface for individual items in the top navigation bar
|
|
||||||
interface Hamburger {
|
|
||||||
index: number
|
|
||||||
name: string
|
|
||||||
router: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Items in the top navigation bar
|
|
||||||
// (be sure to include '/' here, or child routes may have issues!!!)
|
|
||||||
export const hamburgerItem: Hamburger[] = [
|
|
||||||
{ index: 1, name: 'pool', router: '/pool' },
|
|
||||||
{ index: 2, name: 'create', router: '/edit' },
|
|
||||||
{ index: 3, name: 'technique', router: '/technique' },
|
|
||||||
{ index: 4, name: 'about', router: '/kungalgame' },
|
|
||||||
{ index: 5, name: 'ranking', router: '/ranking' },
|
|
||||||
{ index: 6, name: 'update', router: '/update-log' },
|
|
||||||
{ index: 7, name: 'bylaw', router: '/bylaw' },
|
|
||||||
{ index: 8, name: 'balance', router: '/balance' },
|
|
||||||
{ index: 9, name: 'nonMoe', router: '/non-moe' },
|
|
||||||
{ index: 10, name: 'thanks', router: '/thanks-list' },
|
|
||||||
{ index: 11, name: 'join', router: '/contact' },
|
|
||||||
]
|
|
|
@ -1,54 +1,53 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
|
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
|
||||||
import { useTempMessageStore } from '@/store/temp/message'
|
// Using global notifications
|
||||||
import { storeToRefs } from 'pinia'
|
import { useKUNGalgameMessageStore } from '@/store/modules/message'
|
||||||
|
const info = useKUNGalgameMessageStore()
|
||||||
|
|
||||||
|
// Import i18n
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
const { tm } = useI18n()
|
||||||
|
|
||||||
// The parent component instructs it to send the verification code, and it will do so.
|
// The parent component instructs it to send the verification code, and it will do so.
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
email: string
|
email: string
|
||||||
|
isSendCode: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { isCaptureSuccessful } = storeToRefs(useTempMessageStore())
|
|
||||||
const info = useTempMessageStore()
|
|
||||||
|
|
||||||
const isSendCode = ref(false)
|
|
||||||
const isSending = ref(false)
|
const isSending = ref(false)
|
||||||
|
|
||||||
const countdown = ref(0)
|
const countdown = ref(0)
|
||||||
|
|
||||||
watch(
|
const sendCode = () => {
|
||||||
() => isSendCode.value,
|
// If the parent component passes a false value, return directly
|
||||||
async () => {
|
if (!props.isSendCode) {
|
||||||
if (!isSending.value) {
|
return
|
||||||
isSending.value = true
|
|
||||||
countdown.value = 30
|
|
||||||
|
|
||||||
const countdownInterval = setInterval(() => {
|
|
||||||
countdown.value -= 1
|
|
||||||
if (countdown.value === 0) {
|
|
||||||
clearInterval(countdownInterval)
|
|
||||||
isSending.value = false
|
|
||||||
}
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
// Send the verification code
|
|
||||||
await useKUNGalgameUserStore().sendCode(props.email)
|
|
||||||
|
|
||||||
info.info('AlertInfo.code.code')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
const handleSendCode = () => {
|
if (!isSending.value) {
|
||||||
if (isCaptureSuccessful.value) {
|
isSending.value = true
|
||||||
isSendCode.value = !isSendCode.value
|
countdown.value = 30
|
||||||
|
|
||||||
|
const countdownInterval = setInterval(() => {
|
||||||
|
countdown.value -= 1
|
||||||
|
if (countdown.value === 0) {
|
||||||
|
clearInterval(countdownInterval)
|
||||||
|
isSending.value = false
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
// Send the verification code
|
||||||
|
useKUNGalgameUserStore().sendCode(props.email)
|
||||||
|
|
||||||
|
info.info(tm('AlertInfo.code.code'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<button @click="handleSendCode" :disabled="isSending">
|
<button @click="sendCode" :disabled="isSending">
|
||||||
{{ isSending ? countdown : $tm('login.register.send') }}
|
{{ isSending ? countdown : $tm('login.register.send') }}
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -4,15 +4,12 @@
|
||||||
|
|
||||||
import { type App } from 'vue'
|
import { type App } from 'vue'
|
||||||
// Directive for enlarging images on click
|
// Directive for enlarging images on click
|
||||||
// import { zoom } from './zoom/zoom'
|
import { zoom } from './zoom/zoom'
|
||||||
// Permission directive
|
// Permission directive
|
||||||
// import { permission } from './permission/permission'
|
import { permission } from './permission/permission'
|
||||||
// Tooltip directive
|
|
||||||
import { tooltip } from './tooltip/tooltip'
|
|
||||||
|
|
||||||
// Mount directives
|
// Mount directives
|
||||||
export function setupKUNGalgameDirectives(app: App) {
|
export function setupKUNGalgameDirectives(app: App) {
|
||||||
// app.directive('zoom', zoom)
|
app.directive('zoom', zoom)
|
||||||
// app.directive('permission', permission)
|
app.directive('permission', permission)
|
||||||
app.directive('tooltip', tooltip)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import type { Directive, DirectiveBinding } from 'vue'
|
import type { Directive, DirectiveBinding } from 'vue'
|
||||||
import Message from '@/components/alert/Message'
|
import Message from '@/components/alert/Message'
|
||||||
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
|
import { currentUserInfo } from '@/utils/getCurrentUserInfo'
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
|
|
||||||
const { roles, uid } = storeToRefs(useKUNGalgameUserStore())
|
// Current user's UID
|
||||||
|
const currentUserUid = currentUserInfo.uid
|
||||||
|
// Current user's roles
|
||||||
|
const currentUserRoles = currentUserInfo.roles
|
||||||
|
|
||||||
// User roles: Guest 0, User 1, Admin 2, SuperAdmin 3, User Self 4
|
// User roles: Guest 0, User 1, Admin 2, SuperAdmin 3, User Self 4
|
||||||
enum UserRole {
|
enum UserRole {
|
||||||
|
@ -38,17 +40,17 @@ const handleUnauthorizedAccess = (element: HTMLElement) => {
|
||||||
|
|
||||||
export const permission: Directive = {
|
export const permission: Directive = {
|
||||||
mounted(element: HTMLElement, binding: DirectiveBinding<BindingProps>) {
|
mounted(element: HTMLElement, binding: DirectiveBinding<BindingProps>) {
|
||||||
const bindingRoles = [...binding.value.roles]
|
const roles = [...binding.value.roles]
|
||||||
const bindingUid = binding.value.uid
|
const uid = binding.value.uid
|
||||||
|
|
||||||
const hasPermission = () => {
|
const hasPermission = () => {
|
||||||
// User Self
|
// User Self
|
||||||
if (bindingUid === uid.value) {
|
if (uid === currentUserUid) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// User has access permission
|
// User has access permission
|
||||||
if (bindingRoles.includes(roles.value)) {
|
if (roles.includes(currentUserRoles)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
import type { Directive, DirectiveBinding } from 'vue'
|
|
||||||
import { KUNGalgameLanguage } from '@/utils/getDefaultEnv'
|
|
||||||
|
|
||||||
interface TooltipBinding {
|
|
||||||
message: {
|
|
||||||
en: string
|
|
||||||
zh: string
|
|
||||||
}
|
|
||||||
position: 'top' | 'right' | 'bottom' | 'left'
|
|
||||||
}
|
|
||||||
|
|
||||||
const initializeTooltip = (element: HTMLElement, binding: DirectiveBinding) => {
|
|
||||||
const { message, position } = (binding.value as TooltipBinding) || {
|
|
||||||
message: '',
|
|
||||||
position: 'left',
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageI18n = KUNGalgameLanguage === 'en' ? message.en : message.zh
|
|
||||||
|
|
||||||
element.setAttribute('tooltip', messageI18n)
|
|
||||||
element.setAttribute('position', position)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* This plugin is enabled */
|
|
||||||
export const tooltip: Directive = {
|
|
||||||
mounted(element: HTMLElement, binding: DirectiveBinding) {
|
|
||||||
initializeTooltip(element, binding)
|
|
||||||
},
|
|
||||||
|
|
||||||
updated(element: HTMLElement, binding: DirectiveBinding) {
|
|
||||||
initializeTooltip(element, binding)
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -1,86 +1,29 @@
|
||||||
/* -B means backend, message error code is defined by backend */
|
|
||||||
|
|
||||||
const errorMessagesEN: Record<number, string> = {
|
const errorMessagesEN: Record<number, string> = {
|
||||||
// User Part
|
10101: 'User not found',
|
||||||
10101: `User not found (-B)`,
|
10102: 'User password error',
|
||||||
10102: `User password error (-B)`,
|
10103: 'Email verification code error',
|
||||||
10103: `Email verification code error (-B)`,
|
10104: 'Email is already registered, please change it',
|
||||||
10104: `Email is already registered, please change it (-B)`,
|
10105: 'Username is already registered, please change it',
|
||||||
10105: `Username is already registered, please change it (-B)`,
|
|
||||||
10106: `User bio is too long (-B)`,
|
|
||||||
10107: `Invalid Email, Name, Password, or Verification Code Format (-B)`,
|
|
||||||
10108: `Invalid password format (-B)`,
|
|
||||||
10109: `Invalid Email or Verification Code Format (-B)`,
|
|
||||||
10110: `Avatar image upload error. The image is an array. (-B)`,
|
|
||||||
10111: `Avatar image upload error. The final compressed size of the image exceeds 50KB. (-B)`,
|
|
||||||
10112: `In cooldown for login, two identical login attempts should have a one-minute interval. (-B)`,
|
|
||||||
10113: `In cooldown for register, two identical register attempts should have a one-minute interval. (-B)`,
|
|
||||||
|
|
||||||
// Topic Part
|
10201: 'Your daily topic limit has been reached for today.',
|
||||||
10201: `Your daily topic limit has been reached for today. (-B)`,
|
10202: `Your moemoepoints are less than 1100, so you can't use the topic suggestion feature`,
|
||||||
10202: `Your moemoepoints are less than 1100, so you can't use the topic suggestion feature (-B)`,
|
|
||||||
10204: `Topic title length exceed 40 characters. Or empty. (-B)`,
|
|
||||||
10205: `Topic content length exceed 100007 characters. Or empty. (-B)`,
|
|
||||||
10206: `Topic with a maximum of 7 tags. Minimum one tag. (-B)`,
|
|
||||||
10207: `Topic with a maximum of 2 categories. Minimum one category. (-B)`,
|
|
||||||
10208: `Invalid topics timestamp. (-B)`,
|
|
||||||
|
|
||||||
// Auth Part
|
|
||||||
10301: `Sending emails too frequently, please waiting 30s (-B)`,
|
|
||||||
10302: `Invalid Email Format (-B)`,
|
|
||||||
10303: `Invalid Email, Password, or Verification Code Format (-B)`,
|
|
||||||
|
|
||||||
// Comment Part
|
|
||||||
10401: `Comment length exceed 1007 characters. Or empty. (-B)`,
|
|
||||||
|
|
||||||
// Reply Part
|
|
||||||
10501: `Reply with a maximum of 7 tags. (-B)`,
|
|
||||||
10502: `Single tag maximum length is 17 characters (-B)`,
|
|
||||||
10503: `Reply content is empty (-B)`,
|
|
||||||
10504: `Reply maximum length is 10007 characters (-B)`,
|
|
||||||
10505: `Invalid reply timestamp. (-B)`,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorMessagesCN: Record<number, string> = {
|
const errorMessagesCN: Record<number, string> = {
|
||||||
10101: `用户未找到 (-B)`,
|
10101: '用户未找到',
|
||||||
10102: `用户密码错误 (-B)`,
|
10102: '用户密码错误',
|
||||||
10103: `邮箱验证码错误 (-B)`,
|
10103: '邮箱验证码错误',
|
||||||
10104: `邮箱已被注册,请更改 (-B)`,
|
10104: '邮箱已被注册,请更改',
|
||||||
10105: `用户名已被注册,请修改 (-B)`,
|
10105: '用户名已被注册,请修改',
|
||||||
10106: `用户签名过长 (-B)`,
|
|
||||||
10107: `非法的邮箱, 用户名, 密码, 或验证码 (-B)`,
|
|
||||||
10108: `非法的密码格式 (-B)`,
|
|
||||||
10109: `非法的邮箱或验证码格式 (-B)`,
|
|
||||||
10110: `头像上传错误. 图片为数组 (-B)`,
|
|
||||||
10111: `头像上传错误. 图片最终压缩大小超过 50kb (-B)`,
|
|
||||||
10112: `登陆冷却中,两次相同登陆时间间隔一分钟 (-B)`,
|
|
||||||
10113: `注册冷却中,两次相同注册时间间隔一分钟 (-B)`,
|
|
||||||
|
|
||||||
10201: `您今日可以发表的话题数已达上限 (-B)`,
|
10201: '您今日可以发表的话题数已达上限',
|
||||||
10202: `您的萌萌点不足 1100, 无法使用推话题功能 (-B)`,
|
10202: '您的萌萌点不足 1100, 无法使用推话题功能',
|
||||||
10204: `话题标题长度超过 40 个字符, 或为空 (-B)`,
|
|
||||||
10205: `话题内容长度超过 100007 个字符, 或为空 (-B)`,
|
|
||||||
10206: `话题最多 7 个标签, 最少一个标签 (-B)`,
|
|
||||||
10207: `话题最多 2 个分类, 最少一个分类 (-B)`,
|
|
||||||
10208: `非法的话题时间戳. (-B)`,
|
|
||||||
|
|
||||||
10301: `发送邮件频率过快, 请等待 30 秒 (-B)`,
|
|
||||||
10302: `非法的邮箱格式 (-B)`,
|
|
||||||
10303: `非法的邮箱, 密码, 或验证码 (-B)`,
|
|
||||||
|
|
||||||
10401: `评论内容长度超过 1007 个字符, 或为空 (-B)`,
|
|
||||||
|
|
||||||
10501: `回复最多 7 个标签 (-B)`,
|
|
||||||
10502: `单个标签最长 17 个字符 (-B)`,
|
|
||||||
10503: `回复内容不可为空 (-B)`,
|
|
||||||
10504: `回复内容最大长度为 10007 个字符 (-B)`,
|
|
||||||
10505: `非法的回复时间戳 (-B)`,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getErrorMessageEN = (errorCode: number) => {
|
export const getErrorMessageEN = (errorCode: number) => {
|
||||||
return errorMessagesEN[errorCode] || `Unknown server error (-B)`
|
return errorMessagesEN[errorCode] || 'Unknown server error'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getErrorMessageCN = (errorCode: number) => {
|
export const getErrorMessageCN = (errorCode: number) => {
|
||||||
return errorMessagesCN[errorCode] || `未知的服务器错误 (-B)`
|
return errorMessagesCN[errorCode] || '未知的服务器错误'
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
|
// Global message component (top)
|
||||||
import Message from '@/components/alert/Message'
|
import Message from '@/components/alert/Message'
|
||||||
|
import { generateTokenByRefreshTokenApi } from '@/api'
|
||||||
|
// Use the user store
|
||||||
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
|
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
|
||||||
|
// Import the router
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
|
// Import known error handling functions
|
||||||
import { kungalgameErrorHandler } from './errorHandler'
|
import { kungalgameErrorHandler } from './errorHandler'
|
||||||
|
|
||||||
interface ErrorResponseData {
|
interface ErrorResponseData {
|
||||||
|
@ -13,15 +18,27 @@ interface ErrorResponseData {
|
||||||
* Then identifies errors based on custom backend status codes
|
* Then identifies errors based on custom backend status codes
|
||||||
* If unable to recognize, it throws an error.
|
* If unable to recognize, it throws an error.
|
||||||
*/
|
*/
|
||||||
export const onRequestError = async (response: Response) => {
|
export async function onRequestError(response: Response) {
|
||||||
|
// Identify errors based on status codes
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
Message(
|
// Attempt to obtain a new token using the refresh token
|
||||||
'Login expired, please log in again.',
|
const accessTokenResponse = await generateTokenByRefreshTokenApi()
|
||||||
'登陆过期,请重新登陆',
|
|
||||||
'error'
|
// If a new token is successfully obtained, set the token
|
||||||
)
|
if (accessTokenResponse.code === 200 && accessTokenResponse.data.token) {
|
||||||
useKUNGalgameUserStore().removeToken()
|
useKUNGalgameUserStore().setToken(accessTokenResponse.data.token)
|
||||||
router.push('/login')
|
// Set the page to reload with the new token applied
|
||||||
|
location.reload()
|
||||||
|
} else {
|
||||||
|
// Otherwise, prompt the user to log in again
|
||||||
|
Message(
|
||||||
|
'Login expired, please log in again.',
|
||||||
|
'登陆过期,请重新登陆',
|
||||||
|
'error'
|
||||||
|
)
|
||||||
|
useKUNGalgameUserStore().removeToken()
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,6 +51,8 @@ export const onRequestError = async (response: Response) => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the error response data
|
||||||
const data: ErrorResponseData = await response.json()
|
const data: ErrorResponseData = await response.json()
|
||||||
|
// Handle known errors
|
||||||
kungalgameErrorHandler(data.code)
|
kungalgameErrorHandler(data.code)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,8 @@ const { showKUNGalgameBackground, showKUNGalgameCustomBackground } =
|
||||||
|
|
||||||
// Fetch background image data from the backend
|
// Fetch background image data from the backend
|
||||||
const fetchGetBackground = async (imageName: string): Promise<Blob> => {
|
const fetchGetBackground = async (imageName: string): Promise<Blob> => {
|
||||||
const baseUrl = import.meta.env.VITE_API_UPLOADS_URL
|
const baseUrl = import.meta.env.VITE_API_BASE_URL
|
||||||
const url = `/image/bg/${imageName}.webp`
|
const url = `/uploads/image/bg/${imageName}.webp`
|
||||||
const fullUrl = `${baseUrl}${url}`
|
const fullUrl = `${baseUrl}${url}`
|
||||||
const response = await fetch(fullUrl, {
|
const response = await fetch(fullUrl, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
|
81
src/hooks/useLoli.ts
Normal file
81
src/hooks/useLoli.ts
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
|
||||||
|
|
||||||
|
// It's a bit strange here; there's a 'data' property on the buffer
|
||||||
|
// , not sure where it comes from
|
||||||
|
interface LoliBuffer {
|
||||||
|
data: Uint8Array
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoliData {
|
||||||
|
loliBodyLeft: string
|
||||||
|
loliBodyTop: string
|
||||||
|
loliEyeLeft: string
|
||||||
|
loliEyeTop: string
|
||||||
|
loliBrowLeft: string
|
||||||
|
loliBrowTop: string
|
||||||
|
loliMouthLeft: string
|
||||||
|
loliMouthTop: string
|
||||||
|
loliFaceLeft: string
|
||||||
|
loliFaceTop: string
|
||||||
|
|
||||||
|
body: LoliBuffer
|
||||||
|
eye: LoliBuffer
|
||||||
|
brow: LoliBuffer
|
||||||
|
mouth: LoliBuffer
|
||||||
|
face: LoliBuffer
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LoliDataResponseData = KUNGalgameResponseData<LoliData>
|
||||||
|
|
||||||
|
const fetchGetLoliData = async (): Promise<
|
||||||
|
KUNGalgameResponseData<LoliData>
|
||||||
|
> => {
|
||||||
|
const baseUrl = import.meta.env.VITE_API_BASE_URL
|
||||||
|
const url = `/loli/image`
|
||||||
|
const fullUrl = `${baseUrl}${url}`
|
||||||
|
const response = await fetch(fullUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${useKUNGalgameUserStore().getToken()}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return await response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not caching loli data here, as it would clutter the indexdb with too much data
|
||||||
|
const createImageUrl = (imageBuffer: LoliBuffer) => {
|
||||||
|
const uint8Array = new Uint8Array(imageBuffer.data)
|
||||||
|
const blob = new Blob([uint8Array], { type: 'image/webp' })
|
||||||
|
return URL.createObjectURL(blob)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useLoliDataURL = async () => {
|
||||||
|
const loliData = (await fetchGetLoliData()).data
|
||||||
|
|
||||||
|
const imageUrls = [
|
||||||
|
createImageUrl(loliData.body),
|
||||||
|
createImageUrl(loliData.eye),
|
||||||
|
createImageUrl(loliData.brow),
|
||||||
|
createImageUrl(loliData.mouth),
|
||||||
|
createImageUrl(loliData.face),
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
loliBodyLeft: loliData.loliBodyLeft,
|
||||||
|
loliBodyTop: loliData.loliBodyTop,
|
||||||
|
loliEyeLeft: loliData.loliEyeLeft,
|
||||||
|
loliEyeTop: loliData.loliEyeTop,
|
||||||
|
loliBrowLeft: loliData.loliBrowLeft,
|
||||||
|
loliBrowTop: loliData.loliBrowTop,
|
||||||
|
loliMouthLeft: loliData.loliMouthLeft,
|
||||||
|
loliMouthTop: loliData.loliMouthTop,
|
||||||
|
loliFaceLeft: loliData.loliFaceLeft,
|
||||||
|
loliFaceTop: loliData.loliFaceTop,
|
||||||
|
body: imageUrls[0],
|
||||||
|
eye: imageUrls[1],
|
||||||
|
brow: imageUrls[2],
|
||||||
|
mouth: imageUrls[3],
|
||||||
|
face: imageUrls[4],
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,21 +6,6 @@ export default {
|
||||||
technique: 'TECHNIQUE',
|
technique: 'TECHNIQUE',
|
||||||
about: 'ABOUT',
|
about: 'ABOUT',
|
||||||
return: 'HOME',
|
return: 'HOME',
|
||||||
hamburger: {
|
|
||||||
name: 'KUN Visual Novel',
|
|
||||||
pool: 'Pool',
|
|
||||||
create: 'Create Topic',
|
|
||||||
technique: 'Technique',
|
|
||||||
about: 'About Us',
|
|
||||||
ranking: 'Ranking',
|
|
||||||
update: 'Update Log',
|
|
||||||
bylaw: 'Regulations',
|
|
||||||
balance: 'P & L',
|
|
||||||
nonMoe: 'Non-moe',
|
|
||||||
thanks: 'Thanks List',
|
|
||||||
join: 'Join / Contacts',
|
|
||||||
home: 'Back Home',
|
|
||||||
},
|
|
||||||
settings: {
|
settings: {
|
||||||
name: 'Settings',
|
name: 'Settings',
|
||||||
mode: 'Mode',
|
mode: 'Mode',
|
||||||
|
@ -33,7 +18,8 @@ export default {
|
||||||
background: 'Background Setting',
|
background: 'Background Setting',
|
||||||
preset: 'Use our preset background',
|
preset: 'Use our preset background',
|
||||||
custom: 'Custom Background',
|
custom: 'Custom Background',
|
||||||
confirm: 'Confirm',
|
url: 'Paste the picture url here',
|
||||||
|
confirm: 'confirm',
|
||||||
restore: 'Restore blank background',
|
restore: 'Restore blank background',
|
||||||
recover: 'Recover all settings to default',
|
recover: 'Recover all settings to default',
|
||||||
},
|
},
|
||||||
|
@ -44,16 +30,14 @@ export default {
|
||||||
},
|
},
|
||||||
back: {
|
back: {
|
||||||
back: 'Back',
|
back: 'Back',
|
||||||
home: 'Home',
|
|
||||||
},
|
},
|
||||||
mainPage: {
|
mainPage: {
|
||||||
header: {
|
header: {
|
||||||
filter: 'Filter',
|
filter: 'Filter',
|
||||||
category: 'Category',
|
search: 'Search Topics',
|
||||||
galgame: 'Visual Novel',
|
|
||||||
technique: 'Technique',
|
|
||||||
others: 'Others',
|
|
||||||
all: 'All Topics',
|
all: 'All Topics',
|
||||||
|
history: 'Search History',
|
||||||
|
clear: 'Clear all history',
|
||||||
updated: 'Default',
|
updated: 'Default',
|
||||||
time: 'Time',
|
time: 'Time',
|
||||||
popularity: 'Popularity',
|
popularity: 'Popularity',
|
||||||
|
@ -61,15 +45,10 @@ export default {
|
||||||
likes: 'Likes',
|
likes: 'Likes',
|
||||||
replies: 'Replies',
|
replies: 'Replies',
|
||||||
comments: 'Comments',
|
comments: 'Comments',
|
||||||
search: 'Input to Auto Search',
|
|
||||||
history: 'Search History',
|
|
||||||
clear: 'Clear all history',
|
|
||||||
emptyHistory: `This loli hasn't searched for anything`,
|
|
||||||
emptyResult: 'No results found...',
|
|
||||||
},
|
},
|
||||||
asideActive: {
|
asideActive: {
|
||||||
fold: 'Fold Aside',
|
fold: 'Fold Aside',
|
||||||
create: 'CREATE TOPIC',
|
create: 'CREATE NEW!',
|
||||||
update: 'Update',
|
update: 'Update',
|
||||||
balance: 'P & L',
|
balance: 'P & L',
|
||||||
ranking: 'Ranking',
|
ranking: 'Ranking',
|
||||||
|
@ -98,7 +77,6 @@ export default {
|
||||||
acgngame: 'ACGNGAME',
|
acgngame: 'ACGNGAME',
|
||||||
shinnku: `Shinnku's Visual Novel`,
|
shinnku: `Shinnku's Visual Novel`,
|
||||||
ymgal: 'YM galgame',
|
ymgal: 'YM galgame',
|
||||||
kun: `KUN's Blog`,
|
|
||||||
},
|
},
|
||||||
describe: {
|
describe: {
|
||||||
title: 'KUN Visual Novel',
|
title: 'KUN Visual Novel',
|
||||||
|
@ -107,17 +85,19 @@ export default {
|
||||||
kun3: 'NO ADs Forever',
|
kun3: 'NO ADs Forever',
|
||||||
kun4: 'Free Forever',
|
kun4: 'Free Forever',
|
||||||
},
|
},
|
||||||
|
contact: 'Contact Us',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
topic: {
|
topic: {
|
||||||
aside: {
|
aside: {
|
||||||
floor: 'Sort by Floor Number',
|
top: 'Back Top',
|
||||||
like: 'Sort by Likes Count',
|
floorSort: 'Floor Sort',
|
||||||
comment: 'Sort by Comment Count',
|
timeSort: 'Time Sort',
|
||||||
|
likeSort: 'Like Sort',
|
||||||
|
commentSort: 'Reply Sort',
|
||||||
|
updatedSort: 'Update Sort',
|
||||||
tags: 'Topics Under the Same Tags',
|
tags: 'Topics Under the Same Tags',
|
||||||
tagsEmpty: 'The tags currently has no other topics',
|
|
||||||
master: 'Other Topics of The Master',
|
master: 'Other Topics of The Master',
|
||||||
masterEmpty: 'Master currently has no other topics',
|
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
status: 'Topic status',
|
status: 'Topic status',
|
||||||
|
@ -272,7 +252,7 @@ export default {
|
||||||
supportImage: 'Supports images up to 1007KB',
|
supportImage: 'Supports images up to 1007KB',
|
||||||
supportFormat: 'Supports jpg and png formats',
|
supportFormat: 'Supports jpg and png formats',
|
||||||
confirm: 'Confirm',
|
confirm: 'Confirm',
|
||||||
bio: 'Change Bio (Up to 107 characters)',
|
bio: 'Change Bio',
|
||||||
hint: 'Please enter your new signature, up to 107 characters',
|
hint: 'Please enter your new signature, up to 107 characters',
|
||||||
count: 'Character count',
|
count: 'Character count',
|
||||||
},
|
},
|
||||||
|
@ -309,8 +289,6 @@ export default {
|
||||||
agreement: 'User Agreement',
|
agreement: 'User Agreement',
|
||||||
privacy: 'Privacy',
|
privacy: 'Privacy',
|
||||||
redirect: 'Redirect',
|
redirect: 'Redirect',
|
||||||
kungalgame403: '403 Permission Denied',
|
|
||||||
kungalgame404: '404 Not Found',
|
|
||||||
|
|
||||||
home: 'Home',
|
home: 'Home',
|
||||||
balance: 'P & L',
|
balance: 'P & L',
|
||||||
|
@ -365,18 +343,6 @@ export default {
|
||||||
donate: 'Donate Us',
|
donate: 'Donate Us',
|
||||||
home: 'Back Home',
|
home: 'Back Home',
|
||||||
},
|
},
|
||||||
pool: {
|
|
||||||
load: 'Click to Load More Topics',
|
|
||||||
complete: `Already there's nothing left...`,
|
|
||||||
view: 'Sort by Views',
|
|
||||||
like: 'Sort by Likes',
|
|
||||||
time: 'Sort by Time',
|
|
||||||
},
|
|
||||||
technique: {
|
|
||||||
prev: 'Prev',
|
|
||||||
next: 'Next',
|
|
||||||
KKKKK: `We're not sure how this page should be written. If you have any suggestions, please contact us.`,
|
|
||||||
},
|
|
||||||
donate: {
|
donate: {
|
||||||
donate: 'Donate Us',
|
donate: 'Donate Us',
|
||||||
no: 'comes with no Moemoepoint rewards',
|
no: 'comes with no Moemoepoint rewards',
|
||||||
|
@ -397,11 +363,6 @@ export default {
|
||||||
success: 'Login successful',
|
success: 'Login successful',
|
||||||
home: 'You will be redirected to the home page in 3 seconds',
|
home: 'You will be redirected to the home page in 3 seconds',
|
||||||
},
|
},
|
||||||
footer: {
|
|
||||||
copyright: 'Copyright © 2023 KUN Visual Novel (except for images)',
|
|
||||||
openSource: 'GitHub Open Source',
|
|
||||||
reserved: 'All rights reserved | Version',
|
|
||||||
},
|
|
||||||
// 非页面组件这里统一用大驼峰
|
// 非页面组件这里统一用大驼峰
|
||||||
ComponentAlert: {
|
ComponentAlert: {
|
||||||
confirm: 'OK',
|
confirm: 'OK',
|
||||||
|
@ -411,12 +372,14 @@ export default {
|
||||||
edit: {
|
edit: {
|
||||||
publish: 'Confirm to publish?',
|
publish: 'Confirm to publish?',
|
||||||
publishSuccess: 'Publish Successfully',
|
publishSuccess: 'Publish Successfully',
|
||||||
|
publishCancel: 'Cancel Publish',
|
||||||
upvoteTopic:
|
upvoteTopic:
|
||||||
'Are you sure you want to upvote this topic? This will cost you 17 Moe Moe Points',
|
'Are you sure you want to upvote this topic? This will cost you 17 Moe Moe Points',
|
||||||
upvoteReply:
|
upvoteReply:
|
||||||
'Are you sure you want to upvote this reply? This will cost you 3 Moe Moe Points',
|
'Are you sure you want to upvote this reply? This will cost you 3 Moe Moe Points',
|
||||||
rewrite: 'Confirm to Rewrite?',
|
rewrite: 'Confirm to Rewrite?',
|
||||||
rewriteSuccess: 'Rewrite Successfully',
|
rewriteSuccess: 'Rewrite Successfully',
|
||||||
|
rewriteCancel: 'Cancel Rewrite',
|
||||||
closePanel: 'Confirm closing the panel? Your changes will not be saved.',
|
closePanel: 'Confirm closing the panel? Your changes will not be saved.',
|
||||||
draft: 'The draft has been saved successfully!',
|
draft: 'The draft has been saved successfully!',
|
||||||
leave: 'Confirm leaving the page? Your changes will not be saved.',
|
leave: 'Confirm leaving the page? Your changes will not be saved.',
|
||||||
|
@ -425,10 +388,8 @@ export default {
|
||||||
login: {
|
login: {
|
||||||
invalidUsername:
|
invalidUsername:
|
||||||
'Invalid username. Username should be 1 to 17 characters long and can include: Chinese characters, English letters, numbers, underscore, and tilde (~)',
|
'Invalid username. Username should be 1 to 17 characters long and can include: Chinese characters, English letters, numbers, underscore, and tilde (~)',
|
||||||
invalidPassword: `Invalid password format. Password must be 6 to 107 characters long and must include at least one letter and one number. It can optionally include special characters such as {'@'}!#$%^&*()-+=`,
|
invalidPassword:
|
||||||
invalidCode:
|
'Invalid password format. Password must be 6 to 17 characters long and must include at least one letter and one number. It can optionally include special characters such as \\w!@#$%^&()-+=',
|
||||||
'Invalid email verification code format. The email verification code must consist of 7 digits or letters.',
|
|
||||||
success: 'Login Successfully! Welcome to KUN Visual Novel',
|
|
||||||
},
|
},
|
||||||
capture: {
|
capture: {
|
||||||
title: 'Answer Question(s)',
|
title: 'Answer Question(s)',
|
||||||
|
|
|
@ -6,21 +6,6 @@ export default {
|
||||||
technique: '技术交流',
|
technique: '技术交流',
|
||||||
about: '关于我们',
|
about: '关于我们',
|
||||||
return: '返回主页',
|
return: '返回主页',
|
||||||
hamburger: {
|
|
||||||
name: '鲲 Galgame',
|
|
||||||
pool: '所有话题',
|
|
||||||
create: '发布话题',
|
|
||||||
technique: '技术交流',
|
|
||||||
about: '关于我们',
|
|
||||||
ranking: '排行榜单',
|
|
||||||
update: '更新日志',
|
|
||||||
bylaw: '执行条例',
|
|
||||||
balance: '收支公示',
|
|
||||||
nonMoe: '不萌记录',
|
|
||||||
thanks: '感谢名单',
|
|
||||||
join: '加入 / 联系',
|
|
||||||
home: '返回主页',
|
|
||||||
},
|
|
||||||
settings: {
|
settings: {
|
||||||
name: '设置面板',
|
name: '设置面板',
|
||||||
mode: '模式切换',
|
mode: '模式切换',
|
||||||
|
@ -33,6 +18,7 @@ export default {
|
||||||
background: '背景设置',
|
background: '背景设置',
|
||||||
preset: '点击使用我们预设的背景',
|
preset: '点击使用我们预设的背景',
|
||||||
custom: '自定义背景',
|
custom: '自定义背景',
|
||||||
|
url: '请在这里粘贴图片的URL',
|
||||||
confirm: '确定',
|
confirm: '确定',
|
||||||
restore: '恢复空白背景',
|
restore: '恢复空白背景',
|
||||||
recover: '恢复所有设置为默认',
|
recover: '恢复所有设置为默认',
|
||||||
|
@ -44,16 +30,14 @@ export default {
|
||||||
},
|
},
|
||||||
back: {
|
back: {
|
||||||
back: '返回',
|
back: '返回',
|
||||||
home: '主页',
|
|
||||||
},
|
},
|
||||||
mainPage: {
|
mainPage: {
|
||||||
header: {
|
header: {
|
||||||
filter: '筛选',
|
filter: '筛选',
|
||||||
category: '分类',
|
search: '搜索话题',
|
||||||
galgame: 'Galgame',
|
|
||||||
technique: '技术交流',
|
|
||||||
others: '其它',
|
|
||||||
all: '全部话题',
|
all: '全部话题',
|
||||||
|
history: '搜索历史',
|
||||||
|
clear: '清除所有历史',
|
||||||
updated: '恢复默认排序',
|
updated: '恢复默认排序',
|
||||||
time: '按照时间排序',
|
time: '按照时间排序',
|
||||||
popularity: '按热度值排序',
|
popularity: '按热度值排序',
|
||||||
|
@ -61,11 +45,6 @@ export default {
|
||||||
likes: '按点赞数排序',
|
likes: '按点赞数排序',
|
||||||
replies: '按回复数排序',
|
replies: '按回复数排序',
|
||||||
comments: '按评论数排序',
|
comments: '按评论数排序',
|
||||||
search: '输入内容以自动搜索',
|
|
||||||
history: '搜索历史',
|
|
||||||
clear: '清除所有历史',
|
|
||||||
emptyHistory: '这只萝莉什么也没搜索过',
|
|
||||||
emptyResult: '什么也没有搜索到。。。',
|
|
||||||
},
|
},
|
||||||
asideActive: {
|
asideActive: {
|
||||||
fold: '折叠左侧区域',
|
fold: '折叠左侧区域',
|
||||||
|
@ -98,7 +77,6 @@ export default {
|
||||||
acgngame: 'ACGNGAME',
|
acgngame: 'ACGNGAME',
|
||||||
shinnku: '失落的小站',
|
shinnku: '失落的小站',
|
||||||
ymgal: '月幕 galgame',
|
ymgal: '月幕 galgame',
|
||||||
kun: '鲲的博客',
|
|
||||||
},
|
},
|
||||||
describe: {
|
describe: {
|
||||||
title: '鲲 Galgame',
|
title: '鲲 Galgame',
|
||||||
|
@ -107,17 +85,19 @@ export default {
|
||||||
kun3: '鲲 Galgame 永远不会有广告',
|
kun3: '鲲 Galgame 永远不会有广告',
|
||||||
kun4: '鲲 Galgame 永远不会收费',
|
kun4: '鲲 Galgame 永远不会收费',
|
||||||
},
|
},
|
||||||
|
contact: '联系我们',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
topic: {
|
topic: {
|
||||||
aside: {
|
aside: {
|
||||||
floor: '按照楼层数排序',
|
top: '返回到顶端',
|
||||||
like: '按照点赞数排序',
|
floorSort: '按楼层排序',
|
||||||
comment: '按照评论数排序',
|
timeSort: '按时间排序',
|
||||||
|
likeSort: '按点赞排序',
|
||||||
|
commentSort: '按评论排序',
|
||||||
|
updatedSort: '按更新排序',
|
||||||
tags: '相同标签下的其它话题',
|
tags: '相同标签下的其它话题',
|
||||||
tagsEmpty: '该标签下暂无其它话题',
|
|
||||||
master: '楼主的其它话题',
|
master: '楼主的其它话题',
|
||||||
masterEmpty: '楼主暂无其它话题',
|
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
status: '话题状态',
|
status: '话题状态',
|
||||||
|
@ -271,7 +251,7 @@ export default {
|
||||||
supportImage: '支持 1007KB 以内的图片',
|
supportImage: '支持 1007KB 以内的图片',
|
||||||
supportFormat: '支持 jpg 和 png 格式',
|
supportFormat: '支持 jpg 和 png 格式',
|
||||||
confirm: '确定更改',
|
confirm: '确定更改',
|
||||||
bio: '更改签名 (107 字之内)',
|
bio: '更改签名',
|
||||||
hint: '输入您的新签名,最大 107 个字符',
|
hint: '输入您的新签名,最大 107 个字符',
|
||||||
count: '字数',
|
count: '字数',
|
||||||
},
|
},
|
||||||
|
@ -308,8 +288,6 @@ export default {
|
||||||
agreement: '用户协议',
|
agreement: '用户协议',
|
||||||
privacy: '隐私政策',
|
privacy: '隐私政策',
|
||||||
redirect: '重定向',
|
redirect: '重定向',
|
||||||
kungalgame403: '403 无权访问',
|
|
||||||
kungalgame404: '404 页面未找到',
|
|
||||||
|
|
||||||
home: '主页',
|
home: '主页',
|
||||||
balance: '收支公示',
|
balance: '收支公示',
|
||||||
|
@ -364,18 +342,6 @@ export default {
|
||||||
donate: '赞助我们',
|
donate: '赞助我们',
|
||||||
home: '返回主页',
|
home: '返回主页',
|
||||||
},
|
},
|
||||||
pool: {
|
|
||||||
load: '点击继续加载话题',
|
|
||||||
complete: '已经。。。一滴也不剩了',
|
|
||||||
view: '按浏览数排序',
|
|
||||||
like: '按点赞数排序',
|
|
||||||
time: '按照时间排序',
|
|
||||||
},
|
|
||||||
technique: {
|
|
||||||
prev: '上一页',
|
|
||||||
next: '下一页',
|
|
||||||
KKKKK: `我们不知道这个页面怎么写了,如果有建议,请联系我们`,
|
|
||||||
},
|
|
||||||
donate: {
|
donate: {
|
||||||
donate: '赞助我们',
|
donate: '赞助我们',
|
||||||
no: '没有任何的萌萌点奖励',
|
no: '没有任何的萌萌点奖励',
|
||||||
|
@ -395,11 +361,6 @@ export default {
|
||||||
success: '登陆成功',
|
success: '登陆成功',
|
||||||
home: '3 秒后你将会进入主页',
|
home: '3 秒后你将会进入主页',
|
||||||
},
|
},
|
||||||
footer: {
|
|
||||||
copyright: '版权所有 © 2023 鲲 Galgame (图片除外)',
|
|
||||||
openSource: 'GitHub 开源',
|
|
||||||
reserved: '保留所有权利 | 版本',
|
|
||||||
},
|
|
||||||
// 非页面组件这里统一用大驼峰
|
// 非页面组件这里统一用大驼峰
|
||||||
ComponentAlert: {
|
ComponentAlert: {
|
||||||
confirm: '确定',
|
confirm: '确定',
|
||||||
|
@ -409,10 +370,12 @@ export default {
|
||||||
edit: {
|
edit: {
|
||||||
publish: '确认发布吗?',
|
publish: '确认发布吗?',
|
||||||
publishSuccess: '发布成功',
|
publishSuccess: '发布成功',
|
||||||
|
publishCancel: '取消发布',
|
||||||
upvoteTopic: '您确定推这个话题吗,这将会消耗您 17 萌萌点',
|
upvoteTopic: '您确定推这个话题吗,这将会消耗您 17 萌萌点',
|
||||||
upvoteReply: '您确定推这个回复吗,这将会消耗您 3 萌萌点',
|
upvoteReply: '您确定推这个回复吗,这将会消耗您 3 萌萌点',
|
||||||
rewrite: '确认 Rewrite 吗?',
|
rewrite: '确认 Rewrite 吗?',
|
||||||
rewriteSuccess: 'Rewrite 成功',
|
rewriteSuccess: 'Rewrite 成功',
|
||||||
|
rewriteCancel: '取消 Rewrite',
|
||||||
closePanel: '确认关闭面板吗?您的更改将不会被保存',
|
closePanel: '确认关闭面板吗?您的更改将不会被保存',
|
||||||
draft: '草稿已经保存成功!',
|
draft: '草稿已经保存成功!',
|
||||||
leave: '确认离开界面吗?您的更改将不会保存',
|
leave: '确认离开界面吗?您的更改将不会保存',
|
||||||
|
@ -421,9 +384,8 @@ export default {
|
||||||
login: {
|
login: {
|
||||||
invalidUsername:
|
invalidUsername:
|
||||||
'非法的用户名,用户名为 1 到 17 位,可以包含:中文、英文、数字、下划线、波浪线',
|
'非法的用户名,用户名为 1 到 17 位,可以包含:中文、英文、数字、下划线、波浪线',
|
||||||
invalidPassword: `非法的密码格式,密码的长度为 6 到 107 位,必须包含至少一个英文字符和一个数字,可以选择性的包含 {'@'}!#$%^&*()-+= 等特殊字符`,
|
invalidPassword:
|
||||||
invalidCode: '非法的邮箱验证码格式,邮箱验证码必须为 7 位数字或字母',
|
'非法的密码格式,密码的长度为 6 到 17 位,必须包含至少一个英文字符和一个数字,可以选择性的包含 \\w!@#$%^&*()-+= 等特殊字符',
|
||||||
success: '登陆成功! 欢迎来到鲲 Galgame! ',
|
|
||||||
},
|
},
|
||||||
capture: {
|
capture: {
|
||||||
title: '请回答下面的问题',
|
title: '请回答下面的问题',
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, watch, ref } from 'vue'
|
import { onMounted, watch, ref } from 'vue'
|
||||||
|
// Import animations
|
||||||
import 'animate.css'
|
import 'animate.css'
|
||||||
import { getCurrentBackground } from '@/hooks/useBackgroundPicture'
|
import { getCurrentBackground } from '@/hooks/useBackgroundPicture'
|
||||||
import KUNGalgameTopBar from '@/components/top-bar/KUNGalgameTopBar.vue'
|
import KUNGalgameTopBar from '@/components/top-bar/KUNGalgameTopBar.vue'
|
||||||
|
|
||||||
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
|
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { getImage } from '@/hooks/useLocalforage'
|
|
||||||
|
|
||||||
const { showKUNGalgameBackground, showKUNGalgameCustomBackground } =
|
const { showKUNGalgameBackground, showKUNGalgameCustomBackground } =
|
||||||
storeToRefs(useKUNGalgameSettingsStore())
|
storeToRefs(useKUNGalgameSettingsStore())
|
||||||
|
@ -14,12 +14,6 @@ const { showKUNGalgameBackground, showKUNGalgameCustomBackground } =
|
||||||
const imageURL = ref('')
|
const imageURL = ref('')
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const backgroundImageBlobData = await getImage('kun-galgame-custom-bg')
|
|
||||||
if (showKUNGalgameBackground.value === 'bg1007' && backgroundImageBlobData) {
|
|
||||||
showKUNGalgameCustomBackground.value = URL.createObjectURL(
|
|
||||||
backgroundImageBlobData
|
|
||||||
)
|
|
||||||
}
|
|
||||||
imageURL.value = await getCurrentBackground()
|
imageURL.value = await getCurrentBackground()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -37,7 +31,7 @@ watch(
|
||||||
<div class="top-bar">
|
<div class="top-bar">
|
||||||
<KUNGalgameTopBar />
|
<KUNGalgameTopBar />
|
||||||
</div>
|
</div>
|
||||||
|
<!-- <RouterView /> -->
|
||||||
<RouterView #default="{ route, Component }">
|
<RouterView #default="{ route, Component }">
|
||||||
<Transition
|
<Transition
|
||||||
:enter-active-class="`animate__animated ${route.meta.transition}`"
|
:enter-active-class="`animate__animated ${route.meta.transition}`"
|
||||||
|
|
16
src/main.ts
16
src/main.ts
|
@ -1,18 +1,22 @@
|
||||||
|
// Vue core
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
// Import vue i18n
|
||||||
import i18n from '@/language/i18n'
|
import i18n from '@/language/i18n'
|
||||||
|
|
||||||
import { setupKUNGalgameRouterGuard } from '@/router/guard'
|
import { setupRouterGuard } from '@/router/guard'
|
||||||
import { setupKUNGalgamePinia } from '@/store/index'
|
import { setupPinia } from '@/store/index'
|
||||||
import { setupKUNGalgameDirectives } from './directives'
|
|
||||||
|
|
||||||
|
// Import css styles, color, theme, etc.
|
||||||
import '@/styles/index.scss'
|
import '@/styles/index.scss'
|
||||||
|
|
||||||
|
// Get vue App instance
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
setupKUNGalgameRouterGuard(router)
|
// Setup router guard
|
||||||
setupKUNGalgamePinia(app)
|
setupRouterGuard(router)
|
||||||
setupKUNGalgameDirectives(app)
|
// Setup pinia
|
||||||
|
setupPinia(app)
|
||||||
|
|
||||||
app.use(router).use(i18n).mount('#app')
|
app.use(router).use(i18n).mount('#app')
|
||||||
|
|
|
@ -10,7 +10,7 @@ const createPageTitle = (router: Router) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupKUNGalgameRouterGuard(router: Router) {
|
export function setupRouterGuard(router: Router) {
|
||||||
createPermission(router)
|
createPermission(router)
|
||||||
createPageTitle(router)
|
createPageTitle(router)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,56 +1,53 @@
|
||||||
// Import rooter
|
// Import rooter
|
||||||
import { Router } from 'vue-router'
|
import { Router } from 'vue-router'
|
||||||
|
// Import public routes that do not require authentication
|
||||||
import { whiteList } from '../router'
|
import { whiteList } from '../router'
|
||||||
|
// Use user store
|
||||||
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
|
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
|
||||||
import { storeToRefs } from 'pinia'
|
// Progress bar
|
||||||
|
|
||||||
import NProgress from 'nprogress'
|
import NProgress from 'nprogress'
|
||||||
import '@/styles/nprogress/nprogress.scss'
|
import '@/styles/nprogress/nprogress.scss'
|
||||||
|
// Get the current user's role based on UID
|
||||||
|
import { getCurrentUserRole } from '@/utils/getCurrentUserRole'
|
||||||
|
|
||||||
// Do not display the NProgress spinner
|
// Do not display the NProgress spinner
|
||||||
NProgress.configure({ showSpinner: false })
|
NProgress.configure({ showSpinner: false })
|
||||||
|
|
||||||
export const createPermission = (router: Router) => {
|
export const createPermission = (router: Router) => {
|
||||||
router.beforeEach(async (to, from) => {
|
router.beforeEach(async (to, from) => {
|
||||||
|
// Start NProgress
|
||||||
NProgress.start()
|
NProgress.start()
|
||||||
|
// Get the current token, access token;
|
||||||
|
// refresh token is stored on the server as HttpOnly
|
||||||
const token = useKUNGalgameUserStore().getToken()
|
const token = useKUNGalgameUserStore().getToken()
|
||||||
const { uid, roles } = storeToRefs(useKUNGalgameUserStore())
|
// Check if the route is in the whitelist
|
||||||
|
|
||||||
const isInWhitelist = whiteList.includes(to.name as string)
|
const isInWhitelist = whiteList.includes(to.name as string)
|
||||||
// Get the required permissions for the target route
|
// Get the required permissions for the target route
|
||||||
const requiredPermissions = to.meta.permission
|
const requiredPermissions = to.meta.permission
|
||||||
? (to.meta.permission as number[])
|
? (to.meta.permission as number[])
|
||||||
: [1, 2, 3, 4]
|
: [1, 2, 3, 4]
|
||||||
|
|
||||||
|
// If there is no token and it's not in the whitelist
|
||||||
|
// , redirect to the login page
|
||||||
if (!token && !isInWhitelist) {
|
if (!token && !isInWhitelist) {
|
||||||
|
// Redirect other pages without access to the login page
|
||||||
NProgress.done()
|
NProgress.done()
|
||||||
return { name: 'Login' }
|
return '/login'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authentication is required
|
// Authentication is required
|
||||||
const currentPageUid = parseInt(to.params.uid as string)
|
if (requiredPermissions) {
|
||||||
|
const currentPageUid = parseInt(to.params.uid as string)
|
||||||
|
const currentUserRole = getCurrentUserRole(currentPageUid)
|
||||||
|
|
||||||
const currentUserRoles = () => {
|
if (!requiredPermissions.includes(currentUserRole)) {
|
||||||
if (currentPageUid === uid.value) {
|
// If it's a user interface, redirect to 'info';
|
||||||
return 4
|
// Otherwise, redirect to '403'
|
||||||
} else {
|
return to.matched[0].path === '/kungalgamer'
|
||||||
return roles.value
|
? `/kungalgamer/${currentPageUid}/info`
|
||||||
|
: '/kungalgame403'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!requiredPermissions.includes(currentUserRoles())) {
|
|
||||||
if (to.matched[0].path === '/kungalgamer') {
|
|
||||||
return { name: 'KUNGalgamerInfo' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!requiredPermissions.includes(currentUserRoles()) &&
|
|
||||||
to.name === '403'
|
|
||||||
) {
|
|
||||||
return { name: '403' }
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Finish NProgress
|
// Finish NProgress
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { type RouteRecordRaw, createWebHistory, createRouter } from 'vue-router'
|
import { type RouteRecordRaw, createWebHistory, createRouter } from 'vue-router'
|
||||||
import { constantRoutes } from './router'
|
import { constantRoutes, whiteList } from './router'
|
||||||
import { asyncRoutes } from './router'
|
import { asyncRoutes } from './router'
|
||||||
|
|
||||||
// Create a Vue Router instance
|
// Create a Vue Router instance
|
||||||
|
@ -16,4 +16,14 @@ const router = createRouter({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// A function to reset the router by removing routes that are not in the whiteList
|
||||||
|
export function resetRouter() {
|
||||||
|
router.getRoutes().forEach((route) => {
|
||||||
|
const { name } = route
|
||||||
|
if (name && !whiteList.includes(name as string)) {
|
||||||
|
router.hasRoute(name) && router.removeRoute(name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import { type RouteRecordRaw } from 'vue-router'
|
import { type RouteRecordRaw } from 'vue-router'
|
||||||
|
// Current user's information
|
||||||
|
import { currentUserInfo } from '@/utils/getCurrentUserInfo'
|
||||||
|
|
||||||
const Layout = () => import('@/layout/KUNGalgameAPP.vue')
|
const Layout = () => import('@/layout/KUNGalgameAPP.vue')
|
||||||
|
|
||||||
|
@ -10,11 +12,11 @@ const kungalgamer: RouteRecordRaw[] = [
|
||||||
path: '/kungalgamer',
|
path: '/kungalgamer',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
// Access defaults to the current user's main page
|
// Access defaults to the current user's main page
|
||||||
// redirect: `/kungalgamer/${uid.value}/info`,
|
redirect: `/kungalgamer/${currentUserInfo.uid}/info`,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: ':uid',
|
path: ':uid',
|
||||||
// redirect: `/kungalgamer/${uid.value}/info`,
|
redirect: `/kungalgamer/${currentUserInfo.uid}/info`,
|
||||||
component: () => import('@/views/kungalgamer/KUNGalgamer.vue'),
|
component: () => import('@/views/kungalgamer/KUNGalgamer.vue'),
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -77,7 +77,7 @@ export const constantRoutes: RouteRecordRaw[] = [
|
||||||
path: '/:path(.*)*',
|
path: '/:path(.*)*',
|
||||||
component: () => import('@/views/404/404.vue'),
|
component: () => import('@/views/404/404.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: 'kungalgame404',
|
title: '404',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -87,11 +87,14 @@ export const constantRoutes: RouteRecordRaw[] = [
|
||||||
path: '/kungalgame403',
|
path: '/kungalgame403',
|
||||||
component: () => import('@/views/403/403.vue'),
|
component: () => import('@/views/403/403.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: 'kungalgame403',
|
title: '403',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const isArray = (val: any): val is object =>
|
||||||
|
toString.call(val) === '[object Array]'
|
||||||
|
|
||||||
// 获取动态路由表
|
// 获取动态路由表
|
||||||
const getAsyncRoute = (): RouteRecordRaw[] => {
|
const getAsyncRoute = (): RouteRecordRaw[] => {
|
||||||
const modules = import.meta.glob('./modules/*.ts', {
|
const modules = import.meta.glob('./modules/*.ts', {
|
||||||
|
@ -100,7 +103,7 @@ const getAsyncRoute = (): RouteRecordRaw[] => {
|
||||||
})
|
})
|
||||||
const asyncRoute: RouteRecordRaw[] = []
|
const asyncRoute: RouteRecordRaw[] = []
|
||||||
Object.values(modules).forEach((value) => {
|
Object.values(modules).forEach((value) => {
|
||||||
const moduleList = Array.isArray(value)
|
const moduleList = isArray(value)
|
||||||
? [...(value as RouteRecordRaw[])]
|
? [...(value as RouteRecordRaw[])]
|
||||||
: [value as RouteRecordRaw]
|
: [value as RouteRecordRaw]
|
||||||
asyncRoute.push(...moduleList)
|
asyncRoute.push(...moduleList)
|
||||||
|
|
|
@ -6,32 +6,62 @@ import { createPinia } from 'pinia'
|
||||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||||
import type { App } from 'vue'
|
import type { App } from 'vue'
|
||||||
|
|
||||||
|
// Import store for the income and expense public disclosure page
|
||||||
|
import { useKUNGalgameBalanceStore } from './modules/balance'
|
||||||
|
|
||||||
|
// Import store for the editing interface
|
||||||
import { useKUNGalgameEditStore } from './modules/edit'
|
import { useKUNGalgameEditStore } from './modules/edit'
|
||||||
import { usePersistKUNGalgameHomeStore } from './modules/home'
|
|
||||||
|
// Import home store
|
||||||
|
import { useKUNGalgameHomeStore } from './modules/home'
|
||||||
|
|
||||||
|
// Import user store
|
||||||
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
|
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
|
||||||
|
|
||||||
|
// Import message store
|
||||||
|
import { useKUNGalgameMessageStore } from './modules/message'
|
||||||
|
|
||||||
|
// Import non-moe records store
|
||||||
|
import { useKUNGalgameNonMoeStore } from './modules/nonMoe'
|
||||||
|
|
||||||
|
// Import ranking store
|
||||||
|
import { useKUNGalgameRankingStore } from './modules/ranking'
|
||||||
|
|
||||||
|
// Import website settings panel store
|
||||||
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
|
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
|
||||||
import { usePersistKUNGalgameTopicStore } from '@/store/modules/topic/topic'
|
|
||||||
import { usePersistKUNGalgameReplyStore } from '@/store/modules/topic/reply'
|
// Import store for the topic detail page
|
||||||
|
import { useKUNGalgameTopicStore } from './modules/topic'
|
||||||
|
|
||||||
const store = createPinia()
|
const store = createPinia()
|
||||||
|
|
||||||
export function setupKUNGalgamePinia(app: App<Element>) {
|
// Function to set up Pinia, to be called in main.ts
|
||||||
|
export function setupPinia(app: App<Element>) {
|
||||||
store.use(piniaPluginPersistedstate)
|
store.use(piniaPluginPersistedstate)
|
||||||
app.use(store)
|
app.use(store)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset all stores, used for logging out
|
||||||
export function kungalgameStoreReset() {
|
export function kungalgameStoreReset() {
|
||||||
|
const balanceStore = useKUNGalgameBalanceStore()
|
||||||
const editStore = useKUNGalgameEditStore()
|
const editStore = useKUNGalgameEditStore()
|
||||||
const homeStore = usePersistKUNGalgameHomeStore()
|
const homeStore = useKUNGalgameHomeStore()
|
||||||
const userStore = useKUNGalgameUserStore()
|
const userStore = useKUNGalgameUserStore()
|
||||||
|
const messageStore = useKUNGalgameMessageStore()
|
||||||
|
const nonMoeStore = useKUNGalgameNonMoeStore()
|
||||||
|
const rankingStore = useKUNGalgameRankingStore()
|
||||||
const settingsStore = useKUNGalgameSettingsStore()
|
const settingsStore = useKUNGalgameSettingsStore()
|
||||||
const topicStore = usePersistKUNGalgameTopicStore()
|
const topicStore = useKUNGalgameTopicStore()
|
||||||
const replyStore = usePersistKUNGalgameReplyStore()
|
|
||||||
|
|
||||||
|
balanceStore.$reset()
|
||||||
editStore.$reset()
|
editStore.$reset()
|
||||||
homeStore.$reset()
|
homeStore.$reset()
|
||||||
userStore.$reset()
|
userStore.$reset()
|
||||||
|
messageStore.$reset()
|
||||||
|
nonMoeStore.$reset()
|
||||||
|
rankingStore.$reset()
|
||||||
settingsStore.$reset()
|
settingsStore.$reset()
|
||||||
topicStore.$reset()
|
topicStore.$reset()
|
||||||
replyStore.$reset()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { store }
|
||||||
|
|
|
@ -15,8 +15,8 @@ interface BalanceStore {
|
||||||
expenditure: BalanceExpenditureRequestData
|
expenditure: BalanceExpenditureRequestData
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useTempBalanceStore = defineStore({
|
export const useKUNGalgameBalanceStore = defineStore({
|
||||||
id: 'tempBalance',
|
id: 'KUNGalgameBalance',
|
||||||
persist: false,
|
persist: false,
|
||||||
state: (): BalanceStore => ({
|
state: (): BalanceStore => ({
|
||||||
income: {
|
income: {
|
|
@ -1,21 +1,24 @@
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { postNewTopicApi, getTopTagsApi } from '@/api'
|
import { postNewTopicApi, updateNewTopicApi, getTopTagsApi } from '@/api'
|
||||||
import {
|
import {
|
||||||
EditCreateTopicRequestData,
|
EditCreateTopicRequestData,
|
||||||
EditCreateTopicResponseData,
|
EditCreateTopicResponseData,
|
||||||
|
EditUpdateTopicRequestData,
|
||||||
|
EditUpdateTopicResponseData,
|
||||||
EditGetHotTagsRequestData,
|
EditGetHotTagsRequestData,
|
||||||
EditGetHotTagsResponseData,
|
EditGetHotTagsResponseData,
|
||||||
} from '@/api'
|
} from '@/api'
|
||||||
|
import { EditStore } from '../types/edit'
|
||||||
import { checkTopicPublish } from '../utils/checkTopicPublish'
|
import { checkTopicPublish } from '../utils/checkTopicPublish'
|
||||||
import type { EditStorePersist } from '../types/edit'
|
|
||||||
|
|
||||||
export const useKUNGalgameEditStore = defineStore({
|
export const useKUNGalgameEditStore = defineStore({
|
||||||
id: 'KUNGalgameEdit',
|
id: 'KUNGalgameEdit',
|
||||||
persist: true,
|
persist: true,
|
||||||
state: (): EditStorePersist => ({
|
state: (): EditStore => ({
|
||||||
editorHeight: 300,
|
editorHeight: 300,
|
||||||
textCount: 0,
|
textCount: 0,
|
||||||
|
mode: '',
|
||||||
|
theme: 'snow',
|
||||||
|
|
||||||
title: '',
|
title: '',
|
||||||
content: '',
|
content: '',
|
||||||
|
@ -23,6 +26,16 @@ export const useKUNGalgameEditStore = defineStore({
|
||||||
category: [],
|
category: [],
|
||||||
isShowHotKeywords: true,
|
isShowHotKeywords: true,
|
||||||
isSaveTopic: false,
|
isSaveTopic: false,
|
||||||
|
|
||||||
|
topicRewrite: {
|
||||||
|
tid: 0,
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
tags: [],
|
||||||
|
category: [],
|
||||||
|
|
||||||
|
isTopicRewriting: false,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
getters: {},
|
getters: {},
|
||||||
actions: {
|
actions: {
|
||||||
|
@ -44,6 +57,24 @@ export const useKUNGalgameEditStore = defineStore({
|
||||||
return await postNewTopicApi(requestData)
|
return await postNewTopicApi(requestData)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Update a topic
|
||||||
|
async rewriteTopic(): Promise<EditUpdateTopicResponseData | undefined> {
|
||||||
|
const requestData: EditUpdateTopicRequestData = {
|
||||||
|
tid: this.topicRewrite.tid,
|
||||||
|
title: this.topicRewrite.title,
|
||||||
|
content: this.topicRewrite.content,
|
||||||
|
tags: this.topicRewrite.tags,
|
||||||
|
category: this.topicRewrite.category,
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the topic data is invalid, return directly
|
||||||
|
if (!checkTopicPublish(this.textCount, requestData)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return await updateNewTopicApi(requestData)
|
||||||
|
},
|
||||||
|
|
||||||
// Get popular tags
|
// Get popular tags
|
||||||
async getHotTags(limit: number): Promise<EditGetHotTagsResponseData> {
|
async getHotTags(limit: number): Promise<EditGetHotTagsResponseData> {
|
||||||
const requestData: EditGetHotTagsRequestData = { limit }
|
const requestData: EditGetHotTagsRequestData = { limit }
|
||||||
|
@ -53,7 +84,6 @@ export const useKUNGalgameEditStore = defineStore({
|
||||||
// Reset topic draft data for publishing
|
// Reset topic draft data for publishing
|
||||||
resetTopicData() {
|
resetTopicData() {
|
||||||
this.textCount = 0
|
this.textCount = 0
|
||||||
|
|
||||||
this.title = ''
|
this.title = ''
|
||||||
this.content = ''
|
this.content = ''
|
||||||
this.tags = []
|
this.tags = []
|
||||||
|
@ -61,5 +91,16 @@ export const useKUNGalgameEditStore = defineStore({
|
||||||
|
|
||||||
this.isSaveTopic = false
|
this.isSaveTopic = false
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Reset data for re-editing a topic
|
||||||
|
resetRewriteTopicData() {
|
||||||
|
this.textCount = 0
|
||||||
|
this.topicRewrite.title = ''
|
||||||
|
this.topicRewrite.content = ''
|
||||||
|
this.topicRewrite.tags = []
|
||||||
|
this.topicRewrite.category = []
|
||||||
|
|
||||||
|
this.topicRewrite.isTopicRewriting = false
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,12 +1,61 @@
|
||||||
|
/* Home Page Store */
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import type { HomeStorePersist } from '../types/home'
|
// API
|
||||||
|
import { getHomeTopicApi } from '@/api/index'
|
||||||
|
// Data interface types
|
||||||
|
import { HomeTopicRequestData, HomeTopicResponseData } from '@/api/index'
|
||||||
|
|
||||||
export const usePersistKUNGalgameHomeStore = defineStore({
|
// Home store type
|
||||||
|
import { HomeStore } from '../types/home'
|
||||||
|
|
||||||
|
export const useKUNGalgameHomeStore = defineStore({
|
||||||
id: 'KUNGalgameHome',
|
id: 'KUNGalgameHome',
|
||||||
persist: true,
|
persist: true,
|
||||||
state: (): HomeStorePersist => ({
|
state: (): HomeStore => ({
|
||||||
|
// Search box store
|
||||||
|
/**
|
||||||
|
* @param {String} keywords - Search keywords, default to all if not provided
|
||||||
|
* @param {Array} category - Topic categories, currently there are three: Galgame, Technique, Others
|
||||||
|
* @param {Number} page - Page number for pagination
|
||||||
|
* @param {Number} limit - Number of data per page
|
||||||
|
* @param {String} sortField - Field to sort by
|
||||||
|
* @param {String} sortOrder - Sorting order, can be 'asc' or 'desc'
|
||||||
|
* @returns {HomeTopicResponseData} topicData
|
||||||
|
*/
|
||||||
|
keywords: '',
|
||||||
|
category: '',
|
||||||
|
page: 1,
|
||||||
|
limit: 17,
|
||||||
|
sortField: 'updated',
|
||||||
|
sortOrder: 'desc',
|
||||||
|
isLoading: true,
|
||||||
|
|
||||||
|
// Other store
|
||||||
|
// Whether to activate the left interaction panel on the main page
|
||||||
isActiveMainPageAside: true,
|
isActiveMainPageAside: true,
|
||||||
|
|
||||||
|
// Search history storage
|
||||||
searchHistory: [],
|
searchHistory: [],
|
||||||
}),
|
}),
|
||||||
|
getters: {},
|
||||||
|
actions: {
|
||||||
|
// Get home topics
|
||||||
|
async getHomeTopic(): Promise<HomeTopicResponseData> {
|
||||||
|
// The values here are used for initialization
|
||||||
|
const requestData: HomeTopicRequestData = {
|
||||||
|
keywords: this.keywords,
|
||||||
|
category: this.category,
|
||||||
|
page: this.page,
|
||||||
|
limit: this.limit,
|
||||||
|
sortField: this.sortField,
|
||||||
|
sortOrder: this.sortOrder,
|
||||||
|
}
|
||||||
|
return await getHomeTopicApi(requestData)
|
||||||
|
},
|
||||||
|
// Reset page number and loading status for sorting to take effect
|
||||||
|
resetPageStatus() {
|
||||||
|
this.page = 1
|
||||||
|
this.isLoading = true
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
/*
|
/*
|
||||||
|
|
||||||
User Information Storage
|
User Information Storage
|
||||||
*/
|
*/
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
|
@ -51,8 +52,10 @@ import {
|
||||||
updateUserPasswordByEmailApi,
|
updateUserPasswordByEmailApi,
|
||||||
} from '@/api'
|
} from '@/api'
|
||||||
|
|
||||||
import type { KUNGalgamerStore } from '../types/kungalgamer'
|
// KUNGalgamer store type
|
||||||
|
import { KUNGalgamerStore } from '../types/kungalgamer'
|
||||||
|
|
||||||
|
// Here, pinia-plugin-persistedstate is used, so storage is automatic
|
||||||
export const useKUNGalgameUserStore = defineStore({
|
export const useKUNGalgameUserStore = defineStore({
|
||||||
id: 'KUNGalgameUser',
|
id: 'KUNGalgameUser',
|
||||||
persist: true,
|
persist: true,
|
||||||
|
@ -107,6 +110,10 @@ export const useKUNGalgameUserStore = defineStore({
|
||||||
res.data.roles
|
res.data.roles
|
||||||
)
|
)
|
||||||
this.setToken(res.data.token)
|
this.setToken(res.data.token)
|
||||||
|
} else if (res.code === 500) {
|
||||||
|
console.log(res.message)
|
||||||
|
} else {
|
||||||
|
throw new Error('500 Server ERROR')
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,8 +3,8 @@ import { defineStore } from 'pinia'
|
||||||
// Type of message store
|
// Type of message store
|
||||||
import { MessageStore } from '../types/message'
|
import { MessageStore } from '../types/message'
|
||||||
|
|
||||||
export const useTempMessageStore = defineStore({
|
export const useKUNGalgameMessageStore = defineStore({
|
||||||
id: 'tempMessage',
|
id: 'KUNGalgameMessage',
|
||||||
// No need to persist any message components
|
// No need to persist any message components
|
||||||
persist: false,
|
persist: false,
|
||||||
state: (): MessageStore => ({
|
state: (): MessageStore => ({
|
|
@ -4,8 +4,8 @@ import type { NonMoeLogRequestData, NonMoeGetLogsResponseData } from '@/api'
|
||||||
|
|
||||||
import { getNonMoeLogsApi } from '@/api'
|
import { getNonMoeLogsApi } from '@/api'
|
||||||
|
|
||||||
export const useTempNonMoeStore = defineStore({
|
export const useKUNGalgameNonMoeStore = defineStore({
|
||||||
id: 'tempNonMoe',
|
id: 'KUNGalgameNonMoe',
|
||||||
persist: false,
|
persist: false,
|
||||||
state: (): NonMoeLogRequestData => ({
|
state: (): NonMoeLogRequestData => ({
|
||||||
page: 1,
|
page: 1,
|
|
@ -14,8 +14,8 @@ interface RankingStore {
|
||||||
user: RankingGetUserRequestData
|
user: RankingGetUserRequestData
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useTempRankingStore = defineStore({
|
export const useKUNGalgameRankingStore = defineStore({
|
||||||
id: 'tempRanking',
|
id: 'KUNGalgameRanking',
|
||||||
persist: false,
|
persist: false,
|
||||||
state: (): RankingStore => ({
|
state: (): RankingStore => ({
|
||||||
topic: {
|
topic: {
|
|
@ -32,7 +32,7 @@ export const useKUNGalgameSettingsStore = defineStore({
|
||||||
actions: {
|
actions: {
|
||||||
// Set the theme, there are only two modes
|
// Set the theme, there are only two modes
|
||||||
// , light and dark, with light represented as ''
|
// , light and dark, with light represented as ''
|
||||||
setKUNGalgameTheme(theme: '' | 'dark') {
|
setKUNGalgameTheme(theme: string) {
|
||||||
this.showKUNGalgameMode = theme
|
this.showKUNGalgameMode = theme
|
||||||
document.documentElement.className = theme
|
document.documentElement.className = theme
|
||||||
},
|
},
|
||||||
|
|
339
src/store/modules/topic.ts
Normal file
339
src/store/modules/topic.ts
Normal file
|
@ -0,0 +1,339 @@
|
||||||
|
// Store for topic details
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
// Topics
|
||||||
|
import {
|
||||||
|
getTopicByTidApi,
|
||||||
|
getRelatedTopicsByTagsApi,
|
||||||
|
getPopularTopicsByUserUidApi,
|
||||||
|
updateTopicUpvoteApi,
|
||||||
|
updateTopicLikeApi,
|
||||||
|
updateTopicDislikeApi,
|
||||||
|
} from '@/api'
|
||||||
|
|
||||||
|
import type {
|
||||||
|
TopicDetailResponseData,
|
||||||
|
TopicAsideOtherTagRequestData,
|
||||||
|
TopicAsideMasterRequestData,
|
||||||
|
TopicAsideResponseData,
|
||||||
|
TopicUpvoteTopicRequestData,
|
||||||
|
TopicUpvoteTopicResponseData,
|
||||||
|
TopicLikeTopicRequestData,
|
||||||
|
TopicLikeTopicResponseData,
|
||||||
|
TopicDislikeTopicRequestData,
|
||||||
|
TopicDislikeTopicResponseData,
|
||||||
|
} from '@/api'
|
||||||
|
|
||||||
|
// Replies
|
||||||
|
import {
|
||||||
|
getRepliesByPidApi,
|
||||||
|
postReplyByPidApi,
|
||||||
|
updateReplyUpvoteApi,
|
||||||
|
updateReplyLikeApi,
|
||||||
|
updateReplyDislikeApi,
|
||||||
|
updateReplyApi,
|
||||||
|
} from '@/api'
|
||||||
|
|
||||||
|
import type {
|
||||||
|
TopicReplyRequestData,
|
||||||
|
TopicReplyResponseData,
|
||||||
|
TopicCreateReplyRequestData,
|
||||||
|
TopicCreateReplyResponseData,
|
||||||
|
TopicUpvoteReplyRequestData,
|
||||||
|
TopicUpvoteReplyResponseData,
|
||||||
|
TopicLikeReplyRequestData,
|
||||||
|
TopicLikeReplyResponseData,
|
||||||
|
TopicDislikeReplyRequestData,
|
||||||
|
TopicDislikeReplyResponseData,
|
||||||
|
TopicUpdateReplyRequestData,
|
||||||
|
TopicUpdateReplyResponseData,
|
||||||
|
} from '@/api'
|
||||||
|
|
||||||
|
// Comments
|
||||||
|
import {
|
||||||
|
getCommentsByReplyRidApi,
|
||||||
|
updateCommentLikeApi,
|
||||||
|
updateCommentDislikeApi,
|
||||||
|
postCommentByPidAndRidApi,
|
||||||
|
} from '@/api'
|
||||||
|
|
||||||
|
import type {
|
||||||
|
TopicCommentResponseData,
|
||||||
|
TopicLikeCommentRequestData,
|
||||||
|
TopicLikeCommentResponseData,
|
||||||
|
TopicDislikeCommentRequestData,
|
||||||
|
TopicDislikeCommentResponseData,
|
||||||
|
TopicCreateCommentRequestData,
|
||||||
|
TopicCreateCommentResponseData,
|
||||||
|
} from '@/api'
|
||||||
|
|
||||||
|
// Import the type of topic store
|
||||||
|
import { TopicStore } from '../types/topic'
|
||||||
|
|
||||||
|
export const useKUNGalgameTopicStore = defineStore({
|
||||||
|
id: 'KUNGalgameTopic',
|
||||||
|
persist: true,
|
||||||
|
state: (): TopicStore => ({
|
||||||
|
isEdit: false,
|
||||||
|
isShowAdvance: false,
|
||||||
|
isActiveAside: false,
|
||||||
|
isScrollToTop: false,
|
||||||
|
isLoading: true,
|
||||||
|
// Reply ID starts from 0, -1 is just for monitoring data changes
|
||||||
|
// , used for watchEffect
|
||||||
|
scrollToReplyId: -1,
|
||||||
|
|
||||||
|
replyPanelWidth: 90,
|
||||||
|
replyDraft: {
|
||||||
|
editorHeight: 200,
|
||||||
|
textCount: 0,
|
||||||
|
mode: 'minimal',
|
||||||
|
theme: 'snow',
|
||||||
|
isShowHotKeywords: true,
|
||||||
|
|
||||||
|
tid: 0,
|
||||||
|
toUserName: '',
|
||||||
|
to_uid: 0,
|
||||||
|
content: '',
|
||||||
|
tags: [],
|
||||||
|
to_floor: 0,
|
||||||
|
|
||||||
|
isSaveReply: false,
|
||||||
|
},
|
||||||
|
replyRequest: {
|
||||||
|
page: 1,
|
||||||
|
limit: 3,
|
||||||
|
sortField: 'floor',
|
||||||
|
sortOrder: 'asc',
|
||||||
|
},
|
||||||
|
replyRewrite: {
|
||||||
|
tid: 0,
|
||||||
|
rid: 0,
|
||||||
|
content: '',
|
||||||
|
tags: [],
|
||||||
|
|
||||||
|
isReplyRewriting: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
// Other topics under the same tag on the left
|
||||||
|
async getRelatedTopicsByTags(
|
||||||
|
request: TopicAsideOtherTagRequestData
|
||||||
|
): Promise<TopicAsideResponseData> {
|
||||||
|
return await getRelatedTopicsByTagsApi(request)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Other topics by the master
|
||||||
|
async getPopularTopicsByUserUid(
|
||||||
|
request: TopicAsideMasterRequestData
|
||||||
|
): Promise<TopicAsideResponseData> {
|
||||||
|
return await getPopularTopicsByUserUidApi(request)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get a single topic
|
||||||
|
async getTopicByTid(tid: number): Promise<TopicDetailResponseData> {
|
||||||
|
return await getTopicByTidApi(tid)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Upvote a topic
|
||||||
|
async updateTopicUpvote(
|
||||||
|
tid: number,
|
||||||
|
toUid: number
|
||||||
|
): Promise<TopicUpvoteTopicResponseData> {
|
||||||
|
const requestData: TopicUpvoteTopicRequestData = {
|
||||||
|
tid: tid,
|
||||||
|
to_uid: toUid,
|
||||||
|
}
|
||||||
|
return await updateTopicUpvoteApi(requestData)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Like a topic
|
||||||
|
async updateTopicLike(
|
||||||
|
tid: number,
|
||||||
|
toUid: number,
|
||||||
|
isPush: boolean
|
||||||
|
): Promise<TopicLikeTopicResponseData> {
|
||||||
|
const requestData: TopicLikeTopicRequestData = {
|
||||||
|
tid: tid,
|
||||||
|
to_uid: toUid,
|
||||||
|
isPush: isPush,
|
||||||
|
}
|
||||||
|
return await updateTopicLikeApi(requestData)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Dislike a topic
|
||||||
|
async updateTopicDislike(
|
||||||
|
tid: number,
|
||||||
|
toUid: number,
|
||||||
|
isPush: boolean
|
||||||
|
): Promise<TopicDislikeTopicResponseData> {
|
||||||
|
const requestData: TopicDislikeTopicRequestData = {
|
||||||
|
tid: tid,
|
||||||
|
to_uid: toUid,
|
||||||
|
isPush: isPush,
|
||||||
|
}
|
||||||
|
return await updateTopicDislikeApi(requestData)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get replies
|
||||||
|
async getReplies(tid: number): Promise<TopicReplyResponseData> {
|
||||||
|
// The default values here are used for initialization
|
||||||
|
const requestData: TopicReplyRequestData = {
|
||||||
|
tid: tid,
|
||||||
|
page: this.replyRequest.page,
|
||||||
|
limit: this.replyRequest.limit,
|
||||||
|
sortField: this.replyRequest.sortField || 'floor',
|
||||||
|
sortOrder: this.replyRequest.sortOrder || 'desc',
|
||||||
|
}
|
||||||
|
return await getRepliesByPidApi(requestData)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Create a new reply
|
||||||
|
async postNewReply(): Promise<TopicCreateReplyResponseData> {
|
||||||
|
// The values here are used to initialize the reply
|
||||||
|
const requestData: TopicCreateReplyRequestData = {
|
||||||
|
tid: this.replyDraft.tid,
|
||||||
|
to_uid: this.replyDraft.to_uid,
|
||||||
|
to_floor: this.replyDraft.to_floor,
|
||||||
|
tags: this.replyDraft.tags,
|
||||||
|
content: this.replyDraft.content,
|
||||||
|
}
|
||||||
|
return await postReplyByPidApi(requestData)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update a reply
|
||||||
|
async updateReply(): Promise<TopicUpdateReplyResponseData> {
|
||||||
|
const requestData: TopicUpdateReplyRequestData = {
|
||||||
|
tid: this.replyRewrite.tid,
|
||||||
|
rid: this.replyRewrite.rid,
|
||||||
|
content: this.replyRewrite.content,
|
||||||
|
tags: this.replyRewrite.tags,
|
||||||
|
}
|
||||||
|
return await updateReplyApi(requestData)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Upvote a reply
|
||||||
|
async updateReplyUpvote(
|
||||||
|
tid: number,
|
||||||
|
toUid: number,
|
||||||
|
rid: number
|
||||||
|
): Promise<TopicUpvoteReplyResponseData> {
|
||||||
|
const requestData: TopicUpvoteReplyRequestData = {
|
||||||
|
tid: tid,
|
||||||
|
to_uid: toUid,
|
||||||
|
rid: rid,
|
||||||
|
}
|
||||||
|
return await updateReplyUpvoteApi(requestData)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Like a reply
|
||||||
|
async updateReplyLike(
|
||||||
|
tid: number,
|
||||||
|
toUid: number,
|
||||||
|
rid: number,
|
||||||
|
isPush: boolean
|
||||||
|
): Promise<TopicLikeReplyResponseData> {
|
||||||
|
const requestData: TopicLikeReplyRequestData = {
|
||||||
|
tid: tid,
|
||||||
|
to_uid: toUid,
|
||||||
|
rid: rid,
|
||||||
|
isPush: isPush,
|
||||||
|
}
|
||||||
|
return await updateReplyLikeApi(requestData)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Dislike a reply
|
||||||
|
async updateReplyDislike(
|
||||||
|
tid: number,
|
||||||
|
toUid: number,
|
||||||
|
rid: number,
|
||||||
|
isPush: boolean
|
||||||
|
): Promise<TopicDislikeReplyResponseData> {
|
||||||
|
const requestData: TopicDislikeReplyRequestData = {
|
||||||
|
tid: tid,
|
||||||
|
to_uid: toUid,
|
||||||
|
rid: rid,
|
||||||
|
isPush: isPush,
|
||||||
|
}
|
||||||
|
return await updateReplyDislikeApi(requestData)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get comments
|
||||||
|
async getComments(
|
||||||
|
tid: number,
|
||||||
|
rid: number
|
||||||
|
): Promise<TopicCommentResponseData> {
|
||||||
|
return await getCommentsByReplyRidApi(tid, rid)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Like a comment
|
||||||
|
async updateCommentLike(
|
||||||
|
tid: number,
|
||||||
|
cid: number,
|
||||||
|
toUid: number
|
||||||
|
): Promise<TopicLikeCommentResponseData> {
|
||||||
|
const requestData: TopicLikeCommentRequestData = {
|
||||||
|
tid: tid,
|
||||||
|
cid: cid,
|
||||||
|
to_uid: toUid,
|
||||||
|
}
|
||||||
|
return await updateCommentLikeApi(requestData)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Dislike a comment
|
||||||
|
async updateCommentDislike(
|
||||||
|
tid: number,
|
||||||
|
cid: number,
|
||||||
|
toUid: number
|
||||||
|
): Promise<TopicDislikeCommentResponseData> {
|
||||||
|
const requestData: TopicDislikeCommentRequestData = {
|
||||||
|
tid: tid,
|
||||||
|
cid: cid,
|
||||||
|
to_uid: toUid,
|
||||||
|
}
|
||||||
|
return await updateCommentDislikeApi(requestData)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Create a new comment
|
||||||
|
async postNewComment(
|
||||||
|
tid: number,
|
||||||
|
rid: number,
|
||||||
|
toUid: number,
|
||||||
|
content: string
|
||||||
|
): Promise<TopicCreateCommentResponseData> {
|
||||||
|
const requestData: TopicCreateCommentRequestData = {
|
||||||
|
tid: tid,
|
||||||
|
rid: rid,
|
||||||
|
to_uid: toUid,
|
||||||
|
content: content,
|
||||||
|
}
|
||||||
|
return await postCommentByPidAndRidApi(requestData)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Reset reply draft to its original value, used for the reply publish button
|
||||||
|
resetReplyDraft() {
|
||||||
|
this.replyDraft.textCount = 0
|
||||||
|
this.replyDraft.tid = 0
|
||||||
|
this.replyDraft.toUserName = ''
|
||||||
|
this.replyDraft.to_uid = 0
|
||||||
|
this.replyDraft.content = ''
|
||||||
|
this.replyDraft.tags = []
|
||||||
|
|
||||||
|
this.replyDraft.isSaveReply = false
|
||||||
|
},
|
||||||
|
// Reset page number and loading status for reply sorting to take effect
|
||||||
|
resetPageStatus() {
|
||||||
|
this.replyRequest.page = 1
|
||||||
|
this.isLoading = true
|
||||||
|
},
|
||||||
|
// Reset data for re-editing a reply
|
||||||
|
resetRewriteTopicData() {
|
||||||
|
this.replyDraft.textCount = 0
|
||||||
|
this.replyRewrite.tid = 0
|
||||||
|
this.replyRewrite.rid = 0
|
||||||
|
this.replyRewrite.content = ''
|
||||||
|
this.replyRewrite.tags = []
|
||||||
|
|
||||||
|
this.replyRewrite.isReplyRewriting = false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
|
@ -1,68 +0,0 @@
|
||||||
import { defineStore } from 'pinia'
|
|
||||||
|
|
||||||
import { postReplyByTidApi } from '@/api'
|
|
||||||
|
|
||||||
import type {
|
|
||||||
TopicCreateReplyRequestData,
|
|
||||||
TopicCreateReplyResponseData,
|
|
||||||
} from '@/api'
|
|
||||||
|
|
||||||
import { checkReplyPublish } from '@/store/utils/checkReplyPublish'
|
|
||||||
import type { ReplyStorePersist } from '@/store/types/topic/reply'
|
|
||||||
|
|
||||||
export const usePersistKUNGalgameReplyStore = defineStore({
|
|
||||||
id: 'KUNGalgameReply',
|
|
||||||
persist: true,
|
|
||||||
state: (): ReplyStorePersist => ({
|
|
||||||
replyPanelWidth: 90,
|
|
||||||
|
|
||||||
isSaveReply: false,
|
|
||||||
isShowHotKeywords: true,
|
|
||||||
|
|
||||||
editorHeight: 200,
|
|
||||||
textCount: 0,
|
|
||||||
|
|
||||||
replyDraft: {
|
|
||||||
tid: 0,
|
|
||||||
toUserName: '',
|
|
||||||
toUid: 0,
|
|
||||||
content: '',
|
|
||||||
tags: [],
|
|
||||||
toFloor: 0,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
actions: {
|
|
||||||
// Create a new reply
|
|
||||||
async postNewReply(): Promise<TopicCreateReplyResponseData | undefined> {
|
|
||||||
// The values here are used to initialize the reply
|
|
||||||
const requestData: TopicCreateReplyRequestData = {
|
|
||||||
tid: this.replyDraft.tid,
|
|
||||||
to_uid: this.replyDraft.toUid,
|
|
||||||
to_floor: this.replyDraft.toFloor,
|
|
||||||
tags: this.replyDraft.tags,
|
|
||||||
content: this.replyDraft.content,
|
|
||||||
time: Date.now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!checkReplyPublish(requestData.tags, requestData.content)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return await postReplyByTidApi(requestData)
|
|
||||||
},
|
|
||||||
|
|
||||||
// Reset reply draft to its original value, used for the reply publish button
|
|
||||||
resetReplyDraft() {
|
|
||||||
this.textCount = 0
|
|
||||||
|
|
||||||
this.replyDraft.tid = 0
|
|
||||||
this.replyDraft.toUserName = ''
|
|
||||||
this.replyDraft.toUid = 0
|
|
||||||
this.replyDraft.content = ''
|
|
||||||
this.replyDraft.tags = []
|
|
||||||
this.replyDraft.toFloor = 0
|
|
||||||
|
|
||||||
this.isSaveReply = false
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue