merge: fix: fix ssr teleports and i18n #26

This commit is contained in:
KUN1007 2023-11-07 16:26:10 +08:00
parent 640f118185
commit 72a301bfee
10 changed files with 664 additions and 32 deletions

View file

@ -17,6 +17,7 @@
</head>
<body>
<div id="app"><!--ssr-outlet--></div>
<div id="teleported"><!--teleports--></div>
<script type="module" src="/src/entry-client.ts"></script>
<script>

View file

@ -13,6 +13,17 @@ 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()
@ -32,9 +43,20 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
appType: 'custom',
})
// 解析accept-language
function 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'),
@ -45,9 +67,17 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
const { render } = await vite.ssrLoadModule('/src/entry-server.ts')
const [renderedHtml, renderedPinia, renderedLinks] = await render(ctx, {})
const [renderedHtml, renderedPinia, renderedLinks, renderedTeleports] =
await render(
ctx,
{},
{
language,
country,
}
)
const html = template
const html = injectTeleports(template, renderedTeleports)
.replace('<!--preload-links-->', renderedLinks)
.replace('<!--ssr-outlet-->', renderedHtml)
.replace('__pinia', renderedPinia)

View file

@ -0,0 +1,110 @@
<script setup lang="ts">
import { useKUNGalgameMessageStore } from '@/store/modules/message'
import { storeToRefs } from 'pinia'
const { showAlert, alertMsg, isShowCancel } = storeToRefs(
useKUNGalgameMessageStore()
)
const handleClose = () => {
showAlert.value = false
useKUNGalgameMessageStore().handleClose()
}
const handleConfirm = () => {
showAlert.value = false
useKUNGalgameMessageStore().handleConfirm()
}
</script>
<template>
<Teleport to="#teleported" :disabled="showAlert">
<Transition name="alert">
<div>
<div v-if="showAlert" class="mask">
<div class="container">
<div class="header">
<h3>{{ $t(`${alertMsg}`) }}</h3>
</div>
<div class="footer">
<button v-if="isShowCancel" class="button" @click="handleClose">
{{ $tm('ComponentAlert.cancel') }}
</button>
<button class="button" @click="handleConfirm">
{{ $tm('ComponentAlert.confirm') }}
</button>
</div>
</div>
</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;
color: var(--kungalgame-font-color-3);
}
.container {
width: 300px;
margin: auto;
padding: 20px 30px;
background-color: var(--kungalgame-trans-white-2);
border: 1px solid var(--kungalgame-blue-1);
border-radius: 2px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
transition: all 0.3s ease;
}
.footer {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.button {
width: 70px;
height: 30px;
color: var(--kungalgame-font-color-3);
cursor: pointer;
border-radius: 2px;
&:nth-child(1) {
background-color: var(--kungalgame-trans-blue-1);
border: 1px solid var(--kungalgame-blue-4);
}
&:nth-child(2) {
margin-left: 98px;
background-color: var(--kungalgame-trans-red-1);
border: 1px solid var(--kungalgame-red-4);
}
&:active {
transform: scale(0.9);
transition: 0.1s;
}
}
.alert-enter-from {
opacity: 0;
}
.alert-leave-to {
opacity: 0;
}
.alert-enter-from .container,
.alert-leave-to .container {
-webkit-transform: scale(1.1);
transform: scale(1.1);
}
</style>

View file

@ -0,0 +1,152 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { useKUNGalgameMessageStore } from '@/store/modules/message'
import { storeToRefs } from 'pinia'
import img from './loli'
import 'animate.css'
const { showInfo, infoMsg } = storeToRefs(useKUNGalgameMessageStore())
const { loli, name } = img
const handleClose = () => {
showInfo.value = false
}
</script>
<template>
<Teleport to="#teleported" :disabled="showInfo">
<Transition
enter-active-class="animate__animated animate__fadeInUp animate__faster"
leave-active-class="animate__animated animate__fadeOutDown animate__faster"
>
<div>
<div class="container" v-if="showInfo">
<Transition
enter-active-class="animate__animated animate__swing"
appear
>
<div class="lass">
<span>{{ name }}</span>
</div>
</Transition>
<div class="avatar">
<img :src="loli" />
</div>
<Transition
enter-active-class="animate__animated animate__bounceInRight animate__faster"
appear
>
<!-- A ha ha ha! You probably didn't expect that this was inspired by しゅがてん-Sugarfull tempering- -->
<div class="info">{{ `${$t(`${infoMsg}`)}` }}</div>
</Transition>
<div class="close" @click="handleClose">
<Icon icon="line-md:close" />
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<style lang="scss" scoped>
.container {
min-height: 120px;
width: 100%;
color: var(--kungalgame-font-color-3);
background-color: var(--kungalgame-trans-white-5);
backdrop-filter: blur(2px);
box-shadow: var(--shadow);
border-top: 1px solid var(--kungalgame-blue-1);
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 9999;
}
.lass {
padding: 5px;
font-size: 20px;
position: absolute;
top: -41px;
padding-left: 150px;
border-bottom: none;
/* This shadow can only be drawn like this */
filter: drop-shadow(2px 4px 3px var(--kungalgame-trans-blue-4));
span {
padding: 0 50px;
text-align: center;
background-color: var(--kungalgame-trans-white-2);
font-size: 24px;
/* Here, the shape of the character's name is clipped into a hexagon */
clip-path: polygon(10% 0%, 90% 0%, 100% 50%, 90% 100%, 10% 100%, 0 50%);
}
}
.avatar {
position: absolute;
margin-top: 10px;
margin-left: 20px;
img {
height: 100px;
width: 100%;
}
}
.info {
margin-top: 20px;
margin-left: 150px;
margin-right: 50px;
font-size: 20px;
color: var(--kungalgame-white);
text-shadow: 0 1px var(--kungalgame-font-color-3),
1px 0 var(--kungalgame-font-color-3), -1px 0 var(--kungalgame-font-color-3),
0 -1px var(--kungalgame-font-color-3),
1px 2px var(--kungalgame-font-color-3),
1px 2px var(--kungalgame-font-color-3),
1px 2px var(--kungalgame-font-color-3),
1px 2px var(--kungalgame-font-color-3);
}
.close {
font-size: 30px;
position: absolute;
top: 0;
right: 0;
color: var(--kungalgame-font-color-1);
}
@media (max-width: 700px) {
.container {
min-height: 77px;
}
.lass {
padding: 5px;
font-size: 15px;
padding-left: 20px;
top: -33px;
span {
font-size: 17px;
}
}
.info {
margin-top: 10px;
margin-right: 30px;
margin-left: 77px;
}
.avatar {
img {
height: 50px;
width: 100%;
}
}
}
</style>

View file

@ -0,0 +1,307 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
// Import questions
import { questionsEN, Question } from './questionsEN'
import { questionsCN } from './questionsCN'
import Message from '@/components/alert/Message'
import { useKUNGalgameMessageStore } from '@/store/modules/message'
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
import { storeToRefs } from 'pinia'
const { showKUNGalgameLanguage } = storeToRefs(useKUNGalgameSettingsStore())
const { isShowCapture, isCaptureSuccessful } = storeToRefs(
useKUNGalgameMessageStore()
)
// Current language
const questions = ref<Question[]>([])
// Initialize
questions.value =
showKUNGalgameLanguage.value === 'en' ? questionsEN : questionsCN
// Watch for changes in the language setting
watch(showKUNGalgameLanguage, () => {
questions.value =
showKUNGalgameLanguage.value === 'en' ? questionsEN : questionsCN
})
// Function to randomly select a question
const randomizeQuestion = () => {
// Generate a random integer between 0 and the number of questions minus 1
return Math.floor(Math.random() * questions.value.length)
}
// User's input answer
const userAnswers = ref('')
// Current question index
const currentQuestionIndex = ref(randomizeQuestion())
// Current question
const currentQuestion = ref(questions.value[currentQuestionIndex.value])
// Error count
const errorCounter = ref(0)
const expectedKeys = ref(['k', 'u', 'n'])
const currentIndex = ref(0)
// Whether to show hints
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
if (pressedKey === expectedKeys.value[currentIndex.value]) {
// When the user presses the expected key
if (currentIndex.value === expectedKeys.value.length - 1) {
// If the last key "n" has been pressed, trigger the corresponding logic
isShowAnswer.value = true
} else {
// Otherwise, continue to the next expected key
currentIndex.value++
}
} else {
// If the wrong key is pressed, start checking again
currentIndex.value = 0
}
}
// Submit the answer
const submitAnswer = () => {
const correctOption = currentQuestion.value.correctOption
if (userAnswers.value === correctOption) {
// Correct answer
// Set the validation as successful
isCaptureSuccessful.value = true
// Close the panel
isShowCapture.value = false
Message(
'Human-machine identity verification successful ~',
'人机身份验证通过 ~',
'success'
)
resetStatus()
} else {
// Wrong answer
errorCounter.value++
Message('Wrong answer!', '回答错误!', 'warn')
// Randomly select a new question
const randomIndex = randomizeQuestion()
currentQuestionIndex.value = randomIndex
userAnswers.value = ''
// Show hints if the error count is greater than or equal to 3
if (errorCounter.value >= 3) {
isShowHint.value = true
}
}
}
// Close panel
const handleCloseCapture = () => {
isShowCapture.value = false
resetStatus()
}
</script>
<template>
<Teleport to="#teleported" :disabled="isShowCapture">
<Transition name="capture">
<div>
<!-- Mask -->
<div
class="mask"
@keydown="checkKeyPress($event)"
tabindex="0"
v-if="isShowCapture"
>
<div class="validate">
<!-- Title -->
<div class="title">
<!-- <span>{{ `` }}</span> -->
<h2>{{ $tm('AlertInfo.capture.title') }}</h2>
<!-- <span>{{ `` }}</span> -->
</div>
<p class="question">{{ currentQuestion.text }}</p>
<!-- Options -->
<div class="select">
<label
v-for="(option, index) in currentQuestion.options"
:key="index"
>
<input type="radio" v-model="userAnswers" :value="option" />
{{ option }}
</label>
</div>
<!-- Submit buttons -->
<div class="btn">
<button @click="submitAnswer">
{{ $tm('AlertInfo.capture.submit') }}
</button>
<button @click="handleCloseCapture">
{{ $tm('AlertInfo.capture.close') }}
</button>
</div>
<!-- Hints -->
<!-- tabindex allows this element to be focused on the page -->
<div class="hint-container">
<div v-if="isShowHint" class="hint">
<div>{{ $tm('AlertInfo.capture.hint1') }}</div>
<div>
{{ $tm('AlertInfo.capture.hint2') }}
<span>kun</span>
{{ $tm('AlertInfo.capture.hint3') }}
</div>
</div>
<div v-if="isShowAnswer" class="answer">
<div>{{ $tm('AlertInfo.capture.hint4') }}</div>
<a
href="https://github.com/KUN1007/kun-galgame-vue/tree/remove-server/src/components/capture"
target="_blank"
rel="noopener noreferrer"
>
{{ $tm('AlertInfo.capture.answer') }}
</a>
</div>
</div>
</div>
</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;
color: var(--kungalgame-font-color-3);
}
.validate {
width: 300px;
min-height: 300px;
margin: auto;
padding: 17px;
background-color: var(--kungalgame-trans-white-2);
border: 1px solid var(--kungalgame-blue-1);
border-radius: 5px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
transition: all 0.3s ease;
display: flex;
flex-direction: column;
}
.title {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 17px;
color: var(--kungalgame-blue-4);
}
.question {
font-size: 17px;
margin-bottom: 20px;
font-style: oblique;
}
.select {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-rows: repeat(2, minmax(0, 1fr));
margin-bottom: 20px;
}
.btn {
display: flex;
justify-content: space-around;
button {
width: 77px;
padding: 5px;
color: var(--kungalgame-blue-4);
border: 1px solid var(--kungalgame-blue-4);
border-radius: 5px;
background-color: var(--kungalgame-trans-white-9);
transition: all 0.2s;
&:hover {
color: var(--kungalgame-white);
background-color: var(--kungalgame-blue-4);
}
}
}
.hint-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: end;
justify-content: end;
font-style: oblique;
.hint {
width: 100%;
font-size: 10px;
span {
color: var(--kungalgame-pink-4);
font-weight: bold;
}
}
.answer {
width: 100%;
div {
font-size: 10px;
}
a {
color: var(--kungalgame-blue-5);
&:hover {
text-decoration: underline;
}
}
}
}
.active {
transition: all 0.2s;
border: 2px solid var(--kungalgame-pink-3);
}
.capture-enter-from {
opacity: 0;
}
.capture-leave-to {
opacity: 0;
}
.capture-enter-from .validate,
.capture-leave-to .validate {
-webkit-transform: scale(1.1);
transform: scale(1.1);
}
</style>

View file

@ -1,7 +1,7 @@
import { createApp } from './main'
import { createKUNGalgameRouter } from './router'
import { setupPinia } from './store'
import i18n from '@/language/i18n'
import createI18n from '@/language/i18n'
import '@/styles/index.scss'
const router = createKUNGalgameRouter()
@ -9,6 +9,8 @@ const pinia = setupPinia()
const { app } = createApp()
export const i18n = createI18n()
app.use(router).use(pinia).use(i18n)
if (window.__pinia) {

View file

@ -2,7 +2,7 @@ import { createApp } from './main'
import { createKUNGalgameRouter } from './router'
import { setupPinia } from './store'
import i18n from '@/language/i18n'
import createI18n from '@/language/i18n'
import { renderToString } from '@vue/server-renderer'
@ -46,9 +46,11 @@ const renderPreloadLinks = (
export const render = async (
ctx: ParameterizedContext,
manifest: Record<string, string[]>
): Promise<[string, string, string]> => {
manifest: Record<string, string[]>,
options: { language: string; country: string }
) => {
const { app } = createApp()
const { language, country } = options
// router
const router = createKUNGalgameRouter()
@ -63,7 +65,9 @@ export const render = async (
const renderedPinia = JSON.stringify(pinia.state.value)
// i18n
app.use(i18n)
app.use(createI18n(
language.includes('zh') ? 'zh' : 'en',
))
const renderCtx: { modules?: string[] } = {}
@ -71,5 +75,9 @@ export const render = async (
const renderedLinks = renderPreloadLinks(renderCtx.modules, manifest)
return [renderedHtml, renderedPinia, renderedLinks]
const renderedTeleports = renderCtx.teleports as {
'#teleported': string
}
return [renderedHtml, renderedPinia, renderedLinks, renderedTeleports]
}

View file

@ -1,4 +1,4 @@
import { createI18n } from 'vue-i18n'
import { createI18n as _createI18n } from 'vue-i18n'
// 读取本地存储中的语言配置
import { KUNGalgameLanguage } from '@/utils/getDefaultEnv'
@ -6,8 +6,8 @@ import { KUNGalgameLanguage } from '@/utils/getDefaultEnv'
import zh from './zh'
import en from './en'
const i18n = createI18n({
locale: KUNGalgameLanguage,
const createI18n = (language?: string) => _createI18n({
locale: language || KUNGalgameLanguage,
legacy: false,
messages: {
zh,
@ -15,4 +15,4 @@ const i18n = createI18n({
},
})
export default i18n
export default createI18n

16
src/router/guard/index.ts Normal file
View file

@ -0,0 +1,16 @@
import { Router } from 'vue-router'
import { createPermission } from './permission'
import { i18n } from '@/entry-client'
const createPageTitle = (router: Router) => {
router.beforeEach((to) => {
const title = to.meta.title as string
document.title = i18n.global.tm(`router.${title}`)
return true
})
}
export function setupRouterGuard(router: Router) {
createPermission(router)
createPageTitle(router)
}

View file

@ -5,25 +5,31 @@ import vue from '@vitejs/plugin-vue'
import { visualizer } from 'rollup-plugin-visualizer'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(), visualizer() as PluginOption],
/* Set the 'src' alias to '@' */
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
export default defineConfig(({ mode, ssrBuild, command }) => {
return {
plugins: [vue(), visualizer() as PluginOption],
/* Set the 'src' alias to '@' */
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
},
esbuild: {
drop: ['console', 'debugger'],
},
build: {
// Dist dir name
assetsDir: 'kun',
},
// Suppress i18n warnings
define: {
__VUE_I18N_FULL_INSTALL__: true,
__VUE_I18N_LEGACY_API__: false,
__INTLIFY_PROD_DEVTOOLS__: false,
},
esbuild:
command === 'serve'
? {}
: {
drop: ['console', 'debugger'],
},
build: {
// Dist dir name
assetsDir: 'kun',
},
server: { host: '127.0.0.1', port: 1007 },
// Suppress i18n warnings
define: {
__VUE_I18N_FULL_INSTALL__: true,
__VUE_I18N_LEGACY_API__: false,
__INTLIFY_PROD_DEVTOOLS__: false,
},
}
})