Files
tianshu-engine/src/components/ChoicePanel.vue

215 lines
4.9 KiB
Vue

<script setup lang="ts">
import { ref, watch, nextTick } from 'vue'
import type { Choice } from '@engine/types'
import { useI18n } from '@/composables/useI18n'
import { useGameStore } from '@/stores/gameStore'
const props = defineProps<{
choices: Choice[]
timerTotal: number
timerRemaining: number
}>()
const emit = defineEmits<{
choose: [index: number]
prompt: [text: string]
}>()
const { t } = useI18n()
const store = useGameStore()
function choiceText(choice: Choice): string {
if (choice.textKey) {
const translated = t(choice.textKey)
if (translated !== choice.textKey) return translated
}
return choice.text.trim() || choice.textKey || ''
}
const focusIndex = ref(0)
const btnRefs = ref<(HTMLButtonElement | null)[]>([])
const choiceEnabled = ref(!store.antiMistap)
let enableTimer: ReturnType<typeof setTimeout> | null = null
function timerPercent(): number {
if (props.timerTotal <= 0) return 0
return (props.timerRemaining / props.timerTotal) * 100
}
function timerClass(): string {
if (props.timerRemaining <= 3) return 'danger'
return ''
}
function setRef(el: HTMLButtonElement | null, index: number) {
btnRefs.value[index] = el
}
watch(() => props.choices.length, async (len) => {
if (len > 0) {
if (store.antiMistap) {
choiceEnabled.value = false
if (enableTimer) clearTimeout(enableTimer)
enableTimer = setTimeout(() => { choiceEnabled.value = true }, 500)
} else {
choiceEnabled.value = true
}
focusIndex.value = 0
await nextTick()
btnRefs.value[0]?.focus()
}
})
function onKeydown(e: KeyboardEvent, index: number) {
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
e.preventDefault()
const dir = e.key === 'ArrowDown' ? 1 : -1
const next = Math.max(0, Math.min(props.choices.length - 1, index + dir))
focusIndex.value = next
btnRefs.value[next]?.focus()
}
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
handleChoose(index)
}
}
function handleChoose(index: number) {
if (!choiceEnabled.value) return
const choice = props.choices[index]
if (choice?.prompt) {
emit('prompt', t(choice.promptKey || choice.prompt))
}
emit('choose', index)
}
</script>
<template>
<div class="choice-panel" v-if="choices.length > 0">
<div class="timer-bar" v-if="timerTotal > 0">
<div
class="timer-fill"
:class="timerClass()"
:style="{ width: timerPercent() + '%' }"
></div>
<span class="timer-text">{{ timerRemaining.toFixed(1) }}s</span>
</div>
<div class="choice-prompt">{{ t('ui.choose') }}</div>
<div class="choice-list" :class="{ disabled: !choiceEnabled }">
<button
v-for="(choice, index) in choices"
:key="index"
:ref="(el: any) => setRef(el, index)"
:class="['choice-btn', { 'has-prompt': !!choice.prompt }]"
tabindex="0"
@click="handleChoose(index)"
@keydown="onKeydown($event, index)"
>
{{ choiceText(choice) }}
</button>
</div>
</div>
</template>
<style scoped>
.choice-panel {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.85));
padding: 20px 20px 30px;
z-index: 10;
}
.timer-bar {
height: 4px;
background: rgba(255, 255, 255, 0.15);
border-radius: 2px;
margin-bottom: 16px;
position: relative;
overflow: hidden;
}
.timer-fill {
height: 100%;
background: rgba(255, 255, 255, 0.6);
transition: width 0.1s linear;
border-radius: 2px;
}
.timer-fill.danger {
background: #e74c3c;
}
.timer-text {
position: absolute;
top: -18px;
right: 0;
font-size: 12px;
color: #aaa;
}
.choice-prompt {
color: #ccc;
font-size: 14px;
text-align: center;
margin-bottom: 16px;
letter-spacing: 2px;
}
.choice-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
}
.choice-list.disabled {
opacity: 0.5;
pointer-events: none;
}
.choice-btn {
position: relative;
flex: 1;
min-width: 120px;
padding: 14px 24px;
font-size: 16px;
color: #fff;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 4px;
cursor: pointer;
transition: background 0.2s, border-color 0.2s, box-shadow 0.2s;
outline: none;
}
.choice-btn:hover {
background: rgba(255, 255, 255, 0.25);
border-color: rgba(255, 255, 255, 0.6);
}
.choice-btn:focus-visible {
background: rgba(255, 255, 255, 0.25);
border-color: rgba(255, 255, 255, 0.6);
box-shadow: 0 0 8px rgba(255, 255, 255, 0.3);
}
.choice-btn.has-prompt {
border-left: 3px solid #ffc107;
border-color: rgba(255, 193, 7, 0.4);
border-left-color: #ffc107;
}
.choice-btn.has-prompt:hover {
border-color: rgba(255, 193, 7, 0.7);
box-shadow: 0 0 12px rgba(255, 193, 7, 0.25);
}
.choice-btn.has-prompt:focus-visible {
border-color: rgba(255, 193, 7, 0.7);
box-shadow: 0 0 12px rgba(255, 193, 7, 0.3);
}
</style>