feat: i18n system, lang switch component, english subtitles, UI improvements, roadmap update
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
import type { ChapterInfo } from '@engine/types'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
|
||||
const props = defineProps<{
|
||||
chapters: ChapterInfo[]
|
||||
@@ -12,6 +13,7 @@ const emit = defineEmits<{
|
||||
back: []
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const focusIdx = ref(0)
|
||||
const cardRefs = ref<(HTMLDivElement | null)[]>([])
|
||||
|
||||
@@ -60,7 +62,7 @@ function onKeydown(e: KeyboardEvent, index: number) {
|
||||
<template>
|
||||
<div class="chapter-overlay" @keydown="(e: KeyboardEvent) => { if (e.key === 'Escape' || e.key === 'Backspace') { e.preventDefault(); emit('back'); } }">
|
||||
<div class="chapter-panel">
|
||||
<h2 class="chapter-title">章节选择</h2>
|
||||
<h2 class="chapter-title">{{ t('ui.chapters') }}</h2>
|
||||
|
||||
<div class="chapter-grid">
|
||||
<div
|
||||
@@ -82,7 +84,7 @@ function onKeydown(e: KeyboardEvent, index: number) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="back-btn" @click="emit('back')">返回 (Esc)</button>
|
||||
<button class="back-btn" @click="emit('back')">{{ t('ui.back') }} (Esc)</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick, computed } from 'vue'
|
||||
import type { Choice } from '@engine/types'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
|
||||
const props = defineProps<{
|
||||
choices: Choice[]
|
||||
@@ -12,6 +13,7 @@ const emit = defineEmits<{
|
||||
choose: [index: number]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const focusIndex = ref(0)
|
||||
const btnRefs = ref<(HTMLButtonElement | null)[]>([])
|
||||
|
||||
@@ -62,7 +64,7 @@ function onKeydown(e: KeyboardEvent, index: number) {
|
||||
></div>
|
||||
<span class="timer-text">{{ timerRemaining.toFixed(1) }}s</span>
|
||||
</div>
|
||||
<div class="choice-prompt">做出你的选择</div>
|
||||
<div class="choice-prompt">{{ t('ui.choose') }}</div>
|
||||
<div class="choice-list">
|
||||
<button
|
||||
v-for="(choice, index) in choices"
|
||||
|
||||
41
src/components/LangSwitch.vue
Normal file
41
src/components/LangSwitch.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
|
||||
const { currentLang, setLang, t } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="lang-switch">
|
||||
<button
|
||||
:class="['lang-btn', { active: currentLang === 'zh' }]"
|
||||
@click="setLang('zh')"
|
||||
>中文</button>
|
||||
<button
|
||||
:class="['lang-btn', { active: currentLang === 'en' }]"
|
||||
@click="setLang('en')"
|
||||
>English</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.lang-switch {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.lang-btn {
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.lang-btn:first-child { border-radius: 3px 0 0 3px; }
|
||||
.lang-btn:last-child { border-radius: 0 3px 3px 0; }
|
||||
|
||||
.lang-btn:hover { color: #ccc; background: rgba(255, 255, 255, 0.1); }
|
||||
.lang-btn.active { color: #fff; background: rgba(255, 255, 255, 0.15); border-color: rgba(255, 255, 255, 0.25); }
|
||||
</style>
|
||||
@@ -1,5 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
canSkip: boolean
|
||||
@@ -29,7 +32,7 @@ onMounted(() => updateLabel(props.currentSpeed))
|
||||
|
||||
<template>
|
||||
<div class="playback-bar">
|
||||
<button v-if="canSkip" class="pb-btn skip-btn" @click="emit('skip')">跳过</button>
|
||||
<button v-if="canSkip" class="pb-btn skip-btn" @click="emit('skip')">{{ t('ui.skip') }}</button>
|
||||
<button class="pb-btn speed-btn" @click="toggleSpeed">{{ speedLabel }}</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import type { SlotInfo } from '@/stores/gameStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
defineProps<{
|
||||
saves: SlotInfo[]
|
||||
@@ -17,7 +20,7 @@ const maxSlots = 5
|
||||
<template>
|
||||
<div class="save-overlay" @click.self="emit('close')" @keydown.escape="emit('close')">
|
||||
<div class="save-panel">
|
||||
<h2 class="save-title">存档 / 读档</h2>
|
||||
<h2 class="save-title">{{ t('ui.save') }} / {{ t('ui.load') }}</h2>
|
||||
|
||||
<div class="slot-list">
|
||||
<div class="save-slot auto-save-slot">
|
||||
@@ -30,11 +33,11 @@ const maxSlots = 5
|
||||
<span v-else class="thumb-empty">自动</span>
|
||||
</div>
|
||||
<div class="slot-meta">
|
||||
<div class="slot-label auto-save-label">自动存档</div>
|
||||
<div class="slot-label auto-save-label">{{ t('ui.autoSave') }}</div>
|
||||
<div class="slot-info" v-if="saves.find(s => s.slot === 0)">
|
||||
{{ saves.find(s => s.slot === 0)!.sceneLabel }}
|
||||
</div>
|
||||
<div class="slot-info empty" v-else>暂无自动存档</div>
|
||||
<div class="slot-info empty" v-else>{{ t('ui.noAutoSave') }}</div>
|
||||
</div>
|
||||
<div class="slot-actions">
|
||||
<button
|
||||
@@ -42,7 +45,7 @@ const maxSlots = 5
|
||||
:disabled="!saves.find(s => s.slot === 0)"
|
||||
@click="emit('load', 0)"
|
||||
>
|
||||
读取
|
||||
{{ t('ui.load') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -58,30 +61,30 @@ const maxSlots = 5
|
||||
:src="saves.find(s => s.slot === slot)!.thumbnail"
|
||||
class="thumb-img"
|
||||
/>
|
||||
<span v-else class="thumb-empty">空</span>
|
||||
<span v-else class="thumb-empty">{{ t('ui.empty') }}</span>
|
||||
</div>
|
||||
<div class="slot-meta">
|
||||
<div class="slot-label">存档 {{ slot }}</div>
|
||||
<div class="slot-label">{{ t('ui.save') }} {{ slot }}</div>
|
||||
<div class="slot-info" v-if="saves.find(s => s.slot === slot)">
|
||||
{{ saves.find(s => s.slot === slot)!.sceneLabel }}
|
||||
</div>
|
||||
<div class="slot-info empty" v-else>空</div>
|
||||
<div class="slot-info empty" v-else>{{ t('ui.empty') }}</div>
|
||||
</div>
|
||||
<div class="slot-actions">
|
||||
<button class="slot-btn save-btn" @click="emit('save', slot)">保存</button>
|
||||
<button class="slot-btn save-btn" @click="emit('save', slot)">{{ t('ui.save') }}</button>
|
||||
<button
|
||||
class="slot-btn load-btn"
|
||||
:disabled="!saves.find(s => s.slot === slot)"
|
||||
@click="emit('load', slot)"
|
||||
>
|
||||
读取
|
||||
{{ t('ui.load') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="save-hint">游戏会在每次场景切换时自动保存到槽位 0</div>
|
||||
<button class="close-btn" @click="emit('close')">关闭</button>
|
||||
<div class="save-hint">{{ t('ui.autoSaveHint') }}</div>
|
||||
<button class="close-btn" @click="emit('close')">{{ t('ui.close') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { ref, watch, computed } from 'vue'
|
||||
|
||||
interface SubCue {
|
||||
start: number
|
||||
@@ -10,20 +10,25 @@ interface SubCue {
|
||||
const props = defineProps<{
|
||||
currentTime: number
|
||||
subtitleUrl: string | null
|
||||
subtitles?: Record<string, string> | null
|
||||
lang?: string
|
||||
}>()
|
||||
|
||||
const cues = ref<SubCue[]>([])
|
||||
const currentText = ref('')
|
||||
const loadedUrl = ref('')
|
||||
|
||||
watch(() => props.subtitleUrl, async (url) => {
|
||||
const effectiveUrl = computed(() => {
|
||||
if (props.lang && props.subtitles?.[props.lang]) return props.subtitles[props.lang]
|
||||
return props.subtitleUrl
|
||||
})
|
||||
|
||||
watch(effectiveUrl, async (url) => {
|
||||
if (!url) {
|
||||
cues.value = []
|
||||
currentText.value = ''
|
||||
loadedUrl.value = ''
|
||||
return
|
||||
}
|
||||
if (url === loadedUrl.value) return
|
||||
loadedUrl.value = url
|
||||
try {
|
||||
const resp = await fetch(url)
|
||||
|
||||
Reference in New Issue
Block a user