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)
|
||||
|
||||
VITE_API_UPLOADS_URL = `http://127.0.0.1:10007/uploads`
|
||||
VITE_API_BASE_URL = `http://127.0.0.1:10007/api`
|
||||
## Development Environment Address Prefix (usually '/' or './') TODO:
|
||||
VITE_API_BASE_URL = `http://127.0.0.1:10007`
|
||||
|
||||
## Router Mode, hash or 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",
|
||||
"arpa",
|
||||
"Asuka",
|
||||
"atrule",
|
||||
"axios",
|
||||
"azkhx",
|
||||
"bangumi",
|
||||
|
@ -14,8 +13,6 @@
|
|||
"Bishoujo",
|
||||
"Chuudoku",
|
||||
"Codepen",
|
||||
"commonmark",
|
||||
"cooldown",
|
||||
"cout",
|
||||
"dompurify",
|
||||
"fontawesome",
|
||||
|
@ -47,7 +44,6 @@
|
|||
"Mangekyou",
|
||||
"Maniwa",
|
||||
"Meister",
|
||||
"Milkdown",
|
||||
"Minato",
|
||||
"mingcute",
|
||||
"Mirai",
|
||||
|
@ -60,7 +56,6 @@
|
|||
"nawa",
|
||||
"NEKOPARA",
|
||||
"non-moe",
|
||||
"nord",
|
||||
"nprogress",
|
||||
"okaidia",
|
||||
"Otome",
|
||||
|
@ -68,7 +63,6 @@
|
|||
"persistedstate",
|
||||
"Pinia",
|
||||
"prismjs",
|
||||
"prosemirror",
|
||||
"rdquo",
|
||||
"Roka",
|
||||
"Sahou",
|
||||
|
@ -78,7 +72,6 @@
|
|||
"Senren",
|
||||
"Sensei",
|
||||
"Shabondama",
|
||||
"Shiki",
|
||||
"shinnku",
|
||||
"Shugaten",
|
||||
"signin",
|
||||
|
@ -107,7 +100,6 @@
|
|||
"Vite",
|
||||
"VNDB",
|
||||
"vueup",
|
||||
"waifu",
|
||||
"Wataridori",
|
||||
"weixin",
|
||||
"Wenders",
|
||||
|
|
143
LICENSE
143
LICENSE
|
@ -1,5 +1,5 @@
|
|||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
|
@ -7,15 +7,17 @@
|
|||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
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
|
||||
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
|
||||
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
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
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.
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
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.
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
@ -60,7 +72,7 @@ modification follow.
|
|||
|
||||
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
|
||||
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
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU 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.
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU 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
|
||||
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
|
||||
3 of the GNU General Public License.
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU 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
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU 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.
|
||||
|
||||
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
|
||||
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>
|
||||
|
||||
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
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU 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/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
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
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
|
|
|
@ -1,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).
|
||||
|
||||
|
|
94
index.html
94
index.html
|
@ -10,100 +10,12 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta
|
||||
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>
|
||||
<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>
|
||||
<title>KUNGalgame</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="kungalgame-loading-container">
|
||||
<div id="kungalgame-loading"><h2>Loading</h2></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
52
package.json
52
package.json
|
@ -18,43 +18,33 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@milkdown/core": "7.3.6",
|
||||
"@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",
|
||||
"@vueup/vue-quill": "^1.2.0",
|
||||
"animate.css": "^4.1.1",
|
||||
"dayjs": "^1.11.10",
|
||||
"dompurify": "^3.0.6",
|
||||
"localforage": "^1.10.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia-plugin-persistedstate": "^3.2.1",
|
||||
"refractor": "^4.8.1",
|
||||
"vue": "^3.4.24",
|
||||
"vue-i18n": "^9.13.1",
|
||||
"vue-router": "^4.3.2"
|
||||
"pinia-plugin-persistedstate": "^3.2.0",
|
||||
"quill-blot-formatter": "^1.0.5",
|
||||
"quill-image-compress": "^1.2.30",
|
||||
"quill-magic-url": "^4.2.0",
|
||||
"vue": "^3.3.6",
|
||||
"vue-i18n": "^9.5.0",
|
||||
"vue-router": "^4.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/vue": "^4.1.2",
|
||||
"@types/node": "^20.12.7",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"sass": "^1.75.0",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.10",
|
||||
"vue-tsc": "^2.0.14"
|
||||
"@iconify/vue": "^4.1.1",
|
||||
"@types/dompurify": "^3.0.4",
|
||||
"@types/js-cookie": "^3.0.5",
|
||||
"@types/node": "^20.8.7",
|
||||
"@types/nprogress": "^0.2.2",
|
||||
"@vitejs/plugin-vue": "^4.4.0",
|
||||
"rollup-plugin-visualizer": "^5.9.2",
|
||||
"sass": "^1.69.4",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.5.0",
|
||||
"vue-tsc": "^1.8.19"
|
||||
},
|
||||
"keywords": [
|
||||
"kun",
|
||||
|
@ -68,4 +58,4 @@
|
|||
"visual novel"
|
||||
],
|
||||
"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(
|
||||
() => import('@/components/capture/Capture.vue')
|
||||
)
|
||||
const KUNGalgameSearchBox = defineAsyncComponent(
|
||||
() => import('@/components/search/KUNGalgameSearchBox.vue')
|
||||
)
|
||||
|
||||
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
@ -47,9 +44,6 @@ onBeforeMount(() => {
|
|||
<!-- Global capture component -->
|
||||
<Capture />
|
||||
|
||||
<!-- Global search component -->
|
||||
<KUNGalgameSearchBox />
|
||||
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@ export interface EditUpdateTopicRequestData {
|
|||
content: string
|
||||
tags: string[]
|
||||
category: string[]
|
||||
edited: number
|
||||
}
|
||||
|
||||
// Request data format for getting hot tags
|
||||
|
|
|
@ -5,25 +5,11 @@ import type * as Home from './types/home'
|
|||
|
||||
// URLs to be requested
|
||||
const homeURLs = {
|
||||
search: `/home/search`,
|
||||
home: `/home/topic`,
|
||||
navHot: `/home/nav/hot`,
|
||||
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
|
||||
export async function getHomeTopicApi(
|
||||
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 {
|
||||
uid: number
|
||||
avatar: string
|
||||
|
@ -33,6 +17,7 @@ export interface HomeNewTopic {
|
|||
}
|
||||
|
||||
export interface HomeTopicRequestData {
|
||||
keywords: string
|
||||
category: string
|
||||
page: number
|
||||
limit: number
|
||||
|
@ -59,12 +44,11 @@ export interface HomeTopic {
|
|||
upvote_time: number
|
||||
}
|
||||
|
||||
export type HomeSearchTopicResponseData = KUNGalgameResponseData<
|
||||
HomeSearchTopic[]
|
||||
>
|
||||
|
||||
// 10 hot topics on the left side
|
||||
export type HomeHotTopicResponseData = KUNGalgameResponseData<HomeHotTopic[]>
|
||||
|
||||
// 10 latest topics on the left side
|
||||
export type HomeNewTopicResponseData = KUNGalgameResponseData<HomeNewTopic[]>
|
||||
|
||||
// Topics displayed in the middle
|
||||
export type HomeTopicResponseData = KUNGalgameResponseData<HomeTopic[]>
|
||||
|
|
|
@ -16,8 +16,6 @@ export * from './non-moe/types/nonMoe'
|
|||
export * from './ranking/types/ranking'
|
||||
export * from './topic/types'
|
||||
export * from './update-log/types/updateLog'
|
||||
export * from './pool/types/pool'
|
||||
export * from './technique/types/technique'
|
||||
|
||||
// Expose all APIs
|
||||
export * from './balance'
|
||||
|
@ -29,5 +27,3 @@ export * from './non-moe'
|
|||
export * from './ranking'
|
||||
export * from './topic'
|
||||
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'
|
||||
|
||||
// Get topic replies by tid
|
||||
export async function getRepliesByTidApi(
|
||||
export async function getRepliesByPidApi(
|
||||
request: Reply.TopicReplyRequestData
|
||||
): Promise<Reply.TopicReplyResponseData> {
|
||||
const queryParams = objectToQueryParams(request, 'tid')
|
||||
|
@ -16,7 +16,7 @@ export async function getRepliesByTidApi(
|
|||
}
|
||||
|
||||
// Create a reply by tid
|
||||
export async function postReplyByTidApi(
|
||||
export async function postReplyByPidApi(
|
||||
request: Reply.TopicCreateReplyRequestData
|
||||
): Promise<Reply.TopicCreateReplyResponseData> {
|
||||
const url = `/topics/${request.tid}/reply`
|
||||
|
|
|
@ -13,8 +13,6 @@ export interface TopicAsideOtherTagRequestData {
|
|||
}
|
||||
|
||||
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
|
||||
tid: string
|
||||
}
|
||||
|
|
|
@ -37,7 +37,6 @@ export interface TopicCreateReplyRequestData {
|
|||
to_floor: number
|
||||
tags: string[]
|
||||
content: string
|
||||
time: number
|
||||
}
|
||||
|
||||
// Upvote reply, the upvote operation is irreversible
|
||||
|
@ -45,7 +44,6 @@ export interface TopicUpvoteReplyRequestData {
|
|||
tid: number
|
||||
to_uid: number
|
||||
rid: number
|
||||
time: number
|
||||
}
|
||||
|
||||
// Like reply
|
||||
|
@ -70,7 +68,6 @@ export interface TopicUpdateReplyRequestData {
|
|||
rid: number
|
||||
content: string
|
||||
tags: string[]
|
||||
edited: number
|
||||
}
|
||||
|
||||
// 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 {
|
||||
tid: number
|
||||
to_uid: number
|
||||
time: number
|
||||
}
|
||||
|
||||
// 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>
|
||||
|
||||
<template>
|
||||
<!-- Back to homepage -->
|
||||
<div class="return" @click="router.back()">
|
||||
<span>{{ `< ${$tm('back.back')}` }}</span>
|
||||
</div>
|
||||
|
|
|
@ -1,27 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue'
|
||||
</script>
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="footer">
|
||||
<div>{{ $tm('footer.copyright') }}</div>
|
||||
|
||||
<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>
|
||||
<span>Copyright © 2023 KUNGalgame</span>
|
||||
<span>All rights reserved | Version 1.0.0</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -34,25 +16,5 @@ import { Icon } from '@iconify/vue'
|
|||
flex-direction: column;
|
||||
justify-content: 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>
|
||||
|
|
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">
|
||||
import { useTempMessageStore } from '@/store/temp/message'
|
||||
import { useKUNGalgameMessageStore } from '@/store/modules/message'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const { showAlert, alertMsg, isShowCancel } = storeToRefs(useTempMessageStore())
|
||||
const { showAlert, alertMsg, isShowCancel } = storeToRefs(
|
||||
useKUNGalgameMessageStore()
|
||||
)
|
||||
|
||||
const handleClose = () => {
|
||||
showAlert.value = false
|
||||
useTempMessageStore().handleClose()
|
||||
useKUNGalgameMessageStore().handleClose()
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
showAlert.value = false
|
||||
useTempMessageStore().handleConfirm()
|
||||
useKUNGalgameMessageStore().handleConfirm()
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -46,7 +48,7 @@ const handleConfirm = () => {
|
|||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--kungalgame-mask-color-0);
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
transition: opacity 0.3s ease;
|
||||
color: var(--kungalgame-font-color-3);
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useTempMessageStore } from '@/store/temp/message'
|
||||
import { useKUNGalgameMessageStore } from '@/store/modules/message'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import img from './loli'
|
||||
import 'animate.css'
|
||||
|
||||
const { showInfo, infoMsg } = storeToRefs(useTempMessageStore())
|
||||
const { showInfo, infoMsg } = storeToRefs(useKUNGalgameMessageStore())
|
||||
|
||||
const { loli, name } = img
|
||||
|
||||
|
@ -55,7 +55,7 @@ const handleClose = () => {
|
|||
min-height: 120px;
|
||||
width: 100%;
|
||||
color: var(--kungalgame-font-color-3);
|
||||
background-color: var(--kungalgame-trans-white-2);
|
||||
background-color: var(--kungalgame-trans-white-5);
|
||||
backdrop-filter: blur(2px);
|
||||
box-shadow: var(--shadow);
|
||||
border-top: 1px solid var(--kungalgame-blue-1);
|
||||
|
|
|
@ -1,17 +1,21 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
// Import questions
|
||||
import { questionsEN, Question } from './questionsEN'
|
||||
import { questionsCN } from './questionsCN'
|
||||
|
||||
// Global message component (top)
|
||||
import Message from '@/components/alert/Message'
|
||||
|
||||
import { useTempMessageStore } from '@/store/temp/message'
|
||||
// Import message store
|
||||
import { useKUNGalgameMessageStore } from '@/store/modules/message'
|
||||
// Import settings component to get the language
|
||||
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
// Use the settings store to get the language
|
||||
const { showKUNGalgameLanguage } = storeToRefs(useKUNGalgameSettingsStore())
|
||||
// Variables from the message component
|
||||
const { isShowCapture, isCaptureSuccessful } = storeToRefs(
|
||||
useTempMessageStore()
|
||||
useKUNGalgameMessageStore()
|
||||
)
|
||||
// Current language
|
||||
const questions = ref<Question[]>([])
|
||||
|
@ -37,7 +41,9 @@ const userAnswers = ref('')
|
|||
// Current question index
|
||||
const currentQuestionIndex = ref(randomizeQuestion())
|
||||
// Current question
|
||||
const currentQuestion = ref(questions.value[currentQuestionIndex.value])
|
||||
const currentQuestion = computed(
|
||||
() => questions.value[currentQuestionIndex.value]
|
||||
)
|
||||
// Error count
|
||||
const errorCounter = ref(0)
|
||||
const expectedKeys = ref(['k', 'u', 'n'])
|
||||
|
@ -47,16 +53,6 @@ const isShowHint = ref(false)
|
|||
// Whether to show the answer
|
||||
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
|
||||
const checkKeyPress = (event: KeyboardEvent) => {
|
||||
const pressedKey = event.key
|
||||
|
@ -91,7 +87,6 @@ const submitAnswer = () => {
|
|||
'人机身份验证通过 ~',
|
||||
'success'
|
||||
)
|
||||
resetStatus()
|
||||
} else {
|
||||
// Wrong answer
|
||||
errorCounter.value++
|
||||
|
@ -109,12 +104,6 @@ const submitAnswer = () => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close panel
|
||||
const handleCloseCapture = () => {
|
||||
isShowCapture.value = false
|
||||
resetStatus()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -152,7 +141,7 @@ const handleCloseCapture = () => {
|
|||
<button @click="submitAnswer">
|
||||
{{ $tm('AlertInfo.capture.submit') }}
|
||||
</button>
|
||||
<button @click="handleCloseCapture">
|
||||
<button @click="isShowCapture = false">
|
||||
{{ $tm('AlertInfo.capture.close') }}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -193,7 +182,7 @@ const handleCloseCapture = () => {
|
|||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--kungalgame-mask-color-0);
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
transition: opacity 0.3s ease;
|
||||
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">
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { computed, defineAsyncComponent, ref } from 'vue'
|
||||
// Import the router
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
// Import icon font
|
||||
import { Icon } from '@iconify/vue'
|
||||
|
||||
// Asynchronously import the editor settings menu
|
||||
const EditorSettingsMenu = defineAsyncComponent(
|
||||
() => import('./EditorSettingsMenu.vue')
|
||||
|
@ -11,17 +13,25 @@ const EditorSettingsMenu = defineAsyncComponent(
|
|||
// Import CSS animations
|
||||
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'
|
||||
|
||||
// Topic editing page store
|
||||
const { textCount } = storeToRefs(useKUNGalgameEditStore())
|
||||
// Topic page store for replies and adjusting reply panel width
|
||||
const { replyPanelWidth } = storeToRefs(usePersistKUNGalgameReplyStore())
|
||||
const { replyDraft, replyPanelWidth } = storeToRefs(useKUNGalgameTopicStore())
|
||||
|
||||
// Current route
|
||||
const route = useRoute()
|
||||
// Name of the current page route
|
||||
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
|
||||
const isShowSettingsMenu = ref(false)
|
||||
// Style when the settings panel is activated
|
||||
|
@ -45,7 +55,7 @@ const handelCloseSettingsMenu = () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="footer">
|
||||
<!-- Display the settings button -->
|
||||
<div class="settings">
|
||||
<span
|
||||
|
@ -69,6 +79,9 @@ const handelCloseSettingsMenu = () => {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<!-- Word count -->
|
||||
<span class="count">{{ textCountNumber + ` ${$tm('edit.word')}` }}</span>
|
||||
|
||||
<!-- Settings panel -->
|
||||
<EditorSettingsMenu
|
||||
@close="handelCloseSettingsMenu"
|
||||
|
@ -78,7 +91,9 @@ const handelCloseSettingsMenu = () => {
|
|||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
.footer {
|
||||
padding: 10px 17px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
@ -118,10 +133,6 @@ const handelCloseSettingsMenu = () => {
|
|||
margin-bottom: 100px;
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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.
|
||||
.settings-icon-active {
|
||||
color: var(--kungalgame-blue-4);
|
|
@ -1,36 +1,53 @@
|
|||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { computed } from 'vue'
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
// Import the icon font
|
||||
import { Icon } from '@iconify/vue'
|
||||
// Import CSS animations
|
||||
import 'animate.css'
|
||||
|
||||
// Import the topic editing store
|
||||
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 the keyword display toggle button
|
||||
import SwitchButton from './SwitchButton.vue'
|
||||
|
||||
const { editorHeight: editEditorHeight } = storeToRefs(useKUNGalgameEditStore())
|
||||
const { editorHeight: replyEditorHeight } = storeToRefs(
|
||||
usePersistKUNGalgameReplyStore()
|
||||
)
|
||||
// Topic editing page store
|
||||
const { editorHeight, mode } = storeToRefs(useKUNGalgameEditStore())
|
||||
// Topic page store for replies
|
||||
const { replyDraft } = storeToRefs(useKUNGalgameTopicStore())
|
||||
|
||||
defineProps<{
|
||||
isShowSettingsMenu: boolean
|
||||
}>()
|
||||
|
||||
// Define emits to close the settings panel
|
||||
const emits = defineEmits<{
|
||||
close: [isShowSettingsMenu: boolean]
|
||||
}>()
|
||||
|
||||
// Current route
|
||||
const route = useRoute()
|
||||
// Name of the current page route
|
||||
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 = () => {
|
||||
emits('close', false)
|
||||
}
|
||||
|
@ -47,18 +64,18 @@ const handelCloseSettingsPanel = () => {
|
|||
<!-- Editor height settings -->
|
||||
<div class="editor-height-title">
|
||||
<span> {{ $tm('edit.editorHeight') }} </span>
|
||||
<span>{{ editorHeight }} </span>
|
||||
<span>{{ editorHeight }} px</span>
|
||||
</div>
|
||||
|
||||
<!-- Editor page -->
|
||||
<div v-if="routeName === 'Edit'" class="editor-height">
|
||||
<span>200 px</span>
|
||||
<span>300 px</span>
|
||||
<input
|
||||
type="range"
|
||||
min="200"
|
||||
min="300"
|
||||
max="500"
|
||||
step="1"
|
||||
v-model="editEditorHeight"
|
||||
v-model="editorHeight"
|
||||
/>
|
||||
<span>500 px</span>
|
||||
</div>
|
||||
|
@ -71,11 +88,46 @@ const handelCloseSettingsPanel = () => {
|
|||
min="100"
|
||||
max="500"
|
||||
step="1"
|
||||
v-model="replyEditorHeight"
|
||||
v-model="replyDraft.editorHeight"
|
||||
/>
|
||||
<span>500 px</span>
|
||||
</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 -->
|
||||
<div class="keywords">
|
||||
<div class="keywords-title">{{ $tm('edit.tagsHint') }}</div>
|
||||
|
@ -134,6 +186,35 @@ const handelCloseSettingsPanel = () => {
|
|||
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 {
|
||||
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">
|
||||
import { watch, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
// Import the store for the editing page
|
||||
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'
|
||||
|
||||
// Current page's route
|
||||
|
@ -14,20 +15,16 @@ const route = useRoute()
|
|||
const routeName = computed(() => route.name as string)
|
||||
|
||||
// Use the store for the editing page
|
||||
const { isShowHotKeywords: isShowEditHotKeywords } = storeToRefs(
|
||||
useKUNGalgameEditStore()
|
||||
)
|
||||
const { isShowHotKeywords } = storeToRefs(useKUNGalgameEditStore())
|
||||
// Store for the topic page, used for replies
|
||||
const { isShowHotKeywords: isShowReplyHotKeywords, replyDraft } = storeToRefs(
|
||||
usePersistKUNGalgameReplyStore()
|
||||
)
|
||||
const { replyDraft } = storeToRefs(useKUNGalgameTopicStore())
|
||||
|
||||
// Watch for changes in store states to keep button states in sync with the store
|
||||
watch(
|
||||
() => [isShowEditHotKeywords.value, isShowReplyHotKeywords.value],
|
||||
() => [isShowHotKeywords.value, replyDraft.value.isShowHotKeywords],
|
||||
([newValue1, newValue2]) => {
|
||||
isShowEditHotKeywords.value = newValue1
|
||||
isShowReplyHotKeywords.value = newValue2
|
||||
isShowHotKeywords.value = newValue1
|
||||
replyDraft.value.isShowHotKeywords = newValue2
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
@ -38,14 +35,13 @@ watch(
|
|||
v-if="routeName === 'Edit'"
|
||||
type="checkbox"
|
||||
id="switch"
|
||||
v-model="isShowEditHotKeywords"
|
||||
v-model="isShowHotKeywords"
|
||||
/>
|
||||
|
||||
<input
|
||||
v-if="routeName === 'Topic'"
|
||||
type="checkbox"
|
||||
id="switch"
|
||||
v-model="isShowReplyHotKeywords"
|
||||
v-model="replyDraft.isShowHotKeywords"
|
||||
/>
|
||||
<label for="switch"></label>
|
||||
</template>
|
|
@ -1,16 +1,16 @@
|
|||
<script setup lang="ts">
|
||||
import { onBeforeMount, ref } from 'vue'
|
||||
|
||||
import { useTempEditStore } from '@/store/temp/edit'
|
||||
// Import the store for editing topics
|
||||
import { useKUNGalgameEditStore } from '@/store/modules/edit'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
// Import debounce function
|
||||
import { debounce } from '@/utils/debounce'
|
||||
|
||||
const { title: rewriteTitle, isTopicRewriting } = storeToRefs(
|
||||
useTempEditStore()
|
||||
const { isSaveTopic, title, topicRewrite } = storeToRefs(
|
||||
useKUNGalgameEditStore()
|
||||
)
|
||||
const { isSaveTopic, title: editTitle } = storeToRefs(useKUNGalgameEditStore())
|
||||
|
||||
// Topic title text
|
||||
const topicTitle = ref('')
|
||||
|
@ -22,14 +22,14 @@ onBeforeMount(() => {
|
|||
* Editor is in edit mode
|
||||
*/
|
||||
if (isSaveTopic.value) {
|
||||
topicTitle.value = editTitle.value
|
||||
topicTitle.value = title.value
|
||||
}
|
||||
/**
|
||||
* Editor is in re-editing edit mode
|
||||
*/
|
||||
// Load data for re-editing a topic before mounting
|
||||
if (isTopicRewriting.value) {
|
||||
topicTitle.value = rewriteTitle.value
|
||||
if (topicRewrite.value.isTopicRewriting) {
|
||||
topicTitle.value = topicRewrite.value.title
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -40,28 +40,33 @@ const handleInput = () => {
|
|||
topicTitle.value = topicTitle.value.slice(0, maxInputLength)
|
||||
}
|
||||
|
||||
// User input is pure whitespace
|
||||
if (topicTitle.value.trim() === '') {
|
||||
rewriteTitle.value = ''
|
||||
editTitle.value = ''
|
||||
title.value = ''
|
||||
topicRewrite.value.title = ''
|
||||
return
|
||||
}
|
||||
|
||||
return debounce(() => {
|
||||
// Create a debounce handling function
|
||||
const debouncedInput = debounce(() => {
|
||||
/**
|
||||
* Editor is in reply mode
|
||||
*/
|
||||
// Save to the edit store if not in topic re-edit mode
|
||||
if (!isTopicRewriting.value) {
|
||||
editTitle.value = topicTitle.value
|
||||
if (!topicRewrite.value.isTopicRewriting) {
|
||||
title.value = topicTitle.value
|
||||
}
|
||||
/**
|
||||
* Editor is in re-editing edit mode
|
||||
*/
|
||||
// Save to the re-editing page's store if in re-edit mode
|
||||
if (isTopicRewriting.value) {
|
||||
rewriteTitle.value = topicTitle.value
|
||||
if (topicRewrite.value.isTopicRewriting) {
|
||||
topicRewrite.value.title = topicTitle.value
|
||||
}
|
||||
}, 300)
|
||||
|
||||
// Call the debounce handling function, which will execute the update operation only once within the delay time
|
||||
debouncedInput()
|
||||
}
|
||||
</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">
|
||||
// Import icon font
|
||||
import { Icon } from '@iconify/vue'
|
||||
|
||||
// Import mascot component
|
||||
import Loli from './components/Loli.vue'
|
||||
// Import mode switch component
|
||||
import Mode from './components/Mode.vue'
|
||||
// Import language switch component
|
||||
import SwitchLanguage from './components/SwitchLanguage.vue'
|
||||
// Page width adjustment component
|
||||
import PageWidth from './components/PageWidth.vue'
|
||||
// Font settings component
|
||||
import Font from './components/Font.vue'
|
||||
// Import background settings component
|
||||
import Background from './components/Background.vue'
|
||||
|
||||
// Import settings store
|
||||
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
// Use the settings store
|
||||
const settingsStore = useKUNGalgameSettingsStore()
|
||||
const { isShowPageWidth } = storeToRefs(settingsStore)
|
||||
|
||||
// Define emits to close the settings panel
|
||||
const emits = defineEmits<{
|
||||
close: [showKUNGalgamePanel: boolean]
|
||||
}>()
|
||||
|
||||
// Restore all settings to default
|
||||
const handleRecover = () => {
|
||||
settingsStore.setKUNGalgameSettingsRecover()
|
||||
}
|
||||
|
||||
// Close the settings panel
|
||||
const handelCloseSettingsPanel = () => {
|
||||
emits('close', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Root element -->
|
||||
<div class="root">
|
||||
<div class="container">
|
||||
<div class="title">
|
||||
|
@ -35,8 +47,10 @@ const handelCloseSettingsPanel = () => {
|
|||
<span><Icon class="settings-icon" icon="uiw:setting-o" /></span>
|
||||
</div>
|
||||
|
||||
<!-- Mode switch component -->
|
||||
<Mode />
|
||||
|
||||
<!-- Language switch component -->
|
||||
<SwitchLanguage />
|
||||
|
||||
<div class="switch">
|
||||
|
@ -57,15 +71,18 @@ const handelCloseSettingsPanel = () => {
|
|||
|
||||
<TransitionGroup name="item" tag="div">
|
||||
<div class="item" v-if="isShowPageWidth">
|
||||
<!-- Page width adjustment component -->
|
||||
<PageWidth />
|
||||
</div>
|
||||
|
||||
<div class="item" v-else-if="!isShowPageWidth">
|
||||
<!-- Set the page font -->
|
||||
<Font />
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
|
||||
<!-- Background settings component -->
|
||||
<Background />
|
||||
|
||||
<button class="reset" @click="handleRecover">
|
||||
|
@ -73,15 +90,19 @@ const handelCloseSettingsPanel = () => {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mascot component -->
|
||||
<Loli class="loli" />
|
||||
|
||||
<!-- Close panel -->
|
||||
<div class="close">
|
||||
<!-- showKUNGalgamePanel exists in the settings, false to close the settings panel -->
|
||||
<Icon @click="handelCloseSettingsPanel" icon="line-md:close" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* Root container */
|
||||
.root {
|
||||
top: 65px;
|
||||
right: 0;
|
||||
|
@ -113,6 +134,7 @@ const handelCloseSettingsPanel = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// Keep the settings button rotating
|
||||
.settings-icon {
|
||||
animation: settings 3s linear infinite;
|
||||
}
|
||||
|
@ -126,6 +148,7 @@ const handelCloseSettingsPanel = () => {
|
|||
}
|
||||
}
|
||||
|
||||
/* Menu for switching settings options */
|
||||
.switch {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -167,9 +190,9 @@ const handelCloseSettingsPanel = () => {
|
|||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
margin-top: 20px;
|
||||
color: var(--kungalgame-red-4);
|
||||
color: var(--kungalgame-font-color-3);
|
||||
border: 1px solid var(--kungalgame-red-4);
|
||||
background-color: var(--kungalgame-trans-white-9);
|
||||
background-color: var(--kungalgame-trans-red-1);
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
transition: all 0.2s;
|
||||
|
@ -189,7 +212,7 @@ const handelCloseSettingsPanel = () => {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.item-move,
|
||||
.item-move, /* Transition applied to moving elements */
|
||||
.item-enter-active,
|
||||
.item-leave-active {
|
||||
transition: all 0.5s ease;
|
||||
|
@ -201,6 +224,8 @@ const handelCloseSettingsPanel = () => {
|
|||
transform: translateY(77px);
|
||||
}
|
||||
|
||||
/* Ensure the leaving element is removed from the layout flow
|
||||
to correctly calculate the animated movement. */
|
||||
.item-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
|
|
|
@ -1,26 +1,46 @@
|
|||
<script setup lang="ts">
|
||||
// Import Vue functions
|
||||
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 { storeToRefs } from 'pinia'
|
||||
|
||||
// Global message component (top)
|
||||
import Message from '@/components/alert/Message'
|
||||
|
||||
import { backgroundImages } from './background'
|
||||
import { getBackgroundURL } from '@/hooks/useBackgroundPicture'
|
||||
import { restoreBackground } from '@/hooks/useBackgroundPicture'
|
||||
|
||||
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) => {
|
||||
return await getBackgroundURL(`bg${imageNumber}-m`)
|
||||
}
|
||||
|
||||
// Change the background image
|
||||
const handleChangeImage = (index: number) => {
|
||||
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 () => {
|
||||
for (const background of backgroundImages) {
|
||||
const backgroundURL = await getBackground(background.index)
|
||||
|
@ -31,32 +51,45 @@ onMounted(async () => {
|
|||
|
||||
<template>
|
||||
<div class="kungalgame-background">
|
||||
<div class="bg-settings">
|
||||
{{ $tm('header.settings.background') }}
|
||||
</div>
|
||||
<div class="bg-settings">{{ $tm('header.settings.background') }}</div>
|
||||
<ul class="kungalgame-background-container">
|
||||
<li>
|
||||
<span>{{ $tm('header.settings.preset') }}</span>
|
||||
<!-- Preset background collection -->
|
||||
<ul class="kungalgame-restore-bg">
|
||||
<li
|
||||
v-for="kun in backgroundImages"
|
||||
:key="kun.index"
|
||||
v-tooltip="{ message: kun.message, position: 'bottom' }"
|
||||
>
|
||||
<li v-for="kun in backgroundImages" :key="kun.index">
|
||||
<img
|
||||
v-if="kun"
|
||||
:src="imageArray[kun.index - 1]"
|
||||
:alt="kun.alt"
|
||||
@click="handleChangeImage(kun.index)"
|
||||
/>
|
||||
|
||||
<BackgroundImageSkeleton v-if="!imageArray[kun.index - 1]" />
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<!-- User-customized background -->
|
||||
<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">
|
||||
{{ $tm('header.settings.restore') }}
|
||||
</button>
|
||||
|
@ -66,60 +99,102 @@ onMounted(async () => {
|
|||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* Background settings */
|
||||
.kungalgame-background-container {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
/* Height of the background menu */
|
||||
height: 100%;
|
||||
font-size: 15px;
|
||||
font-weight: normal;
|
||||
color: var(--kungalgame-font-color-3);
|
||||
|
||||
/* Font for the title of the background container */
|
||||
span {
|
||||
height: 30px;
|
||||
/* Centered */
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-settings {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
/* Grid of background image thumbnails, three rows and three columns */
|
||||
.kungalgame-restore-bg {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
text-decoration: none;
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
grid-template-columns: repeat(3, 80px);
|
||||
grid-template-rows: repeat(3, 50px);
|
||||
position: relative;
|
||||
/* Distance from the lower area */
|
||||
margin-bottom: 10px;
|
||||
|
||||
/* Center individual images */
|
||||
li {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
/* Spacing for individual images */
|
||||
img {
|
||||
cursor: pointer;
|
||||
width: 70px;
|
||||
position: relative;
|
||||
|
||||
/* Image hover effect */
|
||||
&:hover {
|
||||
transform: scale(3);
|
||||
transition: 0.2s;
|
||||
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 {
|
||||
position: absolute;
|
||||
input {
|
||||
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;
|
||||
color: var(--kungalgame-font-color-3);
|
||||
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;
|
||||
color: var(--kungalgame-blue-4);
|
||||
|
||||
&:hover {
|
||||
color: var(--kungalgame-white);
|
||||
background-color: var(--kungalgame-blue-4);
|
||||
background-color: var(--kungalgame-trans-blue-2);
|
||||
}
|
||||
}
|
||||
</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">
|
||||
import { ref } from 'vue'
|
||||
|
||||
// Global message component (top)
|
||||
import Message from '@/components/alert/Message'
|
||||
|
||||
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
|
||||
|
@ -21,6 +23,7 @@ const setFont = () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Set the width of certain pages -->
|
||||
<div class="font">
|
||||
<div class="title">
|
||||
<span>{{ $tm('header.settings.font') }}</span>
|
||||
|
@ -31,7 +34,6 @@ const setFont = () => {
|
|||
{{ showKUNGalgameFontStyle }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="font-input">
|
||||
<input
|
||||
:placeholder="`${$tm('header.settings.fontInput')}`"
|
||||
|
@ -53,6 +55,7 @@ const setFont = () => {
|
|||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* URL input box */
|
||||
.font-input {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
@ -68,12 +71,13 @@ const setFont = () => {
|
|||
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;
|
||||
|
@ -85,9 +89,11 @@ const setFont = () => {
|
|||
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);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { useLoliDataURL } from '@/hooks/useLoli'
|
||||
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({
|
||||
loliBodyLeft: '',
|
||||
|
@ -22,71 +19,57 @@ const loliData = ref({
|
|||
mouth: '',
|
||||
face: '',
|
||||
})
|
||||
const isShowLoading = ref(false)
|
||||
|
||||
const reGetLoli = async () => {
|
||||
isShowLoading.value = true
|
||||
loliData.value = await getLoli()
|
||||
isShowLoading.value = false
|
||||
}
|
||||
const reGetLoli = async () => (loliData.value = await useLoliDataURL())
|
||||
|
||||
onMounted(async () => {
|
||||
await reGetLoli()
|
||||
loliData.value = await useLoliDataURL()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="loli-container">
|
||||
<div class="loli" @click="reGetLoli" v-if="loliData.body">
|
||||
<img
|
||||
class="body"
|
||||
:src="loliData.body"
|
||||
alt="ren"
|
||||
:style="{ left: loliData.loliBodyLeft, top: loliData.loliBodyTop }"
|
||||
/>
|
||||
<img
|
||||
class="eye"
|
||||
:src="loliData.eye"
|
||||
alt="ren"
|
||||
:style="{ left: loliData.loliEyeLeft, top: loliData.loliEyeTop }"
|
||||
/>
|
||||
<img
|
||||
class="brow"
|
||||
:src="loliData.brow"
|
||||
alt="ren"
|
||||
:style="{ left: loliData.loliBrowLeft, top: loliData.loliBrowTop }"
|
||||
/>
|
||||
<img
|
||||
class="mouth"
|
||||
:src="loliData.mouth"
|
||||
alt="ren"
|
||||
:style="{ left: loliData.loliMouthLeft, top: loliData.loliMouthTop }"
|
||||
/>
|
||||
<img
|
||||
class="face"
|
||||
:src="loliData.face"
|
||||
alt="ren"
|
||||
:style="{ left: loliData.loliFaceLeft, top: loliData.loliFaceTop }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<LoliSkeleton v-if="!loliData.body" />
|
||||
|
||||
<KUNGalgameLoading v-if="isShowLoading" style="top: 310px; left: 140px" />
|
||||
<div class="loli" @click="reGetLoli">
|
||||
<img
|
||||
class="body"
|
||||
:src="loliData.body"
|
||||
alt="ren"
|
||||
:style="{ left: loliData.loliBodyLeft, top: loliData.loliBodyTop }"
|
||||
/>
|
||||
<img
|
||||
class="eye"
|
||||
:src="loliData.eye"
|
||||
alt="ren"
|
||||
:style="{ left: loliData.loliEyeLeft, top: loliData.loliEyeTop }"
|
||||
/>
|
||||
<img
|
||||
class="brow"
|
||||
:src="loliData.brow"
|
||||
alt="ren"
|
||||
:style="{ left: loliData.loliBrowLeft, top: loliData.loliBrowTop }"
|
||||
/>
|
||||
<img
|
||||
class="mouth"
|
||||
:src="loliData.mouth"
|
||||
alt="ren"
|
||||
:style="{ left: loliData.loliMouthLeft, top: loliData.loliMouthTop }"
|
||||
/>
|
||||
<img
|
||||
class="face"
|
||||
:src="loliData.face"
|
||||
alt="ren"
|
||||
:style="{ left: loliData.loliFaceLeft, top: loliData.loliFaceTop }"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.loli-container {
|
||||
top: -270px;
|
||||
left: 130px;
|
||||
}
|
||||
|
||||
.loli {
|
||||
cursor: pointer;
|
||||
width: 0;
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
top: -270px;
|
||||
left: 130px;
|
||||
}
|
||||
.body {
|
||||
position: absolute;
|
||||
|
|
|
@ -1,25 +1,30 @@
|
|||
<script setup lang="ts">
|
||||
// Import the icon font
|
||||
import { Icon } from '@iconify/vue'
|
||||
// Import the settings store
|
||||
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
|
||||
|
||||
// Use the settings store
|
||||
const settingsStore = useKUNGalgameSettingsStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mode">
|
||||
<!-- Day / Night mode toggle -->
|
||||
<span>{{ $tm('header.settings.mode') }}</span>
|
||||
<div class="mode-container">
|
||||
<span>
|
||||
<Icon
|
||||
class="sun"
|
||||
icon="line-md:moon-filled-alt-to-sunny-filled-loop-transition"
|
||||
@click="useKUNGalgameSettingsStore().setKUNGalgameTheme('')"
|
||||
@click="settingsStore.setKUNGalgameTheme('')"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<Icon
|
||||
class="moon"
|
||||
icon="line-md:sunny-outline-to-moon-loop-transition"
|
||||
@click="useKUNGalgameSettingsStore().setKUNGalgameTheme('dark')"
|
||||
@click="settingsStore.setKUNGalgameTheme('dark')"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -33,7 +38,6 @@ import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
|
|||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.mode-container {
|
||||
font-size: 25px;
|
||||
width: 60%;
|
||||
|
|
|
@ -9,10 +9,14 @@ const route = useRoute()
|
|||
const settingsStore = useKUNGalgameSettingsStore()
|
||||
const { showKUNGalgamePageWidth } = storeToRefs(settingsStore)
|
||||
|
||||
// Page width
|
||||
const pageWidth = ref(0)
|
||||
// Current route name
|
||||
const routeName = computed(() => route.name as string)
|
||||
// Whether page width adjustment is disabled
|
||||
const isDisabled = ref(false)
|
||||
|
||||
// Pages where width adjustment is allowed
|
||||
const pageNameArray = [
|
||||
'KUN',
|
||||
'Topic',
|
||||
|
@ -24,9 +28,12 @@ const pageNameArray = [
|
|||
'ThanksList',
|
||||
]
|
||||
|
||||
// Initialize page width
|
||||
const initPageWidth = () => {
|
||||
if (pageNameArray.includes(routeName.value)) {
|
||||
// Page width value equals store width value
|
||||
pageWidth.value = showKUNGalgamePageWidth.value[routeName.value]
|
||||
// Enable input
|
||||
isDisabled.value = false
|
||||
} else {
|
||||
isDisabled.value = true
|
||||
|
@ -35,6 +42,7 @@ const initPageWidth = () => {
|
|||
|
||||
watch(pageWidth, () => {
|
||||
if (pageNameArray.includes(routeName.value)) {
|
||||
// Store user-input width
|
||||
showKUNGalgamePageWidth.value[routeName.value] = pageWidth.value
|
||||
}
|
||||
})
|
||||
|
@ -49,6 +57,7 @@ onActivated(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Set the width for specific pages -->
|
||||
<div
|
||||
class="width"
|
||||
:class="isDisabled ? 'disabled' : ''"
|
||||
|
@ -58,7 +67,6 @@ onActivated(() => {
|
|||
<span>{{ $tm('header.settings.width') }}</span>
|
||||
<span>{{ pageWidth }}%</span>
|
||||
</div>
|
||||
|
||||
<div class="page-width">
|
||||
<span>50%</span>
|
||||
<input
|
||||
|
@ -85,7 +93,7 @@ onActivated(() => {
|
|||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Main page width slider */
|
||||
.main {
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
|
@ -97,6 +105,7 @@ onActivated(() => {
|
|||
justify-content: space-between;
|
||||
}
|
||||
|
||||
/* Styles when page width adjustment is disabled */
|
||||
.disabled {
|
||||
cursor: not-allowed;
|
||||
color: var(--kungalgame-font-color-0);
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
import { watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// Import the settings store
|
||||
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
|
||||
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' })
|
||||
|
||||
watch(showKUNGalgameLanguage, () => {
|
||||
|
@ -24,12 +31,13 @@ watch(showKUNGalgameLanguage, () => {
|
|||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// Language settings
|
||||
.set-lang {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
// Language selection box
|
||||
.select {
|
||||
width: 100px;
|
||||
font-size: 16px;
|
||||
|
|
|
@ -11,78 +11,46 @@
|
|||
https://s3.bmp.ovh/imgs/2023/05/30/7aa57120cc6977a1.png
|
||||
*/
|
||||
|
||||
interface Background {
|
||||
interface background {
|
||||
index: number
|
||||
message: BackgroundDetail
|
||||
alt: string
|
||||
}
|
||||
|
||||
interface BackgroundDetail {
|
||||
en: string
|
||||
zh: string
|
||||
}
|
||||
|
||||
export const backgroundImages: Background[] = [
|
||||
export const backgroundImages: background[] = [
|
||||
{
|
||||
index: 1,
|
||||
message: {
|
||||
en: 'Akai Hitomi ni Utsuru Sekai 紅い瞳に映るセカイ',
|
||||
zh: '紅い瞳に映るセカイ 红瞳映入的世界',
|
||||
},
|
||||
alt: 'Akai Hitomi ni Utsuru Sekai 紅い瞳に映るセカイ 红瞳映入的世界',
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
message: {
|
||||
en: 'Shugaten! しゅがてん!',
|
||||
zh: 'しゅがてん! 糖调',
|
||||
},
|
||||
alt: 'Shugaten! しゅがてん! 糖调',
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
message: {
|
||||
en: 'Amayui Castle Meister 天結いキャッスルマイスター',
|
||||
zh: '天結いキャッスルマイスター 天结神缘',
|
||||
},
|
||||
alt: 'Amayui Castle Meister 天結いキャッスルマイスター 天结神缘',
|
||||
},
|
||||
{
|
||||
index: 4,
|
||||
message: {
|
||||
en: 'Pieces Wataridori no Somnium 渡り鳥のソムニウム',
|
||||
zh: '渡り鳥のソムニウム 渡鸟的梦',
|
||||
},
|
||||
alt: 'Pieces Wataridori no Somnium 渡り鳥のソムニウム 渡鸟的梦',
|
||||
},
|
||||
{
|
||||
index: 5,
|
||||
message: {
|
||||
en: 'Karenai Sekai to Owaru Hana 枯れない世界と終わる花',
|
||||
zh: '枯れない世界と終わる花 不败世界与终焉之花',
|
||||
},
|
||||
alt: 'Karenai Sekai to Owaru Hana 枯れない世界と終わる花 不败世界与终焉之花',
|
||||
},
|
||||
{
|
||||
index: 6,
|
||||
message: {
|
||||
en: 'NEKOPARA ネコぱら',
|
||||
zh: 'ネコぱら 猫娘乐园',
|
||||
},
|
||||
alt: 'NEKOPARA ネコぱら 猫娘乐园',
|
||||
},
|
||||
{
|
||||
index: 7,
|
||||
message: {
|
||||
en: 'Sakura no Uta サクラノ詩',
|
||||
zh: 'サクラノ詩 樱之诗',
|
||||
},
|
||||
alt: 'Sakura no Uta サクラノ詩 樱之诗',
|
||||
},
|
||||
{
|
||||
index: 8,
|
||||
message: {
|
||||
en: 'Hokenshitsu no Sensei to Shabondama Chuudoku no Joshu 保健室のセンセーとシャボン玉中毒の助手',
|
||||
zh: '保健室のセンセーとシャボン玉中毒の助手 保健室的老师与肥皂泡中毒的助手',
|
||||
},
|
||||
alt: 'Hokenshitsu no Sensei to Shabondama Chuudoku no Joshu 保健室のセンセーとシャボン玉中毒の助手 保健室的老师与肥皂泡中毒的助手',
|
||||
},
|
||||
{
|
||||
index: 9,
|
||||
message: {
|
||||
en: 'Senren * Banka 千戀*萬花',
|
||||
zh: '千戀*萬花 千恋*万花',
|
||||
},
|
||||
alt: 'Senren * Banka 千恋*万花 千恋*万花',
|
||||
},
|
||||
]
|
||||
|
|
|
@ -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">
|
||||
import { Icon } from '@iconify/vue'
|
||||
// Import mode switch component
|
||||
import Mode from '../setting-panel/components/Mode.vue'
|
||||
// Import language switch component
|
||||
import SwitchLanguage from '../setting-panel/components/SwitchLanguage.vue'
|
||||
import CustomBackground from '../setting-panel/components/CustomBackground.vue'
|
||||
import { hamburgerItem } from './hamburgerItem'
|
||||
|
||||
// Import top navigation bar items
|
||||
import { topBarItem } from './topBarItem'
|
||||
// Send a close command to the parent element
|
||||
defineEmits(['showKUNGalgameHamburger'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="root" @click="$emit('showKUNGalgameHamburger', false)">
|
||||
<Transition
|
||||
enter-active-class="animate__animated animate__fadeInLeft animate__faster"
|
||||
appear
|
||||
>
|
||||
<div class="container" @click.stop>
|
||||
<div class="kungalgame">
|
||||
<img src="@/assets/images/favicon.webp" alt="KUNGalgame" />
|
||||
<span>{{ $tm('header.name') }}</span>
|
||||
</div>
|
||||
<!-- Interactive items -->
|
||||
<div class="item" style="font-size: 17px">
|
||||
<span v-for="kun in hamburgerItem" :key="kun.index">
|
||||
<RouterLink :to="kun.router">
|
||||
{{ $tm(`header.hamburger.${kun.name}`) }}
|
||||
</RouterLink>
|
||||
</span>
|
||||
</div>
|
||||
<!-- Main container -->
|
||||
<div class="container">
|
||||
<div class="kungalgame">
|
||||
<img src="@/assets/images/favicon.webp" alt="KUNGalgame" />
|
||||
<span>{{ $tm('header.name') }}</span>
|
||||
</div>
|
||||
<!-- Interactive items -->
|
||||
<div class="item" style="font-size: 17px">
|
||||
<span v-for="kun in topBarItem" :key="kun.index">
|
||||
<RouterLink :to="kun.router">{{
|
||||
$tm(`header['${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" />
|
||||
|
||||
<CustomBackground :is-mobile="true" />
|
||||
|
||||
<div class="home">
|
||||
<RouterLink to="/kun">{{ $tm('header.hamburger.home') }}</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
<!-- Language switch component -->
|
||||
<SwitchLanguage style="font-size: 20px; margin-bottom: 40px" />
|
||||
<!-- Close button -->
|
||||
<div class="close">
|
||||
<Icon
|
||||
icon="line-md:menu-fold-left"
|
||||
@click="$emit('showKUNGalgameHamburger', false)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.root {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
.container {
|
||||
height: 400px;
|
||||
width: 277px;
|
||||
position: fixed;
|
||||
padding: 10px;
|
||||
top: 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);
|
||||
border: 1px solid var(--kungalgame-blue-1);
|
||||
box-shadow: var(--shadow);
|
||||
border-left: none;
|
||||
border-top: none;
|
||||
border-radius: 0 5px 5px 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--kungalgame-font-color-3);
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
.item {
|
||||
|
@ -94,20 +86,9 @@ defineEmits(['showKUNGalgameHamburger'])
|
|||
}
|
||||
}
|
||||
|
||||
.home {
|
||||
.close {
|
||||
width: 100%;
|
||||
margin-top: 50px;
|
||||
|
||||
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);
|
||||
}
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -4,29 +4,33 @@ import { Icon } from '@iconify/vue'
|
|||
import 'animate.css'
|
||||
import { topBarItem } from './topBarItem'
|
||||
import { onBeforeRouteLeave } from 'vue-router'
|
||||
|
||||
// Mobile version hamburger
|
||||
const Hamburger = defineAsyncComponent(() => import('./Hamburger.vue'))
|
||||
// Settings panel
|
||||
const KUNGalgameSettingsPanel = defineAsyncComponent(
|
||||
() => import('../setting-panel/KUNGalgameSettingPanel.vue')
|
||||
)
|
||||
// Panel when clicking on the user's avatar
|
||||
const KUNGalgameUserInfo = defineAsyncComponent(
|
||||
() => import('./KUNGalgameUserInfo.vue')
|
||||
)
|
||||
|
||||
import { useTempHomeStore } from '@/store/temp/home'
|
||||
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const { isShowSearch } = storeToRefs(useTempHomeStore())
|
||||
const { name, avatarMin } = storeToRefs(useKUNGalgameUserStore())
|
||||
|
||||
const showKUNGalgameHamburger = ref(false)
|
||||
// Show settings panel state
|
||||
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)
|
||||
|
||||
// Set the navigation bar width based on the number of navigation items
|
||||
const navItemNum = topBarItem.length
|
||||
const navItemLength = `${navItemNum}00px`
|
||||
|
||||
// Destroy the SettingsPanel and Hamburger before leaving the route
|
||||
onBeforeRouteLeave(() => {
|
||||
showKUNGalgamePanel.value = false
|
||||
showKUNGalgameHamburger.value = false
|
||||
|
@ -35,6 +39,7 @@ onBeforeRouteLeave(() => {
|
|||
|
||||
<template>
|
||||
<div class="header">
|
||||
<!-- Top left interactive bar -->
|
||||
<div class="nav-top">
|
||||
<div class="hamburger">
|
||||
<Icon
|
||||
|
@ -42,40 +47,45 @@ onBeforeRouteLeave(() => {
|
|||
v-if="!showKUNGalgameHamburger"
|
||||
@click="showKUNGalgameHamburger = !showKUNGalgameHamburger"
|
||||
/>
|
||||
<Transition name="hamburger">
|
||||
<Hamburger
|
||||
v-if="showKUNGalgameHamburger"
|
||||
@showKUNGalgameHamburger="showKUNGalgameHamburger = false"
|
||||
/>
|
||||
</Transition>
|
||||
<transition
|
||||
enter-active-class="animate__animated animate__fadeInLeft animate__faster"
|
||||
leave-active-class="animate__animated animate__fadeOutLeft animate__faster"
|
||||
>
|
||||
<KeepAlive>
|
||||
<Hamburger
|
||||
v-if="showKUNGalgameHamburger"
|
||||
@showKUNGalgameHamburger="showKUNGalgameHamburger = false"
|
||||
/>
|
||||
</KeepAlive>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<!-- Website name and logo -->
|
||||
<div class="kungalgame">
|
||||
<RouterLink to="/kun">
|
||||
<img
|
||||
src="@/assets/images/favicon.webp"
|
||||
alt="KUN Visual Novel | 鲲 Galgame"
|
||||
alt="KUN Visual Novel 鲲 Galgame"
|
||||
/>
|
||||
<span>{{ $tm('header.name') }}</span>
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<!-- Navigation bar -->
|
||||
<div class="top-bar">
|
||||
<!-- Top individual sections -->
|
||||
<span v-for="kun in topBarItem" :key="kun.index">
|
||||
<RouterLink :to="{ path: kun.router }">
|
||||
{{ $tm(`header.${kun.name}`) }}
|
||||
{{ $tm(`header['${kun.name}']`) }}
|
||||
</RouterLink>
|
||||
</span>
|
||||
|
||||
<!-- Hover effect under the top section -->
|
||||
<div class="box"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kungalgamer-info">
|
||||
<span class="search" @click="isShowSearch = true">
|
||||
<Icon icon="line-md:search" />
|
||||
</span>
|
||||
|
||||
<!-- showKUNGalgamePanel is a boolean value in the store, true/false controls the display and close of the settings panel -->
|
||||
<span
|
||||
class="settings"
|
||||
@click="showKUNGalgamePanel = !showKUNGalgamePanel"
|
||||
|
@ -125,6 +135,7 @@ onBeforeRouteLeave(() => {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
/* 相对于设置面板定位 */
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-bottom: 7px;
|
||||
|
@ -236,16 +247,6 @@ $navNumber: v-bind(navItemNum);
|
|||
align-items: center;
|
||||
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 {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
@ -288,20 +289,6 @@ $navNumber: v-bind(navItemNum);
|
|||
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) {
|
||||
.kungalgame {
|
||||
span {
|
||||
|
@ -311,13 +298,12 @@ $navNumber: v-bind(navItemNum);
|
|||
margin-right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.settings {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.settings {
|
||||
display: none !important;
|
||||
}
|
||||
.top-bar {
|
||||
display: none;
|
||||
}
|
||||
|
@ -332,11 +318,5 @@ $navNumber: v-bind(navItemNum);
|
|||
.kungalgamer-info {
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
img {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -5,11 +5,13 @@ import { useRouter } from 'vue-router'
|
|||
// Global message component (top)
|
||||
import Message from '@/components/alert/Message'
|
||||
// Global message component (bottom)
|
||||
import { useTempMessageStore } from '@/store/temp/message'
|
||||
import { useKUNGalgameMessageStore } from '@/store/modules/message'
|
||||
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
|
||||
import { storeToRefs } from 'pinia'
|
||||
// Reset store
|
||||
import { kungalgameStoreReset } from '@/store'
|
||||
// Reset router
|
||||
import { resetRouter } from '@/router'
|
||||
|
||||
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.
|
||||
const logOut = async () => {
|
||||
// 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) {
|
||||
kungalgameStoreReset()
|
||||
router.push('/login')
|
||||
resetRouter()
|
||||
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">
|
||||
import { ref, watch } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
|
||||
import { useTempMessageStore } from '@/store/temp/message'
|
||||
import { storeToRefs } from 'pinia'
|
||||
// Using global notifications
|
||||
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.
|
||||
const props = defineProps<{
|
||||
email: string
|
||||
isSendCode: boolean
|
||||
}>()
|
||||
|
||||
const { isCaptureSuccessful } = storeToRefs(useTempMessageStore())
|
||||
const info = useTempMessageStore()
|
||||
|
||||
const isSendCode = ref(false)
|
||||
const isSending = ref(false)
|
||||
|
||||
const countdown = ref(0)
|
||||
|
||||
watch(
|
||||
() => isSendCode.value,
|
||||
async () => {
|
||||
if (!isSending.value) {
|
||||
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 sendCode = () => {
|
||||
// If the parent component passes a false value, return directly
|
||||
if (!props.isSendCode) {
|
||||
return
|
||||
}
|
||||
)
|
||||
|
||||
const handleSendCode = () => {
|
||||
if (isCaptureSuccessful.value) {
|
||||
isSendCode.value = !isSendCode.value
|
||||
if (!isSending.value) {
|
||||
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
|
||||
useKUNGalgameUserStore().sendCode(props.email)
|
||||
|
||||
info.info(tm('AlertInfo.code.code'))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button @click="handleSendCode" :disabled="isSending">
|
||||
<button @click="sendCode" :disabled="isSending">
|
||||
{{ isSending ? countdown : $tm('login.register.send') }}
|
||||
</button>
|
||||
</template>
|
||||
|
|
|
@ -4,15 +4,12 @@
|
|||
|
||||
import { type App } from 'vue'
|
||||
// Directive for enlarging images on click
|
||||
// import { zoom } from './zoom/zoom'
|
||||
import { zoom } from './zoom/zoom'
|
||||
// Permission directive
|
||||
// import { permission } from './permission/permission'
|
||||
// Tooltip directive
|
||||
import { tooltip } from './tooltip/tooltip'
|
||||
import { permission } from './permission/permission'
|
||||
|
||||
// Mount directives
|
||||
export function setupKUNGalgameDirectives(app: App) {
|
||||
// app.directive('zoom', zoom)
|
||||
// app.directive('permission', permission)
|
||||
app.directive('tooltip', tooltip)
|
||||
app.directive('zoom', zoom)
|
||||
app.directive('permission', permission)
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import type { Directive, DirectiveBinding } from 'vue'
|
||||
import Message from '@/components/alert/Message'
|
||||
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { currentUserInfo } from '@/utils/getCurrentUserInfo'
|
||||
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
|
||||
enum UserRole {
|
||||
|
@ -38,17 +40,17 @@ const handleUnauthorizedAccess = (element: HTMLElement) => {
|
|||
|
||||
export const permission: Directive = {
|
||||
mounted(element: HTMLElement, binding: DirectiveBinding<BindingProps>) {
|
||||
const bindingRoles = [...binding.value.roles]
|
||||
const bindingUid = binding.value.uid
|
||||
const roles = [...binding.value.roles]
|
||||
const uid = binding.value.uid
|
||||
|
||||
const hasPermission = () => {
|
||||
// User Self
|
||||
if (bindingUid === uid.value) {
|
||||
if (uid === currentUserUid) {
|
||||
return true
|
||||
}
|
||||
|
||||
// User has access permission
|
||||
if (bindingRoles.includes(roles.value)) {
|
||||
if (roles.includes(currentUserRoles)) {
|
||||
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> = {
|
||||
// User Part
|
||||
10101: `User not found (-B)`,
|
||||
10102: `User password error (-B)`,
|
||||
10103: `Email verification code error (-B)`,
|
||||
10104: `Email is already registered, please change it (-B)`,
|
||||
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)`,
|
||||
10101: 'User not found',
|
||||
10102: 'User password error',
|
||||
10103: 'Email verification code error',
|
||||
10104: 'Email is already registered, please change it',
|
||||
10105: 'Username is already registered, please change it',
|
||||
|
||||
// Topic Part
|
||||
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 (-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)`,
|
||||
10201: 'Your daily topic limit has been reached for today.',
|
||||
10202: `Your moemoepoints are less than 1100, so you can't use the topic suggestion feature`,
|
||||
}
|
||||
|
||||
const errorMessagesCN: Record<number, string> = {
|
||||
10101: `用户未找到 (-B)`,
|
||||
10102: `用户密码错误 (-B)`,
|
||||
10103: `邮箱验证码错误 (-B)`,
|
||||
10104: `邮箱已被注册,请更改 (-B)`,
|
||||
10105: `用户名已被注册,请修改 (-B)`,
|
||||
10106: `用户签名过长 (-B)`,
|
||||
10107: `非法的邮箱, 用户名, 密码, 或验证码 (-B)`,
|
||||
10108: `非法的密码格式 (-B)`,
|
||||
10109: `非法的邮箱或验证码格式 (-B)`,
|
||||
10110: `头像上传错误. 图片为数组 (-B)`,
|
||||
10111: `头像上传错误. 图片最终压缩大小超过 50kb (-B)`,
|
||||
10112: `登陆冷却中,两次相同登陆时间间隔一分钟 (-B)`,
|
||||
10113: `注册冷却中,两次相同注册时间间隔一分钟 (-B)`,
|
||||
10101: '用户未找到',
|
||||
10102: '用户密码错误',
|
||||
10103: '邮箱验证码错误',
|
||||
10104: '邮箱已被注册,请更改',
|
||||
10105: '用户名已被注册,请修改',
|
||||
|
||||
10201: `您今日可以发表的话题数已达上限 (-B)`,
|
||||
10202: `您的萌萌点不足 1100, 无法使用推话题功能 (-B)`,
|
||||
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)`,
|
||||
10201: '您今日可以发表的话题数已达上限',
|
||||
10202: '您的萌萌点不足 1100, 无法使用推话题功能',
|
||||
}
|
||||
|
||||
export const getErrorMessageEN = (errorCode: number) => {
|
||||
return errorMessagesEN[errorCode] || `Unknown server error (-B)`
|
||||
return errorMessagesEN[errorCode] || 'Unknown server error'
|
||||
}
|
||||
|
||||
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 { generateTokenByRefreshTokenApi } from '@/api'
|
||||
// Use the user store
|
||||
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
|
||||
// Import the router
|
||||
import router from '@/router'
|
||||
// Import known error handling functions
|
||||
import { kungalgameErrorHandler } from './errorHandler'
|
||||
|
||||
interface ErrorResponseData {
|
||||
|
@ -13,15 +18,27 @@ interface ErrorResponseData {
|
|||
* Then identifies errors based on custom backend status codes
|
||||
* 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) {
|
||||
Message(
|
||||
'Login expired, please log in again.',
|
||||
'登陆过期,请重新登陆',
|
||||
'error'
|
||||
)
|
||||
useKUNGalgameUserStore().removeToken()
|
||||
router.push('/login')
|
||||
// Attempt to obtain a new token using the refresh token
|
||||
const accessTokenResponse = await generateTokenByRefreshTokenApi()
|
||||
|
||||
// If a new token is successfully obtained, set the token
|
||||
if (accessTokenResponse.code === 200 && accessTokenResponse.data.token) {
|
||||
useKUNGalgameUserStore().setToken(accessTokenResponse.data.token)
|
||||
// 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
|
||||
}
|
||||
|
||||
|
@ -34,6 +51,8 @@ export const onRequestError = async (response: Response) => {
|
|||
return
|
||||
}
|
||||
|
||||
// Get the error response data
|
||||
const data: ErrorResponseData = await response.json()
|
||||
// Handle known errors
|
||||
kungalgameErrorHandler(data.code)
|
||||
}
|
||||
|
|
|
@ -10,8 +10,8 @@ const { showKUNGalgameBackground, showKUNGalgameCustomBackground } =
|
|||
|
||||
// Fetch background image data from the backend
|
||||
const fetchGetBackground = async (imageName: string): Promise<Blob> => {
|
||||
const baseUrl = import.meta.env.VITE_API_UPLOADS_URL
|
||||
const url = `/image/bg/${imageName}.webp`
|
||||
const baseUrl = import.meta.env.VITE_API_BASE_URL
|
||||
const url = `/uploads/image/bg/${imageName}.webp`
|
||||
const fullUrl = `${baseUrl}${url}`
|
||||
const response = await fetch(fullUrl, {
|
||||
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',
|
||||
about: 'ABOUT',
|
||||
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: {
|
||||
name: 'Settings',
|
||||
mode: 'Mode',
|
||||
|
@ -33,7 +18,8 @@ export default {
|
|||
background: 'Background Setting',
|
||||
preset: 'Use our preset background',
|
||||
custom: 'Custom Background',
|
||||
confirm: 'Confirm',
|
||||
url: 'Paste the picture url here',
|
||||
confirm: 'confirm',
|
||||
restore: 'Restore blank background',
|
||||
recover: 'Recover all settings to default',
|
||||
},
|
||||
|
@ -44,16 +30,14 @@ export default {
|
|||
},
|
||||
back: {
|
||||
back: 'Back',
|
||||
home: 'Home',
|
||||
},
|
||||
mainPage: {
|
||||
header: {
|
||||
filter: 'Filter',
|
||||
category: 'Category',
|
||||
galgame: 'Visual Novel',
|
||||
technique: 'Technique',
|
||||
others: 'Others',
|
||||
search: 'Search Topics',
|
||||
all: 'All Topics',
|
||||
history: 'Search History',
|
||||
clear: 'Clear all history',
|
||||
updated: 'Default',
|
||||
time: 'Time',
|
||||
popularity: 'Popularity',
|
||||
|
@ -61,15 +45,10 @@ export default {
|
|||
likes: 'Likes',
|
||||
replies: 'Replies',
|
||||
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: {
|
||||
fold: 'Fold Aside',
|
||||
create: 'CREATE TOPIC',
|
||||
create: 'CREATE NEW!',
|
||||
update: 'Update',
|
||||
balance: 'P & L',
|
||||
ranking: 'Ranking',
|
||||
|
@ -98,7 +77,6 @@ export default {
|
|||
acgngame: 'ACGNGAME',
|
||||
shinnku: `Shinnku's Visual Novel`,
|
||||
ymgal: 'YM galgame',
|
||||
kun: `KUN's Blog`,
|
||||
},
|
||||
describe: {
|
||||
title: 'KUN Visual Novel',
|
||||
|
@ -107,17 +85,19 @@ export default {
|
|||
kun3: 'NO ADs Forever',
|
||||
kun4: 'Free Forever',
|
||||
},
|
||||
contact: 'Contact Us',
|
||||
},
|
||||
},
|
||||
topic: {
|
||||
aside: {
|
||||
floor: 'Sort by Floor Number',
|
||||
like: 'Sort by Likes Count',
|
||||
comment: 'Sort by Comment Count',
|
||||
top: 'Back Top',
|
||||
floorSort: 'Floor Sort',
|
||||
timeSort: 'Time Sort',
|
||||
likeSort: 'Like Sort',
|
||||
commentSort: 'Reply Sort',
|
||||
updatedSort: 'Update Sort',
|
||||
tags: 'Topics Under the Same Tags',
|
||||
tagsEmpty: 'The tags currently has no other topics',
|
||||
master: 'Other Topics of The Master',
|
||||
masterEmpty: 'Master currently has no other topics',
|
||||
},
|
||||
content: {
|
||||
status: 'Topic status',
|
||||
|
@ -272,7 +252,7 @@ export default {
|
|||
supportImage: 'Supports images up to 1007KB',
|
||||
supportFormat: 'Supports jpg and png formats',
|
||||
confirm: 'Confirm',
|
||||
bio: 'Change Bio (Up to 107 characters)',
|
||||
bio: 'Change Bio',
|
||||
hint: 'Please enter your new signature, up to 107 characters',
|
||||
count: 'Character count',
|
||||
},
|
||||
|
@ -309,8 +289,6 @@ export default {
|
|||
agreement: 'User Agreement',
|
||||
privacy: 'Privacy',
|
||||
redirect: 'Redirect',
|
||||
kungalgame403: '403 Permission Denied',
|
||||
kungalgame404: '404 Not Found',
|
||||
|
||||
home: 'Home',
|
||||
balance: 'P & L',
|
||||
|
@ -365,18 +343,6 @@ export default {
|
|||
donate: 'Donate Us',
|
||||
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 Us',
|
||||
no: 'comes with no Moemoepoint rewards',
|
||||
|
@ -397,11 +363,6 @@ export default {
|
|||
success: 'Login successful',
|
||||
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: {
|
||||
confirm: 'OK',
|
||||
|
@ -411,12 +372,14 @@ export default {
|
|||
edit: {
|
||||
publish: 'Confirm to publish?',
|
||||
publishSuccess: 'Publish Successfully',
|
||||
publishCancel: 'Cancel Publish',
|
||||
upvoteTopic:
|
||||
'Are you sure you want to upvote this topic? This will cost you 17 Moe Moe Points',
|
||||
upvoteReply:
|
||||
'Are you sure you want to upvote this reply? This will cost you 3 Moe Moe Points',
|
||||
rewrite: 'Confirm to Rewrite?',
|
||||
rewriteSuccess: 'Rewrite Successfully',
|
||||
rewriteCancel: 'Cancel Rewrite',
|
||||
closePanel: 'Confirm closing the panel? Your changes will not be saved.',
|
||||
draft: 'The draft has been saved successfully!',
|
||||
leave: 'Confirm leaving the page? Your changes will not be saved.',
|
||||
|
@ -425,10 +388,8 @@ export default {
|
|||
login: {
|
||||
invalidUsername:
|
||||
'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 {'@'}!#$%^&*()-+=`,
|
||||
invalidCode:
|
||||
'Invalid email verification code format. The email verification code must consist of 7 digits or letters.',
|
||||
success: 'Login Successfully! Welcome to KUN Visual Novel',
|
||||
invalidPassword:
|
||||
'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!@#$%^&()-+=',
|
||||
},
|
||||
capture: {
|
||||
title: 'Answer Question(s)',
|
||||
|
|
|
@ -6,21 +6,6 @@ export default {
|
|||
technique: '技术交流',
|
||||
about: '关于我们',
|
||||
return: '返回主页',
|
||||
hamburger: {
|
||||
name: '鲲 Galgame',
|
||||
pool: '所有话题',
|
||||
create: '发布话题',
|
||||
technique: '技术交流',
|
||||
about: '关于我们',
|
||||
ranking: '排行榜单',
|
||||
update: '更新日志',
|
||||
bylaw: '执行条例',
|
||||
balance: '收支公示',
|
||||
nonMoe: '不萌记录',
|
||||
thanks: '感谢名单',
|
||||
join: '加入 / 联系',
|
||||
home: '返回主页',
|
||||
},
|
||||
settings: {
|
||||
name: '设置面板',
|
||||
mode: '模式切换',
|
||||
|
@ -33,6 +18,7 @@ export default {
|
|||
background: '背景设置',
|
||||
preset: '点击使用我们预设的背景',
|
||||
custom: '自定义背景',
|
||||
url: '请在这里粘贴图片的URL',
|
||||
confirm: '确定',
|
||||
restore: '恢复空白背景',
|
||||
recover: '恢复所有设置为默认',
|
||||
|
@ -44,16 +30,14 @@ export default {
|
|||
},
|
||||
back: {
|
||||
back: '返回',
|
||||
home: '主页',
|
||||
},
|
||||
mainPage: {
|
||||
header: {
|
||||
filter: '筛选',
|
||||
category: '分类',
|
||||
galgame: 'Galgame',
|
||||
technique: '技术交流',
|
||||
others: '其它',
|
||||
search: '搜索话题',
|
||||
all: '全部话题',
|
||||
history: '搜索历史',
|
||||
clear: '清除所有历史',
|
||||
updated: '恢复默认排序',
|
||||
time: '按照时间排序',
|
||||
popularity: '按热度值排序',
|
||||
|
@ -61,11 +45,6 @@ export default {
|
|||
likes: '按点赞数排序',
|
||||
replies: '按回复数排序',
|
||||
comments: '按评论数排序',
|
||||
search: '输入内容以自动搜索',
|
||||
history: '搜索历史',
|
||||
clear: '清除所有历史',
|
||||
emptyHistory: '这只萝莉什么也没搜索过',
|
||||
emptyResult: '什么也没有搜索到。。。',
|
||||
},
|
||||
asideActive: {
|
||||
fold: '折叠左侧区域',
|
||||
|
@ -98,7 +77,6 @@ export default {
|
|||
acgngame: 'ACGNGAME',
|
||||
shinnku: '失落的小站',
|
||||
ymgal: '月幕 galgame',
|
||||
kun: '鲲的博客',
|
||||
},
|
||||
describe: {
|
||||
title: '鲲 Galgame',
|
||||
|
@ -107,17 +85,19 @@ export default {
|
|||
kun3: '鲲 Galgame 永远不会有广告',
|
||||
kun4: '鲲 Galgame 永远不会收费',
|
||||
},
|
||||
contact: '联系我们',
|
||||
},
|
||||
},
|
||||
topic: {
|
||||
aside: {
|
||||
floor: '按照楼层数排序',
|
||||
like: '按照点赞数排序',
|
||||
comment: '按照评论数排序',
|
||||
top: '返回到顶端',
|
||||
floorSort: '按楼层排序',
|
||||
timeSort: '按时间排序',
|
||||
likeSort: '按点赞排序',
|
||||
commentSort: '按评论排序',
|
||||
updatedSort: '按更新排序',
|
||||
tags: '相同标签下的其它话题',
|
||||
tagsEmpty: '该标签下暂无其它话题',
|
||||
master: '楼主的其它话题',
|
||||
masterEmpty: '楼主暂无其它话题',
|
||||
},
|
||||
content: {
|
||||
status: '话题状态',
|
||||
|
@ -271,7 +251,7 @@ export default {
|
|||
supportImage: '支持 1007KB 以内的图片',
|
||||
supportFormat: '支持 jpg 和 png 格式',
|
||||
confirm: '确定更改',
|
||||
bio: '更改签名 (107 字之内)',
|
||||
bio: '更改签名',
|
||||
hint: '输入您的新签名,最大 107 个字符',
|
||||
count: '字数',
|
||||
},
|
||||
|
@ -308,8 +288,6 @@ export default {
|
|||
agreement: '用户协议',
|
||||
privacy: '隐私政策',
|
||||
redirect: '重定向',
|
||||
kungalgame403: '403 无权访问',
|
||||
kungalgame404: '404 页面未找到',
|
||||
|
||||
home: '主页',
|
||||
balance: '收支公示',
|
||||
|
@ -364,18 +342,6 @@ export default {
|
|||
donate: '赞助我们',
|
||||
home: '返回主页',
|
||||
},
|
||||
pool: {
|
||||
load: '点击继续加载话题',
|
||||
complete: '已经。。。一滴也不剩了',
|
||||
view: '按浏览数排序',
|
||||
like: '按点赞数排序',
|
||||
time: '按照时间排序',
|
||||
},
|
||||
technique: {
|
||||
prev: '上一页',
|
||||
next: '下一页',
|
||||
KKKKK: `我们不知道这个页面怎么写了,如果有建议,请联系我们`,
|
||||
},
|
||||
donate: {
|
||||
donate: '赞助我们',
|
||||
no: '没有任何的萌萌点奖励',
|
||||
|
@ -395,11 +361,6 @@ export default {
|
|||
success: '登陆成功',
|
||||
home: '3 秒后你将会进入主页',
|
||||
},
|
||||
footer: {
|
||||
copyright: '版权所有 © 2023 鲲 Galgame (图片除外)',
|
||||
openSource: 'GitHub 开源',
|
||||
reserved: '保留所有权利 | 版本',
|
||||
},
|
||||
// 非页面组件这里统一用大驼峰
|
||||
ComponentAlert: {
|
||||
confirm: '确定',
|
||||
|
@ -409,10 +370,12 @@ export default {
|
|||
edit: {
|
||||
publish: '确认发布吗?',
|
||||
publishSuccess: '发布成功',
|
||||
publishCancel: '取消发布',
|
||||
upvoteTopic: '您确定推这个话题吗,这将会消耗您 17 萌萌点',
|
||||
upvoteReply: '您确定推这个回复吗,这将会消耗您 3 萌萌点',
|
||||
rewrite: '确认 Rewrite 吗?',
|
||||
rewriteSuccess: 'Rewrite 成功',
|
||||
rewriteCancel: '取消 Rewrite',
|
||||
closePanel: '确认关闭面板吗?您的更改将不会被保存',
|
||||
draft: '草稿已经保存成功!',
|
||||
leave: '确认离开界面吗?您的更改将不会保存',
|
||||
|
@ -421,9 +384,8 @@ export default {
|
|||
login: {
|
||||
invalidUsername:
|
||||
'非法的用户名,用户名为 1 到 17 位,可以包含:中文、英文、数字、下划线、波浪线',
|
||||
invalidPassword: `非法的密码格式,密码的长度为 6 到 107 位,必须包含至少一个英文字符和一个数字,可以选择性的包含 {'@'}!#$%^&*()-+= 等特殊字符`,
|
||||
invalidCode: '非法的邮箱验证码格式,邮箱验证码必须为 7 位数字或字母',
|
||||
success: '登陆成功! 欢迎来到鲲 Galgame! ',
|
||||
invalidPassword:
|
||||
'非法的密码格式,密码的长度为 6 到 17 位,必须包含至少一个英文字符和一个数字,可以选择性的包含 \\w!@#$%^&*()-+= 等特殊字符',
|
||||
},
|
||||
capture: {
|
||||
title: '请回答下面的问题',
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, watch, ref } from 'vue'
|
||||
// Import animations
|
||||
import 'animate.css'
|
||||
import { getCurrentBackground } from '@/hooks/useBackgroundPicture'
|
||||
import KUNGalgameTopBar from '@/components/top-bar/KUNGalgameTopBar.vue'
|
||||
|
||||
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { getImage } from '@/hooks/useLocalforage'
|
||||
|
||||
const { showKUNGalgameBackground, showKUNGalgameCustomBackground } =
|
||||
storeToRefs(useKUNGalgameSettingsStore())
|
||||
|
@ -14,12 +14,6 @@ const { showKUNGalgameBackground, showKUNGalgameCustomBackground } =
|
|||
const imageURL = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
const backgroundImageBlobData = await getImage('kun-galgame-custom-bg')
|
||||
if (showKUNGalgameBackground.value === 'bg1007' && backgroundImageBlobData) {
|
||||
showKUNGalgameCustomBackground.value = URL.createObjectURL(
|
||||
backgroundImageBlobData
|
||||
)
|
||||
}
|
||||
imageURL.value = await getCurrentBackground()
|
||||
})
|
||||
|
||||
|
@ -37,7 +31,7 @@ watch(
|
|||
<div class="top-bar">
|
||||
<KUNGalgameTopBar />
|
||||
</div>
|
||||
|
||||
<!-- <RouterView /> -->
|
||||
<RouterView #default="{ route, Component }">
|
||||
<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 App from './App.vue'
|
||||
import router from './router'
|
||||
// Import vue i18n
|
||||
import i18n from '@/language/i18n'
|
||||
|
||||
import { setupKUNGalgameRouterGuard } from '@/router/guard'
|
||||
import { setupKUNGalgamePinia } from '@/store/index'
|
||||
import { setupKUNGalgameDirectives } from './directives'
|
||||
import { setupRouterGuard } from '@/router/guard'
|
||||
import { setupPinia } from '@/store/index'
|
||||
|
||||
// Import css styles, color, theme, etc.
|
||||
import '@/styles/index.scss'
|
||||
|
||||
// Get vue App instance
|
||||
const app = createApp(App)
|
||||
|
||||
setupKUNGalgameRouterGuard(router)
|
||||
setupKUNGalgamePinia(app)
|
||||
setupKUNGalgameDirectives(app)
|
||||
// Setup router guard
|
||||
setupRouterGuard(router)
|
||||
// Setup pinia
|
||||
setupPinia(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)
|
||||
createPageTitle(router)
|
||||
}
|
||||
|
|
|
@ -1,56 +1,53 @@
|
|||
// Import rooter
|
||||
import { Router } from 'vue-router'
|
||||
// Import public routes that do not require authentication
|
||||
import { whiteList } from '../router'
|
||||
// Use user store
|
||||
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
// Progress bar
|
||||
import NProgress from 'nprogress'
|
||||
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
|
||||
NProgress.configure({ showSpinner: false })
|
||||
|
||||
export const createPermission = (router: Router) => {
|
||||
router.beforeEach(async (to, from) => {
|
||||
// Start NProgress
|
||||
NProgress.start()
|
||||
|
||||
// Get the current token, access token;
|
||||
// refresh token is stored on the server as HttpOnly
|
||||
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)
|
||||
// Get the required permissions for the target route
|
||||
const requiredPermissions = to.meta.permission
|
||||
? (to.meta.permission as number[])
|
||||
: [1, 2, 3, 4]
|
||||
|
||||
// If there is no token and it's not in the whitelist
|
||||
// , redirect to the login page
|
||||
if (!token && !isInWhitelist) {
|
||||
// Redirect other pages without access to the login page
|
||||
NProgress.done()
|
||||
return { name: 'Login' }
|
||||
return '/login'
|
||||
}
|
||||
|
||||
// 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 (currentPageUid === uid.value) {
|
||||
return 4
|
||||
} else {
|
||||
return roles.value
|
||||
if (!requiredPermissions.includes(currentUserRole)) {
|
||||
// If it's a user interface, redirect to 'info';
|
||||
// Otherwise, redirect to '403'
|
||||
return to.matched[0].path === '/kungalgamer'
|
||||
? `/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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { type RouteRecordRaw, createWebHistory, createRouter } from 'vue-router'
|
||||
import { constantRoutes } from './router'
|
||||
import { constantRoutes, whiteList } from './router'
|
||||
import { asyncRoutes } from './router'
|
||||
|
||||
// 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
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { type RouteRecordRaw } from 'vue-router'
|
||||
// Current user's information
|
||||
import { currentUserInfo } from '@/utils/getCurrentUserInfo'
|
||||
|
||||
const Layout = () => import('@/layout/KUNGalgameAPP.vue')
|
||||
|
||||
|
@ -10,11 +12,11 @@ const kungalgamer: RouteRecordRaw[] = [
|
|||
path: '/kungalgamer',
|
||||
component: Layout,
|
||||
// Access defaults to the current user's main page
|
||||
// redirect: `/kungalgamer/${uid.value}/info`,
|
||||
redirect: `/kungalgamer/${currentUserInfo.uid}/info`,
|
||||
children: [
|
||||
{
|
||||
path: ':uid',
|
||||
// redirect: `/kungalgamer/${uid.value}/info`,
|
||||
redirect: `/kungalgamer/${currentUserInfo.uid}/info`,
|
||||
component: () => import('@/views/kungalgamer/KUNGalgamer.vue'),
|
||||
children: [
|
||||
{
|
||||
|
|
|
@ -77,7 +77,7 @@ export const constantRoutes: RouteRecordRaw[] = [
|
|||
path: '/:path(.*)*',
|
||||
component: () => import('@/views/404/404.vue'),
|
||||
meta: {
|
||||
title: 'kungalgame404',
|
||||
title: '404',
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -87,11 +87,14 @@ export const constantRoutes: RouteRecordRaw[] = [
|
|||
path: '/kungalgame403',
|
||||
component: () => import('@/views/403/403.vue'),
|
||||
meta: {
|
||||
title: 'kungalgame403',
|
||||
title: '403',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const isArray = (val: any): val is object =>
|
||||
toString.call(val) === '[object Array]'
|
||||
|
||||
// 获取动态路由表
|
||||
const getAsyncRoute = (): RouteRecordRaw[] => {
|
||||
const modules = import.meta.glob('./modules/*.ts', {
|
||||
|
@ -100,7 +103,7 @@ const getAsyncRoute = (): RouteRecordRaw[] => {
|
|||
})
|
||||
const asyncRoute: RouteRecordRaw[] = []
|
||||
Object.values(modules).forEach((value) => {
|
||||
const moduleList = Array.isArray(value)
|
||||
const moduleList = isArray(value)
|
||||
? [...(value as RouteRecordRaw[])]
|
||||
: [value as RouteRecordRaw]
|
||||
asyncRoute.push(...moduleList)
|
||||
|
|
|
@ -6,32 +6,62 @@ import { createPinia } from 'pinia'
|
|||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
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 { usePersistKUNGalgameHomeStore } from './modules/home'
|
||||
|
||||
// Import home store
|
||||
import { useKUNGalgameHomeStore } from './modules/home'
|
||||
|
||||
// Import user store
|
||||
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 { 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()
|
||||
|
||||
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)
|
||||
app.use(store)
|
||||
}
|
||||
|
||||
// Reset all stores, used for logging out
|
||||
export function kungalgameStoreReset() {
|
||||
const balanceStore = useKUNGalgameBalanceStore()
|
||||
const editStore = useKUNGalgameEditStore()
|
||||
const homeStore = usePersistKUNGalgameHomeStore()
|
||||
const homeStore = useKUNGalgameHomeStore()
|
||||
const userStore = useKUNGalgameUserStore()
|
||||
const messageStore = useKUNGalgameMessageStore()
|
||||
const nonMoeStore = useKUNGalgameNonMoeStore()
|
||||
const rankingStore = useKUNGalgameRankingStore()
|
||||
const settingsStore = useKUNGalgameSettingsStore()
|
||||
const topicStore = usePersistKUNGalgameTopicStore()
|
||||
const replyStore = usePersistKUNGalgameReplyStore()
|
||||
const topicStore = useKUNGalgameTopicStore()
|
||||
|
||||
balanceStore.$reset()
|
||||
editStore.$reset()
|
||||
homeStore.$reset()
|
||||
userStore.$reset()
|
||||
messageStore.$reset()
|
||||
nonMoeStore.$reset()
|
||||
rankingStore.$reset()
|
||||
settingsStore.$reset()
|
||||
topicStore.$reset()
|
||||
replyStore.$reset()
|
||||
}
|
||||
|
||||
export { store }
|
||||
|
|
|
@ -15,8 +15,8 @@ interface BalanceStore {
|
|||
expenditure: BalanceExpenditureRequestData
|
||||
}
|
||||
|
||||
export const useTempBalanceStore = defineStore({
|
||||
id: 'tempBalance',
|
||||
export const useKUNGalgameBalanceStore = defineStore({
|
||||
id: 'KUNGalgameBalance',
|
||||
persist: false,
|
||||
state: (): BalanceStore => ({
|
||||
income: {
|
|
@ -1,21 +1,24 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { postNewTopicApi, getTopTagsApi } from '@/api'
|
||||
import { postNewTopicApi, updateNewTopicApi, getTopTagsApi } from '@/api'
|
||||
import {
|
||||
EditCreateTopicRequestData,
|
||||
EditCreateTopicResponseData,
|
||||
EditUpdateTopicRequestData,
|
||||
EditUpdateTopicResponseData,
|
||||
EditGetHotTagsRequestData,
|
||||
EditGetHotTagsResponseData,
|
||||
} from '@/api'
|
||||
|
||||
import { EditStore } from '../types/edit'
|
||||
import { checkTopicPublish } from '../utils/checkTopicPublish'
|
||||
import type { EditStorePersist } from '../types/edit'
|
||||
|
||||
export const useKUNGalgameEditStore = defineStore({
|
||||
id: 'KUNGalgameEdit',
|
||||
persist: true,
|
||||
state: (): EditStorePersist => ({
|
||||
state: (): EditStore => ({
|
||||
editorHeight: 300,
|
||||
textCount: 0,
|
||||
mode: '',
|
||||
theme: 'snow',
|
||||
|
||||
title: '',
|
||||
content: '',
|
||||
|
@ -23,6 +26,16 @@ export const useKUNGalgameEditStore = defineStore({
|
|||
category: [],
|
||||
isShowHotKeywords: true,
|
||||
isSaveTopic: false,
|
||||
|
||||
topicRewrite: {
|
||||
tid: 0,
|
||||
title: '',
|
||||
content: '',
|
||||
tags: [],
|
||||
category: [],
|
||||
|
||||
isTopicRewriting: false,
|
||||
},
|
||||
}),
|
||||
getters: {},
|
||||
actions: {
|
||||
|
@ -44,6 +57,24 @@ export const useKUNGalgameEditStore = defineStore({
|
|||
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
|
||||
async getHotTags(limit: number): Promise<EditGetHotTagsResponseData> {
|
||||
const requestData: EditGetHotTagsRequestData = { limit }
|
||||
|
@ -53,7 +84,6 @@ export const useKUNGalgameEditStore = defineStore({
|
|||
// Reset topic draft data for publishing
|
||||
resetTopicData() {
|
||||
this.textCount = 0
|
||||
|
||||
this.title = ''
|
||||
this.content = ''
|
||||
this.tags = []
|
||||
|
@ -61,5 +91,16 @@ export const useKUNGalgameEditStore = defineStore({
|
|||
|
||||
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 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',
|
||||
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,
|
||||
|
||||
// Search history storage
|
||||
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
|
||||
*/
|
||||
import { defineStore } from 'pinia'
|
||||
|
@ -51,8 +52,10 @@ import {
|
|||
updateUserPasswordByEmailApi,
|
||||
} 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({
|
||||
id: 'KUNGalgameUser',
|
||||
persist: true,
|
||||
|
@ -107,6 +110,10 @@ export const useKUNGalgameUserStore = defineStore({
|
|||
res.data.roles
|
||||
)
|
||||
this.setToken(res.data.token)
|
||||
} else if (res.code === 500) {
|
||||
console.log(res.message)
|
||||
} else {
|
||||
throw new Error('500 Server ERROR')
|
||||
}
|
||||
return res
|
||||
},
|
||||
|
|
|
@ -3,8 +3,8 @@ import { defineStore } from 'pinia'
|
|||
// Type of message store
|
||||
import { MessageStore } from '../types/message'
|
||||
|
||||
export const useTempMessageStore = defineStore({
|
||||
id: 'tempMessage',
|
||||
export const useKUNGalgameMessageStore = defineStore({
|
||||
id: 'KUNGalgameMessage',
|
||||
// No need to persist any message components
|
||||
persist: false,
|
||||
state: (): MessageStore => ({
|
|
@ -4,8 +4,8 @@ import type { NonMoeLogRequestData, NonMoeGetLogsResponseData } from '@/api'
|
|||
|
||||
import { getNonMoeLogsApi } from '@/api'
|
||||
|
||||
export const useTempNonMoeStore = defineStore({
|
||||
id: 'tempNonMoe',
|
||||
export const useKUNGalgameNonMoeStore = defineStore({
|
||||
id: 'KUNGalgameNonMoe',
|
||||
persist: false,
|
||||
state: (): NonMoeLogRequestData => ({
|
||||
page: 1,
|
|
@ -14,8 +14,8 @@ interface RankingStore {
|
|||
user: RankingGetUserRequestData
|
||||
}
|
||||
|
||||
export const useTempRankingStore = defineStore({
|
||||
id: 'tempRanking',
|
||||
export const useKUNGalgameRankingStore = defineStore({
|
||||
id: 'KUNGalgameRanking',
|
||||
persist: false,
|
||||
state: (): RankingStore => ({
|
||||
topic: {
|
|
@ -32,7 +32,7 @@ export const useKUNGalgameSettingsStore = defineStore({
|
|||
actions: {
|
||||
// Set the theme, there are only two modes
|
||||
// , light and dark, with light represented as ''
|
||||
setKUNGalgameTheme(theme: '' | 'dark') {
|
||||
setKUNGalgameTheme(theme: string) {
|
||||
this.showKUNGalgameMode = 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