change: remove SSR

This commit is contained in:
KUN1007 2023-11-07 22:28:42 +08:00
parent 8c4e97c6f1
commit c2e3002bbc
33 changed files with 400 additions and 1523 deletions

View file

@ -16,7 +16,6 @@
"Codepen", "Codepen",
"commonmark", "commonmark",
"cout", "cout",
"crossorigin",
"dompurify", "dompurify",
"fontawesome", "fontawesome",
"galgame", "galgame",
@ -62,7 +61,6 @@
"non-moe", "non-moe",
"nord", "nord",
"nprogress", "nprogress",
"nuxt",
"okaidia", "okaidia",
"Otome", "Otome",
"Owaru", "Owaru",

View file

@ -10,18 +10,12 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta <meta
name="description" name="description"
content="KUN Visual Novel | 鲲 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 | 鲲 Galgame</title> <title>KUNGalgame</title>
<!--preload-links-->
</head> </head>
<body> <body>
<div id="app"><!--ssr-outlet--></div> <div id="app"></div>
<div id="teleported"><!--teleports--></div> <script type="module" src="/src/main.ts"></script>
<script type="module" src="/src/entry-client.ts"></script>
<script>
window.__INITIAL_STATE__ = '__pinia'
</script>
</body> </body>
</html> </html>

View file

@ -13,11 +13,9 @@
}, },
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "ts-node-esm server-dev.ts", "dev": "vite",
"prod": "ts-node-esm server-prod.ts", "build": "vue-tsc && vite build",
"build:client": "vite build --outDir dist/client --ssrManifest", "preview": "vite preview"
"build:server": "vite build --outDir dist/server --ssr src/entry-server.ts",
"build": "pnpm build:client && pnpm build:server"
}, },
"dependencies": { "dependencies": {
"@milkdown/core": "^7.3.1", "@milkdown/core": "^7.3.1",
@ -36,12 +34,10 @@
"@milkdown/utils": "^7.3.1", "@milkdown/utils": "^7.3.1",
"@milkdown/vue": "^7.3.1", "@milkdown/vue": "^7.3.1",
"@prosemirror-adapter/vue": "^0.2.6", "@prosemirror-adapter/vue": "^0.2.6",
"@vue/server-renderer": "^3.3.7",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"dompurify": "^3.0.6", "dompurify": "^3.0.6",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"node-fetch": "^3.3.2",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.0", "pinia-plugin-persistedstate": "^3.2.0",
@ -52,22 +48,13 @@
}, },
"devDependencies": { "devDependencies": {
"@iconify/vue": "^4.1.1", "@iconify/vue": "^4.1.1",
"@koa/router": "^12.0.1",
"@nuxt/devalue": "^2.0.2",
"@types/dompurify": "^3.0.4", "@types/dompurify": "^3.0.4",
"@types/js-cookie": "^3.0.5", "@types/js-cookie": "^3.0.5",
"@types/koa": "^2.13.10",
"@types/koa-send": "^4.1.5",
"@types/node": "^20.8.10", "@types/node": "^20.8.10",
"@types/nprogress": "^0.2.2", "@types/nprogress": "^0.2.2",
"@vitejs/plugin-vue": "^4.4.0", "@vitejs/plugin-vue": "^4.4.0",
"express": "^4.18.2",
"koa": "^2.14.2",
"koa-connect": "^2.1.0",
"koa-send": "^5.0.1",
"rollup-plugin-visualizer": "^5.9.2", "rollup-plugin-visualizer": "^5.9.2",
"sass": "^1.69.5", "sass": "^1.69.5",
"ts-node": "^10.9.1",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^4.5.0", "vite": "^4.5.0",
"vue-tsc": "^1.8.22" "vue-tsc": "^1.8.22"

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,96 +0,0 @@
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import Koa from 'koa'
import koaConnect from 'koa-connect'
import { createServer as createViteServer } from 'vite'
const HOST_NAME = '127.0.0.1'
const APP_PORT = 1007
const __dirname = path.dirname(fileURLToPath(import.meta.url))
// Inject teleports in template
const injectTeleports = (
html: string,
teleports: {
'#teleported': string
}
) => {
// return html
return html.replace('<!--teleports-->', teleports['#teleported'])
}
;(async (hmrPort) => {
const app = new Koa()
const vite = await createViteServer({
server: {
middlewareMode: true,
watch: {
// During tests we edit the files too fast and sometimes chokidar
// misses change events, so enforce polling for consistency
usePolling: true,
interval: 107,
},
hmr: {
port: hmrPort,
},
},
appType: 'custom',
})
// Parse accept-language
const parseAcceptLanguage = (acceptLanguage: string) => {
const languages = acceptLanguage.split(',')
const language = languages[0]
const country = language.split('-')[1]
return { language, country }
}
app.use(koaConnect(vite.middlewares))
app.use(async (ctx) => {
const { language, country } = parseAcceptLanguage(
ctx.request.headers['accept-language'] as string
)
try {
let template = fs.readFileSync(
path.resolve(__dirname, 'index.html'),
'utf-8'
)
template = await vite.transformIndexHtml(ctx.path, template)
const { render } = await vite.ssrLoadModule('/src/entry-server.ts')
const [renderedHtml, renderedPinia, renderedLinks, renderedTeleports] =
await render(
ctx,
{},
{
language,
country,
}
)
const html = injectTeleports(template, renderedTeleports)
.replace('<!--preload-links-->', renderedLinks)
.replace('<!--ssr-outlet-->', renderedHtml)
.replace('__pinia', renderedPinia)
ctx.type = 'text/html'
ctx.body = html
} catch (e) {
vite && vite.ssrFixStacktrace(e as Error)
ctx.throw(500, e as Error)
}
})
app.listen(APP_PORT, () => {
console.log(`Server is listening on http://${HOST_NAME}:${APP_PORT}`)
})
})()

View file

@ -1,46 +0,0 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import Koa from 'koa'
import sendFile from 'koa-send'
import path from 'path'
import fs from 'fs'
import { fileURLToPath } from 'url'
const HOST_NAME = '127.0.0.1'
const APP_PORT = 7777
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const resolve = (pathName: string) => path.resolve(__dirname, pathName)
const clientRoot = resolve('dist/client')
const template = fs.readFileSync(resolve('dist/client/index.html'), 'utf-8')
// @ts-ignore
import { render } from './dist/server/entry-server.js'
// @ts-ignore
import manifest from './dist/client/ssr-manifest.json' assert { type: 'json' }
;(async () => {
const app = new Koa()
app.use(async (ctx) => {
if (ctx.path.startsWith('/kun')) {
await sendFile(ctx, ctx.path, { root: clientRoot })
return
}
const [renderedHtml, state, preloadLinks] = await render(ctx, manifest)
const html = template
.replace('<!--preload-links-->', preloadLinks)
.replace('<!--ssr-outlet-->', renderedHtml)
.replace('__pinia', state)
ctx.type = 'text/html'
ctx.body = html
})
app.listen(APP_PORT, () => {
console.log(`Server is listening on http://${HOST_NAME}:${APP_PORT}`)
})
})()

View file

@ -18,9 +18,8 @@ const handleConfirm = () => {
</script> </script>
<template> <template>
<Teleport to="#teleported" :disabled="showAlert"> <Teleport to="body" :disabled="showAlert">
<Transition name="alert"> <Transition name="alert">
<div>
<div v-if="showAlert" class="mask"> <div v-if="showAlert" class="mask">
<div class="container"> <div class="container">
<div class="header"> <div class="header">
@ -37,7 +36,6 @@ const handleConfirm = () => {
</div> </div>
</div> </div>
</div> </div>
</div>
</Transition> </Transition>
</Teleport> </Teleport>
</template> </template>

View file

@ -15,12 +15,11 @@ const handleClose = () => {
</script> </script>
<template> <template>
<Teleport to="#teleported" :disabled="showInfo"> <Teleport to="body" :disabled="showInfo">
<Transition <Transition
enter-active-class="animate__animated animate__fadeInUp animate__faster" enter-active-class="animate__animated animate__fadeInUp animate__faster"
leave-active-class="animate__animated animate__fadeOutDown animate__faster" leave-active-class="animate__animated animate__fadeOutDown animate__faster"
> >
<div>
<div class="container" v-if="showInfo"> <div class="container" v-if="showInfo">
<Transition <Transition
enter-active-class="animate__animated animate__swing" enter-active-class="animate__animated animate__swing"
@ -47,7 +46,6 @@ const handleClose = () => {
<Icon icon="line-md:close" /> <Icon icon="line-md:close" />
</div> </div>
</div> </div>
</div>
</Transition> </Transition>
</Teleport> </Teleport>
</template> </template>

View file

@ -119,9 +119,8 @@ const handleCloseCapture = () => {
</script> </script>
<template> <template>
<Teleport to="#teleported" :disabled="isShowCapture"> <Teleport to="body" :disabled="isShowCapture">
<Transition name="capture"> <Transition name="capture">
<div>
<!-- Mask --> <!-- Mask -->
<div <div
class="mask" class="mask"
@ -183,7 +182,6 @@ const handleCloseCapture = () => {
</div> </div>
</div> </div>
</div> </div>
</div>
</Transition> </Transition>
</Teleport> </Teleport>
</template> </template>

View file

@ -11,6 +11,7 @@ import Message from '@/components/alert/Message'
import { backgroundImages } from './background' import { backgroundImages } from './background'
import { getBackgroundURL } from '@/hooks/useBackgroundPicture' import { getBackgroundURL } from '@/hooks/useBackgroundPicture'
import { restoreBackground } from '@/hooks/useBackgroundPicture'
const imageArray = ref<string[]>([]) const imageArray = ref<string[]>([])
// Use the settings panel store // Use the settings panel store
@ -27,11 +28,6 @@ const handleChangeImage = (index: number) => {
showKUNGalgameBackground.value = `bg${index}` showKUNGalgameBackground.value = `bg${index}`
} }
// Restore the blank background
const restoreBackground = () => {
showKUNGalgameBackground.value = 'bg0'
}
// Custom background // Custom background
const url = ref('') const url = ref('')

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineAsyncComponent, onMounted, ref } from 'vue' import { defineAsyncComponent, ref } from 'vue'
import { Icon } from '@iconify/vue' import { Icon } from '@iconify/vue'
import 'animate.css' import 'animate.css'
import { topBarItem } from './topBarItem' import { topBarItem } from './topBarItem'
@ -25,8 +25,6 @@ const showKUNGalgamePanel = ref(false)
const showKUNGalgameHamburger = ref(false) const showKUNGalgameHamburger = ref(false)
// Show user panel when clicking on the user's avatar // Show user panel when clicking on the user's avatar
const showKUNGalgameUserPanel = ref(false) const showKUNGalgameUserPanel = ref(false)
// User avatar image link, ssr friendly
const userAvatar = ref('')
// Set the navigation bar width based on the number of navigation items // Set the navigation bar width based on the number of navigation items
const navItemNum = topBarItem.length const navItemNum = topBarItem.length
@ -37,16 +35,9 @@ onBeforeRouteLeave(() => {
showKUNGalgamePanel.value = false showKUNGalgamePanel.value = false
showKUNGalgameHamburger.value = false showKUNGalgameHamburger.value = false
}) })
onMounted(() => {
userAvatar.value = avatarMin.value
? avatarMin.value
: '/src/assets/images/favicon.webp'
})
</script> </script>
<template> <template>
<div>
<div class="header"> <div class="header">
<!-- Top left interactive bar --> <!-- Top left interactive bar -->
<div class="nav-top"> <div class="nav-top">
@ -99,16 +90,12 @@ onMounted(() => {
<div class="avatar"> <div class="avatar">
<img <img
v-if="avatarMin"
@click="showKUNGalgameUserPanel = true" @click="showKUNGalgameUserPanel = true"
:src="userAvatar" :src="avatarMin"
:alt="name" alt="KUN"
/> />
<span @click="showKUNGalgameUserPanel = true" v-if="!avatarMin">
<!-- For SSR -->
<span
@click="showKUNGalgameUserPanel = true"
v-if="userAvatar === '/src/assets/images/favicon.webp'"
>
{{ name }} {{ name }}
</span> </span>
</div> </div>
@ -132,7 +119,6 @@ onMounted(() => {
</KeepAlive> </KeepAlive>
</transition> </transition>
</div> </div>
</div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>

15
src/directives/index.ts Normal file
View file

@ -0,0 +1,15 @@
/**
* Currently, all directives are not enabled.
*/
import { type App } from 'vue'
// Directive for enlarging images on click
import { zoom } from './zoom/zoom'
// Permission directive
import { permission } from './permission/permission'
// Mount directives
export function setupKUNGalgameDirectives(app: App) {
app.directive('zoom', zoom)
app.directive('permission', permission)
}

View file

@ -0,0 +1,62 @@
import type { Directive, DirectiveBinding } from 'vue'
import Message from '@/components/alert/Message'
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
import { storeToRefs } from 'pinia'
import router from '@/router'
const { roles, uid } = storeToRefs(useKUNGalgameUserStore())
// User roles: Guest 0, User 1, Admin 2, SuperAdmin 3, User Self 4
enum UserRole {
Guest = 0,
User = 1,
Admin = 2,
SuperAdmin = 3,
Self = 4,
}
/**
* @roles: User types that can access this resource.
* @uid: User ID that needs authorization.
*/
interface BindingProps {
roles: UserRole[]
uid?: number
}
const handleUnauthorizedAccess = (element: HTMLElement) => {
element.parentNode?.removeChild(element)
Message(
'You do not have sufficient permissions!',
'您没有足够的权限!',
'error',
5000
)
router.push('/kungalgame403')
}
export const permission: Directive = {
mounted(element: HTMLElement, binding: DirectiveBinding<BindingProps>) {
const bindingRoles = [...binding.value.roles]
const bindingUid = binding.value.uid
const hasPermission = () => {
// User Self
if (bindingUid === uid.value) {
return true
}
// User has access permission
if (bindingRoles.includes(roles.value)) {
return true
}
return false
}
if (!hasPermission()) {
handleUnauthorizedAccess(element)
}
},
}

View file

@ -0,0 +1,20 @@
import type { Directive, DirectiveBinding } from 'vue'
/* This plugin is not enabled */
// Enlarge images in nodes rendered by v-html
export const zoom: Directive = {
// The binding refers to values passed to the directive; there are none here
mounted(element: HTMLElement, binding: DirectiveBinding) {
const { scale, time } = binding.value || { scale: 1.2, time: 0.3 }
element.querySelectorAll('img').forEach((img: HTMLImageElement) => {
img.addEventListener('click', () => {
img.style.transition = `transform ${time}s ease`
img.style.transform = img.classList.contains('zoomed')
? 'scale(1)'
: `scale(${scale})`
})
})
},
}

View file

@ -1,22 +0,0 @@
import { createApp } from './main'
import { createKUNGalgameRouter } from './router'
import { setupPinia } from './store'
import createI18n from '@/language/i18n'
import '@/styles/index.scss'
const router = createKUNGalgameRouter()
const pinia = setupPinia()
const { app } = createApp()
export const i18n = createI18n()
app.use(router).use(pinia).use(i18n)
if (window.__pinia) {
pinia.state.value = JSON.parse(JSON.stringify(window.__pinia))
}
router.isReady().then(() => {
app.mount('#app', true)
})

View file

@ -1,84 +0,0 @@
import { createApp } from './main'
import { createKUNGalgameRouter } from './router'
import { setupPinia } from './store'
import createI18n from '@/language/i18n'
import { renderToString } from '@vue/server-renderer'
import type { ParameterizedContext } from 'koa'
import '@/styles/index.scss'
const renderPreloadLink = (file: string) => {
if (file.endsWith('.js')) {
return `<link rel="modulepreload" crossorigin href="${file}">`
} else if (file.endsWith('.css')) {
return `<link rel="stylesheet" href="${file}">`
} else if (file.endsWith('.png')) {
return ` <link rel="preload" href="${file}" as="image" type="image/png">`
} else {
// TODO
return ''
}
}
const renderPreloadLinks = (
modules: undefined | string[],
manifest: Record<string, string[]>
) => {
let links = ''
const seen = new Set()
if (modules === undefined) throw new Error()
modules.forEach((id) => {
const files = manifest[id]
if (files) {
files.forEach((file) => {
if (!seen.has(file)) {
seen.add(file)
links += renderPreloadLink(file)
}
})
}
})
return links
}
export const render = async (
ctx: ParameterizedContext,
manifest: Record<string, string[]>,
options: { language: string; country: string }
) => {
const { app } = createApp()
const { language, country } = options
// router
const router = createKUNGalgameRouter()
app.use(router)
await router.push(ctx.path)
await router.isReady()
// pinia
const pinia = setupPinia()
app.use(pinia)
const renderedPinia = JSON.stringify(pinia.state.value)
// i18n
app.use(createI18n(language.includes('zh') ? 'zh' : 'en'))
const renderCtx: Record<string, string[] | {}> = {}
const renderedHtml = await renderToString(app, renderCtx)
const renderedLinks = renderPreloadLinks(
renderCtx.modules as string[],
manifest
)
const renderedTeleports = renderCtx.teleports as {
'#teleported': string
}
return [renderedHtml, renderedPinia, renderedLinks, renderedTeleports]
}

View file

@ -1,11 +1,10 @@
import { useRouter } from 'vue-router'
// Global message component (top) // Global message component (top)
import Message from '@/components/alert/Message' import Message from '@/components/alert/Message'
import { generateTokenByRefreshTokenApi } from '@/api' import { generateTokenByRefreshTokenApi } from '@/api'
// Use the user store // Use the user store
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer' import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
// Import the router // Import the router
import { createKUNGalgameRouter } from '@/router' import router from '@/router'
// Import known error handling functions // Import known error handling functions
import { kungalgameErrorHandler } from './errorHandler' import { kungalgameErrorHandler } from './errorHandler'
@ -29,9 +28,7 @@ export async function onRequestError(response: Response) {
if (accessTokenResponse.code === 200 && accessTokenResponse.data.token) { if (accessTokenResponse.code === 200 && accessTokenResponse.data.token) {
useKUNGalgameUserStore().setToken(accessTokenResponse.data.token) useKUNGalgameUserStore().setToken(accessTokenResponse.data.token)
// Set the page to reload with the new token applied // Set the page to reload with the new token applied
if (typeof window !== 'undefined') {
location.reload() location.reload()
}
} else { } else {
// Otherwise, prompt the user to log in again // Otherwise, prompt the user to log in again
Message( Message(
@ -40,7 +37,7 @@ export async function onRequestError(response: Response) {
'error' 'error'
) )
useKUNGalgameUserStore().removeToken() useKUNGalgameUserStore().removeToken()
useRouter().push('/login') router.push('/login')
} }
return return
} }

View file

@ -1,6 +1,12 @@
// Import the settings panel store // Import the settings panel store
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer' import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
import { saveImage, getImage } from './useLocalforage' import { saveImage, getImage } from './useLocalforage'
import { storeToRefs } from 'pinia'
// Use the settings panel store
const { showKUNGalgameBackground, showKUNGalgameCustomBackground } =
storeToRefs(useKUNGalgameSettingsStore())
// Fetch background image data from the backend // Fetch background image data from the backend
const fetchGetBackground = async (imageName: string): Promise<Blob> => { const fetchGetBackground = async (imageName: string): Promise<Blob> => {
@ -33,4 +39,25 @@ const getBackgroundURL = async (imageName: string) => {
} }
} }
export { getBackgroundURL } // The image names here are defined by the backend as bg1.webp (large image) and bg1-m.webp (preview image)
const getCurrentBackground = async () => {
if (
showKUNGalgameBackground.value === 'bg0' ||
showKUNGalgameBackground.value === 'none'
) {
return 'none'
}
if (showKUNGalgameBackground.value === 'bg1007') {
return `${showKUNGalgameCustomBackground.value}`
}
// Get the image's blob URL
const url = await getBackgroundURL(showKUNGalgameBackground.value)
return url
}
// Restore the blank background
const restoreBackground = () => {
showKUNGalgameBackground.value = 'bg0'
}
export { getBackgroundURL, getCurrentBackground, restoreBackground }

View file

@ -1,19 +1,21 @@
import { createI18n as _createI18n } from 'vue-i18n' import { createI18n } from 'vue-i18n'
// Get localeStorage language config // 读取本地存储中的语言配置
import { KUNGalgameLanguage } from '@/utils/getDefaultEnv' import { KUNGalgameLanguage } from '@/utils/getDefaultEnv'
// Import language file // 引入语言文件
import zh from './zh' import zh from './zh'
import en from './en' import en from './en'
const createI18n = (language?: string) => const i18n = createI18n({
_createI18n({ locale: KUNGalgameLanguage,
locale: language || KUNGalgameLanguage, // 支持 Vue3 composition API
legacy: false, legacy: false,
// 全局注册 t 方法
globalInjection: true,
messages: { messages: {
zh, zh,
en, en,
}, },
}) })
export default createI18n export default i18n

View file

@ -2,7 +2,7 @@
import { onMounted, watch, ref } from 'vue' import { onMounted, watch, ref } from 'vue'
// Import animations // Import animations
import 'animate.css' import 'animate.css'
import { getBackgroundURL } from '@/hooks/useBackgroundPicture' import { getCurrentBackground } from '@/hooks/useBackgroundPicture'
import KUNGalgameTopBar from '@/components/top-bar/KUNGalgameTopBar.vue' import KUNGalgameTopBar from '@/components/top-bar/KUNGalgameTopBar.vue'
import { useKUNGalgameSettingsStore } from '@/store/modules/settings' import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
@ -10,20 +10,6 @@ import { storeToRefs } from 'pinia'
const { showKUNGalgameBackground, showKUNGalgameCustomBackground } = const { showKUNGalgameBackground, showKUNGalgameCustomBackground } =
storeToRefs(useKUNGalgameSettingsStore()) storeToRefs(useKUNGalgameSettingsStore())
const getCurrentBackground = async () => {
if (
showKUNGalgameBackground.value === 'bg0' ||
showKUNGalgameBackground.value === 'none'
) {
return 'none'
}
if (showKUNGalgameBackground.value === 'bg1007') {
return `${showKUNGalgameCustomBackground.value}`
}
// Get the image's blob URL
const url = await getBackgroundURL(showKUNGalgameBackground.value)
return url
}
const imageURL = ref('') const imageURL = ref('')

View file

@ -1,7 +1,22 @@
import { createSSRApp } from 'vue' // Vue core
import { createApp } from 'vue'
import App from './App.vue' import App from './App.vue'
import router from './router'
// Import vue i18n
import i18n from '@/language/i18n'
export const createApp = () => { import { setupRouterGuard } from '@/router/guard'
const app = createSSRApp(App) import { setupPinia } from '@/store/index'
return { app }
} // Import css styles, color, theme, etc.
import '@/styles/index.scss'
// Get vue App instance
const app = createApp(App)
// Setup router guard
setupRouterGuard(router)
// Setup pinia
setupPinia(app)
app.use(router).use(i18n).mount('#app')

View file

@ -1,6 +1,6 @@
import { Router } from 'vue-router' import { Router } from 'vue-router'
import { createPermission } from './permission' import { createPermission } from './permission'
import { i18n } from '@/entry-client' import i18n from '@/language/i18n'
const createPageTitle = (router: Router) => { const createPageTitle = (router: Router) => {
router.beforeEach((to) => { router.beforeEach((to) => {

View file

@ -1,16 +1,11 @@
import type { RouteRecordRaw, Router } from 'vue-router' import { type RouteRecordRaw, createWebHistory, createRouter } from 'vue-router'
import { createWebHistory, createRouter, createMemoryHistory } from 'vue-router'
import { constantRoutes } from './router' import { constantRoutes } from './router'
import { asyncRoutes } from './router' import { asyncRoutes } from './router'
export const createKUNGalgameRouter = (): Router => // Create a Vue Router instance
createRouter({ const router = createRouter({
history: import.meta.env.SSR history: createWebHistory(import.meta.env.BASE_URL),
? createMemoryHistory(import.meta.env.BASE_URL)
: createWebHistory(import.meta.env.BASE_URL),
routes: [...constantRoutes, ...asyncRoutes] as RouteRecordRaw[], routes: [...constantRoutes, ...asyncRoutes] as RouteRecordRaw[],
// Scroll to the top of the page with a smooth animation on each page navigation // Scroll to the top of the page with a smooth animation on each page navigation
scrollBehavior(to, from, savedPosition) { scrollBehavior(to, from, savedPosition) {
if (savedPosition) { if (savedPosition) {
@ -19,4 +14,6 @@ export const createKUNGalgameRouter = (): Router =>
return { top: 0, behavior: 'smooth' } return { top: 0, behavior: 'smooth' }
} }
}, },
}) })
export default router

View file

@ -4,6 +4,7 @@
*/ */
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import type { App } from 'vue'
// Import store for the income and expense public disclosure page // Import store for the income and expense public disclosure page
import { useKUNGalgameBalanceStore } from './modules/balance' import { useKUNGalgameBalanceStore } from './modules/balance'
@ -32,19 +33,16 @@ import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
// Import store for the topic detail page // Import store for the topic detail page
import { useKUNGalgameTopicStore } from './modules/topic' import { useKUNGalgameTopicStore } from './modules/topic'
const pinia = createPinia() const store = createPinia()
// Function to set up Pinia, to be called in main.ts // Function to set up Pinia, to be called in main.ts
export const setupPinia = () => { export function setupPinia(app: App<Element>) {
if (!import.meta.env.SSR) { store.use(piniaPluginPersistedstate)
pinia.use(piniaPluginPersistedstate) app.use(store)
}
return pinia
} }
// Reset all stores, used for logging out // Reset all stores, used for logging out
export const kungalgameStoreReset = () => { export function kungalgameStoreReset() {
const balanceStore = useKUNGalgameBalanceStore() const balanceStore = useKUNGalgameBalanceStore()
const editStore = useKUNGalgameEditStore() const editStore = useKUNGalgameEditStore()
const homeStore = useKUNGalgameHomeStore() const homeStore = useKUNGalgameHomeStore()
@ -65,3 +63,5 @@ export const kungalgameStoreReset = () => {
settingsStore.$reset() settingsStore.$reset()
topicStore.$reset() topicStore.$reset()
} }
export { store }

View file

@ -1,3 +0,0 @@
interface Window {
__pinia: string
}

View file

@ -3,19 +3,11 @@
*/ */
// Read language configuration from local storage // Read language configuration from local storage
let localStorageString = '' const localStorageString = localStorage.getItem('KUNGalgameSettings')
if (typeof window !== 'undefined') {
const storage = localStorage.getItem('KUNGalgameSettings')
localStorageString = storage ? storage : ''
}
// To ensure compatibility with various browsers, some browsers have 'zh-CN' as navigator.language, which may cause errors. // To ensure compatibility with various browsers, some browsers have 'zh-CN' as navigator.language, which may cause errors.
const getInitLanguage = () => { const getInitLanguage = () => {
let userLanguage = '' const userLanguage = navigator.language
if (typeof navigator !== 'undefined') {
userLanguage = navigator.language
}
if (userLanguage.includes('en')) { if (userLanguage.includes('en')) {
return 'en' return 'en'
@ -32,11 +24,6 @@ export const KUNGalgameLanguage = localStorageString
: getInitLanguage() : getInitLanguage()
// Read local day-night mode, this function will return 'true' if it's in dark mode. // Read local day-night mode, this function will return 'true' if it's in dark mode.
export const mode = () => { export const mode = window.matchMedia('(prefers-color-scheme: dark)').matches
if (typeof window !== 'undefined') {
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark' ? 'dark'
: '' : ''
}
return ''
}

View file

@ -7,7 +7,7 @@ type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
const successResponseArray = [200, 201, 202, 204, 205, 206] const successResponseArray = [200, 201, 202, 204, 205, 206]
export type FetchOptions = RequestInit & { export type FetchOptions = {
method: HttpMethod method: HttpMethod
credentials: 'include' credentials: 'include'
headers?: Record<string, string> headers?: Record<string, string>

View file

@ -38,7 +38,7 @@ const mainPageWidth = computed(() => {
.content-container { .content-container {
width: v-bind(mainPageWidth); width: v-bind(mainPageWidth);
transition: width 0.2s; transition: all 0.2s;
height: 100%; height: 100%;
margin: 0 auto; margin: 0 auto;
display: flex; display: flex;

View file

@ -48,6 +48,7 @@ const getRepliesCount = computed(() => {
<span>{{ title }}</span> <span>{{ title }}</span>
</div> </div>
<!-- Topic status, likes, etc. -->
<div class="status"> <div class="status">
<ul> <ul>
<li> <li>
@ -63,6 +64,7 @@ const getRepliesCount = computed(() => {
</li> </li>
</ul> </ul>
<!-- Publication date of the topic -->
<div class="time"> <div class="time">
<span> <span>
{{ {{
@ -76,6 +78,7 @@ const getRepliesCount = computed(() => {
</div> </div>
</div> </div>
<!-- Preview introduction of the topic -->
<div class="introduction"> <div class="introduction">
<p> <p>
{{ plainText }} {{ plainText }}

View file

@ -47,13 +47,14 @@ const handleClosePanel = async () => {
</script> </script>
<template> <template>
<Teleport to="#teleported" :disabled="isEdit"> <Teleport to="body" :disabled="isEdit">
<Transition <Transition
enter-active-class="animate__animated animate__fadeInUp animate__faster" enter-active-class="animate__animated animate__fadeInUp animate__faster"
leave-active-class="animate__animated animate__fadeOutDown animate__faster" leave-active-class="animate__animated animate__fadeOutDown animate__faster"
> >
<div class="root" v-if="isEdit"> <div class="root" v-if="isEdit">
<div class="container" :style="`width: ${panelWidth}`"> <div class="container" :style="`width: ${panelWidth}`">
<!-- Reply panel - reply to whom -->
<div class="title"> <div class="title">
<h3> <h3>
<span>{{ $tm('topic.panel.to') + ' @' }}</span> <span>{{ $tm('topic.panel.to') + ' @' }}</span>
@ -70,10 +71,12 @@ const handleClosePanel = async () => {
/> />
</div> </div>
<!-- Reply editor -->
<div class="content"> <div class="content">
<MilkdownEditor :is-show-menu="isShowAdvance" /> <MilkdownEditor :is-show-menu="isShowAdvance" />
</div> </div>
<!-- Reply footer -->
<div class="footer"> <div class="footer">
<Tags <Tags
style="margin-top: 10px; padding: 10px" style="margin-top: 10px; padding: 10px"

View file

@ -24,9 +24,7 @@
"src/**/*.d.ts", "src/**/*.d.ts",
"src/**/**/*.d.ts", "src/**/**/*.d.ts",
"src/**/*.tsx", "src/**/*.tsx",
"src/**/*.vue", "src/**/*.vue"
"server-dev.ts",
"server-prod.ts"
], ],
"exclude": ["node_modules", "dist"], "exclude": ["node_modules", "dist"],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{ "path": "./tsconfig.node.json" }]

View file

@ -5,8 +5,7 @@ import vue from '@vitejs/plugin-vue'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig(({ mode, ssrBuild, command }) => { export default defineConfig({
return {
plugins: [vue(), visualizer() as PluginOption], plugins: [vue(), visualizer() as PluginOption],
/* Set the 'src' alias to '@' */ /* Set the 'src' alias to '@' */
resolve: { resolve: {
@ -14,10 +13,7 @@ export default defineConfig(({ mode, ssrBuild, command }) => {
'@': path.resolve(__dirname, './src'), '@': path.resolve(__dirname, './src'),
}, },
}, },
esbuild: esbuild: {
command === 'serve'
? {}
: {
drop: ['console', 'debugger'], drop: ['console', 'debugger'],
}, },
build: { build: {
@ -31,5 +27,4 @@ export default defineConfig(({ mode, ssrBuild, command }) => {
__VUE_I18N_LEGACY_API__: false, __VUE_I18N_LEGACY_API__: false,
__INTLIFY_PROD_DEVTOOLS__: false, __INTLIFY_PROD_DEVTOOLS__: false,
}, },
}
}) })