feat: 闯关页剧场化(开场演出覆盖层+动作卡呈现+分支演出晃动过渡)

This commit is contained in:
2026-08-13 15:37:51 +08:00
parent cd8b176ef1
commit b48bb41955
+62 -36
View File
@@ -9,6 +9,22 @@
</view>
</view>
<!-- 开场演出覆盖层播完/跳过进入决策 -->
<SceneTheater
v-if="showIntro && detail"
:scene-name="detail.scene ? detail.scene.name : ''"
:scene-emoji="scene.emoji"
:scene-color="scene.color"
:scene-image="detail.scene_image"
:content="detail.scene_content"
:content-pinyin="detail.scene_content_pinyin"
:audio="detail.scene_audio"
:character="introCharacter"
:props="introProps"
:show-pinyin="showPinyin"
@done="showIntro = false"
/>
<view v-if="detail" class="content">
<!-- 进度点每走一步点亮一个 -->
<view class="progress-dots">
@@ -16,7 +32,7 @@
</view>
<!-- 场景面板环境 + 情境描述 + 朗读 + 漂浮装饰 -->
<view class="scene-card card" :style="{ background: scene.color + '44' }">
<view class="scene-card card" :class="{ 'fx-shake': sceneFx }" :style="{ background: scene.color + '44' }">
<image v-if="sceneImg" class="scene-img" :src="sceneImg" mode="aspectFill" />
<view class="scene-float f1"></view>
<view class="scene-float f2"></view>
@@ -48,32 +64,21 @@
</view>
<SpeakButton class="node-speak" :text="curNode.content" :file="curNode.audio" label="读一读" />
<!-- 互动组件触控 5 -->
<PropPick v-if="curNode.interaction_type === 2" :config="curNode.config" @done="interactionDone" />
<FindSpot v-else-if="curNode.interaction_type === 7" :config="curNode.config" @done="interactionDone" />
<DragPlace v-else-if="curNode.interaction_type === 5" :config="curNode.config" @done="interactionDone" />
<StepSort v-else-if="curNode.interaction_type === 3" :config="curNode.config" @done="interactionDone" />
<LinkMatch v-else-if="curNode.interaction_type === 8" :config="curNode.config" @done="interactionDone" />
<!-- 互动组件触控 5 config 互动节点 -->
<PropPick v-if="activeInteraction === 'PropPick'" :config="curNode.config" @done="interactionDone" />
<FindSpot v-else-if="activeInteraction === 'FindSpot'" :config="curNode.config" @done="interactionDone" />
<DragPlace v-else-if="activeInteraction === 'DragPlace'" :config="curNode.config" @done="interactionDone" />
<StepSort v-else-if="activeInteraction === 'StepSort'" :config="curNode.config" @done="interactionDone" />
<LinkMatch v-else-if="activeInteraction === 'LinkMatch'" :config="curNode.config" @done="interactionDone" />
<!-- 选项选择type 1 -->
<view v-else-if="curNode.interaction_type === 1" class="options">
<view
v-for="(o, idx) in curNode.options"
:key="o.option_id"
class="option"
:class="['c' + (idx + 1), { disabled: choosing }]"
@click="choose(o, idx)"
>
<view class="option-mark">{{ ['A', 'B', 'C', 'D'][idx] }}</view>
<view v-if="o.prop" class="option-icon">
<image v-if="o.prop.image" class="option-icon-img" :src="o.prop.image" mode="aspectFit" />
<text v-else class="option-icon-emoji">{{ propEmoji(o.prop.name) }}</text>
</view>
<view class="option-text">
<RubyText :text="o.text" :pinyin="o.text_pinyin" :show="showPinyin" />
</view>
</view>
</view>
<!-- 动作卡普通决策节点多分支选项语义完整保留 -->
<ActionCard
v-else-if="activeInteraction === 'ActionCard'"
:node="curNode"
:show-pinyin="showPinyin"
:disabled="choosing"
@choose="choose"
/>
<view v-else class="soon card">该互动玩法类型 {{ curNode.interaction_type }}敬请期待</view>
</view>
</view>
@@ -99,10 +104,13 @@
<script>
import { api } from '../../api/index.js'
import { getChildId, getChildAge } from '../../store/user.js'
import { sceneVisual, personVisual, propVisual } from '../../utils/visual.js'
import { sceneVisual, personVisual } from '../../utils/visual.js'
import { playSound, isSoundEnabled, setSoundEnabled } from '../../utils/sound.js'
import { pickInteraction, entryProps, entryCharacter } from '../../utils/theater.js'
import RubyText from '../../components/RubyText.vue'
import SpeakButton from '../../components/SpeakButton.vue'
import SceneTheater from '../../components/SceneTheater.vue'
import ActionCard from '../../components/ActionCard.vue'
import PropPick from '../../components/interactions/PropPick.vue'
import FindSpot from '../../components/interactions/FindSpot.vue'
import DragPlace from '../../components/interactions/DragPlace.vue'
@@ -112,7 +120,7 @@ import LinkMatch from '../../components/interactions/LinkMatch.vue'
const RESULT_KEY = '36wisdom_result'
export default {
components: { RubyText, SpeakButton, PropPick, FindSpot, DragPlace, StepSort, LinkMatch },
components: { RubyText, SpeakButton, SceneTheater, ActionCard, PropPick, FindSpot, DragPlace, StepSort, LinkMatch },
data() {
return {
childId: getChildId(),
@@ -125,7 +133,9 @@ export default {
routeSteps: [],
mood: '🤔',
combo: 0,
soundOn: isSoundEnabled()
soundOn: isSoundEnabled(),
showIntro: false,
sceneFx: ''
}
},
onLoad(options) {
@@ -144,6 +154,15 @@ export default {
},
stepCount() {
return Math.min(this.routeSteps.length + 1, 6)
},
activeInteraction() {
return this.curNode ? pickInteraction(this.curNode) : ''
},
introCharacter() {
return this.detail ? entryCharacter(this.detail.entry) : null
},
introProps() {
return this.detail ? entryProps(this.detail.entry) : []
}
},
methods: {
@@ -152,6 +171,7 @@ export default {
const data = await api.levelDetail(this.childId, this.levelId)
this.detail = data
this.curNode = data.entry
this.showIntro = true
} catch (e) {
setTimeout(() => this.goBack(), 800)
}
@@ -168,9 +188,6 @@ export default {
const opt = success ? this.curNode.options[0] : this.curNode.options[1]
if (opt) this.submit(opt)
},
propEmoji(name) {
return propVisual(name).emoji
},
toggleSound() {
this.soundOn = !this.soundOn
setSoundEnabled(this.soundOn)
@@ -186,6 +203,13 @@ export default {
const data = await api.levelChoose(this.childId, this.levelId, this.curNode.node_id, o.option_id)
this.routeSteps.push({ text: o.text, pros: o.feedback_pros || '' })
if (data.next) {
this.mood = o.feedback_cons ? '😢' : '😊'
this.combo = o.feedback_cons ? 0 : Math.min(this.combo + 1, 9)
playSound(o.feedback_cons ? 'bad' : 'good')
// 分支演出:场景晃动 + 0.6s 后弹点评
this.sceneFx = 'fx-shake'
setTimeout(() => {
this.sceneFx = ''
this.feedback = {
text: o.text,
textPinyin: o.text_pinyin,
@@ -195,9 +219,8 @@ export default {
cons: o.feedback_cons,
next: data.next
}
this.mood = o.feedback_cons ? '😢' : '😊'
this.combo = o.feedback_cons ? 0 : Math.min(this.combo + 1, 9)
playSound(o.feedback_cons ? 'bad' : 'good')
this.choosing = false
}, 600)
} else if (data.final) {
uni.setStorageSync(RESULT_KEY, {
level_id: this.levelId,
@@ -208,9 +231,10 @@ export default {
})
playSound('finish')
uni.redirectTo({ url: '/pages/result/result' })
} else {
this.choosing = false
}
} catch (e) {
} finally {
this.choosing = false
}
},
@@ -289,4 +313,6 @@ export default {
.fb-prop-name { font-size: 30rpx; font-weight: 700; color: #7a6248; margin-bottom: 8rpx; }
.fb-prop-desc { font-size: 26rpx; line-height: 1.7; color: #a08c74; }
.fb-continue { margin-top: 24rpx; font-size: 30rpx; }
.fx-shake { animation: fx-shake 0.5s ease; }
@keyframes fx-shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-12rpx); } 50% { transform: translateX(10rpx); } 75% { transform: translateX(-6rpx); } }
</style>