Files
36Wisdom/ui-src/src/components/SceneTheater.vue
T

148 lines
6.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="theater" :style="{ background: sceneColor + 'ee' }">
<image v-if="sceneImage" class="theater-bg" :src="sceneImage" mode="aspectFill" />
<view class="theater-skip" @click="finish">跳过 </view>
<view class="theater-scene">
<view class="theater-emoji">{{ sceneEmoji }}</view>
<view class="theater-name">{{ sceneName }}</view>
</view>
<!-- 道具飞入右侧逐个 -->
<view class="theater-props">
<view
v-for="(p, i) in props"
:key="i"
class="theater-prop"
:style="{ animationDelay: 0.6 + i * 0.3 + 's' }"
>{{ propEmoji(p) }}</view>
</view>
<!-- 角色滑入左侧 -->
<view v-if="character" class="theater-char">
<image v-if="character.image" class="theater-char-img" :src="character.image" mode="aspectFit" />
<view v-else class="theater-char-avatar" :style="{ background: charColor }">{{ character.name.slice(0, 1) }}</view>
<view class="theater-char-mood">🤔</view>
</view>
<!-- 旁白字幕逐字淡入 + 拼音 -->
<view class="theater-sub card">
<view class="theater-sub-head">
<text class="theater-sub-label">📖 情境</text>
<text class="theater-sub-state">{{ speaking ? '正在朗读…' : '' }}</text>
</view>
<view class="theater-sub-ruby">
<view
v-for="(ch, i) in pairs"
:key="i"
class="theater-unit"
:style="{ animationDelay: i * 0.05 + 's' }"
>
<text v-if="ch.p && showPinyin" class="theater-py">{{ ch.p }}</text>
<text class="theater-ch">{{ ch.c }}</text>
</view>
</view>
</view>
</view>
</template>
<script>
import { parsePinyin, zipText } from '../utils/pinyin.js'
import { propVisual, personVisual } from '../utils/visual.js'
import { speak, stopSpeak } from '../utils/speech.js'
export default {
name: 'SceneTheater',
props: {
sceneName: { type: String, default: '' },
sceneEmoji: { type: String, default: '🏞️' },
sceneColor: { type: String, default: '#ffd08a' },
sceneImage: { type: String, default: '' },
content: { type: String, default: '' },
contentPinyin: { type: String, default: '' },
audio: { type: String, default: '' },
character: { type: Object, default: null }, // {name, image, id}
props: { type: Array, default: () => [] }, // 道具名数组
showPinyin: { type: Boolean, default: true }
},
emits: ['done'],
data() {
return { speaking: false }
},
computed: {
pairs() {
return zipText(this.content, parsePinyin(this.contentPinyin))
},
charColor() {
return personVisual(this.character && this.character.id ? this.character.id : 0).color
}
},
mounted() {
this.speaking = true
// v2.1:朗读结束自动进入决策幕——最短展示 1.2s,之后 speak onEnd 即进入;
// 无 TTS/onEnd 缺失最迟 15s 兜底(「跳过」仍可手动跳过)
this.speechDone = false
this.ready = false
let done = false
const finishAuto = () => {
if (done) return
done = true
this.speaking = false
this.$emit('done')
}
this.minTimer = setTimeout(() => { this.ready = true; if (this.speechDone) finishAuto() }, 1200)
this.autoTimer = setTimeout(finishAuto, 15000)
const onSpeechEnd = () => {
this.speechDone = true
if (this.ready) finishAuto()
}
if (!this.content) {
finishAuto()
return
}
speak(this.content, this.audio, onSpeechEnd)
},
beforeUnmount() {
clearTimeout(this.minTimer)
clearTimeout(this.autoTimer)
stopSpeak()
},
methods: {
finish() {
stopSpeak()
this.$emit('done')
},
propEmoji(name) {
return propVisual(name).emoji
}
}
}
</script>
<style scoped>
.theater { position: fixed; inset: 0; z-index: 30; padding: 120rpx 48rpx 80rpx; display: flex; flex-direction: column; overflow: hidden; }
.theater-bg { position: absolute; inset: 0; width: 100%; height: 100%; }
.theater-skip { position: absolute; top: calc(24rpx + env(safe-area-inset-top)); right: 32rpx; font-size: 28rpx; color: #5b4636; background: rgba(255, 255, 255, 0.8); border-radius: 30rpx; padding: 10rpx 28rpx; z-index: 2; }
.theater-scene { display: flex; align-items: center; justify-content: center; gap: 16rpx; margin-bottom: 48rpx; animation: t-pop 0.6s ease; }
.theater-emoji { font-size: 88rpx; }
.theater-name { font-size: 44rpx; font-weight: 800; color: #5b4636; }
@keyframes t-pop { 0% { opacity: 0; transform: scale(0.5); } 70% { transform: scale(1.1); } 100% { opacity: 1; transform: scale(1); } }
.theater-props { position: absolute; top: 40%; right: 32rpx; display: flex; flex-direction: column; gap: 24rpx; z-index: 1; }
.theater-prop { font-size: 64rpx; opacity: 0; animation: prop-fly 0.8s ease forwards; }
@keyframes prop-fly { 0% { opacity: 0; transform: translateX(140rpx) rotate(40deg); } 70% { transform: translateX(-16rpx) rotate(-10deg); } 100% { opacity: 1; transform: translateX(0) rotate(0); } }
.theater-char { position: absolute; left: 32rpx; bottom: 36%; z-index: 1; opacity: 0; animation: char-in 0.9s cubic-bezier(0.34, 1.56, 0.64, 1) 0.2s forwards; }
.theater-char-img { width: 180rpx; height: 180rpx; }
.theater-char-avatar { width: 160rpx; height: 160rpx; border-radius: 50%; color: #fff; font-size: 72rpx; font-weight: 800; display: flex; align-items: center; justify-content: center; }
.theater-char-mood { position: absolute; right: -16rpx; bottom: -16rpx; font-size: 56rpx; }
@keyframes char-in { 0% { opacity: 0; transform: translateX(-220rpx); } 70% { transform: translateX(16rpx); } 100% { opacity: 1; transform: translateX(0); } }
.theater-sub { margin-top: auto; padding: 32rpx 36rpx; position: relative; z-index: 2; }
.theater-sub-head { display: flex; justify-content: space-between; margin-bottom: 12rpx; }
.theater-sub-label { font-size: 26rpx; color: #a08c74; font-weight: 700; }
.theater-sub-state { font-size: 24rpx; color: #ff6b35; }
.theater-sub-ruby { display: flex; flex-wrap: wrap; line-height: 1.9; }
.theater-unit { display: flex; flex-direction: column; align-items: center; margin: 0 2rpx; opacity: 0; animation: t-word 0.4s ease forwards; }
@keyframes t-word { 0% { opacity: 0; transform: translateY(12rpx); } 100% { opacity: 1; transform: translateY(0); } }
.theater-py { font-size: 20rpx; color: #ff6b35; line-height: 1.3; height: 28rpx; }
.theater-ch { font-size: 34rpx; line-height: 1.5; color: #5b4636; }
</style>