Compare commits

...

No commits in common. "V1.2" and "V1" have entirely different histories.
V1.2 ... V1

230 changed files with 6931 additions and 9884 deletions

View file

@ -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'

View file

@ -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.

View file

@ -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.

View file

@ -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
View file

@ -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>.

View file

@ -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).

View file

@ -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>

View file

@ -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"
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,2 +0,0 @@
User-agent: *
Allow: /

View file

@ -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>

View file

@ -17,7 +17,6 @@ export interface EditUpdateTopicRequestData {
content: string
tags: string[]
category: string[]
edited: number
}
// Request data format for getting hot tags

View file

@ -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

View file

@ -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[]>

View file

@ -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'

View file

@ -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
}

View file

@ -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[]>

View file

@ -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
}

View file

@ -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[]
>

View file

@ -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`

View file

@ -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
}

View file

@ -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

View file

@ -37,7 +37,6 @@ export interface TopicDetail {
export interface TopicUpvoteTopicRequestData {
tid: number
to_uid: number
time: number
}
// Request data for liking a topic

View file

@ -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
}
]

View file

@ -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>

View file

@ -5,6 +5,7 @@ const router = useRouter()
</script>
<template>
<!-- Back to homepage -->
<div class="return" @click="router.back()">
<span>{{ `< ${$tm('back.back')}` }}</span>
</div>

View file

@ -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>

View 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>

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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);

View file

@ -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;

View 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>

View 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>

View file

@ -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>

View file

@ -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>

View 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,
},
},
]

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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;
}

View file

@ -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>

View file

@ -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>

View file

@ -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);
}

View file

@ -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;

View file

@ -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%;

View file

@ -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);

View file

@ -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;

View file

@ -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 千恋*万花 千恋*万花',
},
]

View file

@ -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,
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View 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>

View file

@ -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>

View file

@ -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>

View file

@ -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')
}
}

View file

@ -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' },
]

View file

@ -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>

View file

@ -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)
}

View file

@ -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
}

View file

@ -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)
},
}

View file

@ -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] || '未知的服务器错误'
}

View file

@ -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)
}

View file

@ -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
View 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],
}
}

View file

@ -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)',

View file

@ -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: '请回答下面的问题',

View file

@ -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}`"

View file

@ -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')

View file

@ -10,7 +10,7 @@ const createPageTitle = (router: Router) => {
})
}
export function setupKUNGalgameRouterGuard(router: Router) {
export function setupRouterGuard(router: Router) {
createPermission(router)
createPageTitle(router)
}

View file

@ -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

View file

@ -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

View file

@ -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: [
{

View file

@ -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)

View file

@ -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 }

View file

@ -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: {

View file

@ -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
},
},
})

View file

@ -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
},
},
})

View file

@ -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
},

View file

@ -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 => ({

View file

@ -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,

View file

@ -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: {

View file

@ -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
View 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
},
},
})

View file

@ -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