feat: play.vue 剧情流集成——StoryPlayer + 选项嵌入 + 角色回应气泡(script 空回退 v1)
This commit is contained in:
@@ -45,8 +45,39 @@
|
||||
<SpeakButton class="scene-speak" :text="detail.scene_content" :file="detail.scene_audio" label="读一读情境" />
|
||||
</view>
|
||||
|
||||
<!-- 节点:互动分发 -->
|
||||
<view v-if="curNode" class="node card">
|
||||
<!-- 节点:剧情流(script 非空)或 v1 卡片(回退) -->
|
||||
<view v-if="curNode && isStoryNode" class="node story-node">
|
||||
<StoryPlayer :script="storyScript" :character="curNode.character" :show-pinyin="showPinyin" @done="showOptions = true" />
|
||||
<view v-if="showOptions" class="story-options">
|
||||
<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" />
|
||||
<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 v-if="reply" class="story-reply card">
|
||||
<view class="reply-char">🧑🎓 你的选择</view>
|
||||
<view class="fb-option"><RubyText :text="reply.text" :pinyin="reply.textPinyin" :show="showPinyin" /></view>
|
||||
<view v-if="reply.pros" class="fb-line fb-pros fb-in"><text class="fb-mark">✓</text>{{ reply.pros }}</view>
|
||||
<view v-if="reply.cons" class="fb-line fb-cons fb-in d2"><text class="fb-mark">✗</text>{{ reply.cons }}</view>
|
||||
<view v-if="reply.prop" class="fb-prop">
|
||||
<view class="fb-prop-name">{{ reply.prop.name }}</view>
|
||||
<view v-if="reply.prop.description" class="fb-prop-desc">{{ reply.prop.description }}</view>
|
||||
</view>
|
||||
<SpeakButton :text="reply.text" :file="reply.audio" label="听点评" />
|
||||
<view class="btn-primary fb-continue" @click="continuePlay">继续 ›</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else-if="curNode" class="node card">
|
||||
<view class="node-title">{{ curNode.title }}</view>
|
||||
<view v-if="curNode.character" class="character">
|
||||
<view class="char-card">
|
||||
@@ -106,10 +137,11 @@ import { api } from '../../api/index.js'
|
||||
import { getChildId, getChildAge } from '../../store/user.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 { pickInteraction, entryProps, entryCharacter, parseScript } from '../../utils/theater.js'
|
||||
import RubyText from '../../components/RubyText.vue'
|
||||
import SpeakButton from '../../components/SpeakButton.vue'
|
||||
import SceneTheater from '../../components/SceneTheater.vue'
|
||||
import StoryPlayer from '../../components/StoryPlayer.vue'
|
||||
import ActionCard from '../../components/ActionCard.vue'
|
||||
import PropPick from '../../components/interactions/PropPick.vue'
|
||||
import FindSpot from '../../components/interactions/FindSpot.vue'
|
||||
@@ -120,7 +152,7 @@ import LinkMatch from '../../components/interactions/LinkMatch.vue'
|
||||
const RESULT_KEY = '36wisdom_result'
|
||||
|
||||
export default {
|
||||
components: { RubyText, SpeakButton, SceneTheater, ActionCard, PropPick, FindSpot, DragPlace, StepSort, LinkMatch },
|
||||
components: { RubyText, SpeakButton, SceneTheater, StoryPlayer, ActionCard, PropPick, FindSpot, DragPlace, StepSort, LinkMatch },
|
||||
data() {
|
||||
return {
|
||||
childId: getChildId(),
|
||||
@@ -128,6 +160,8 @@ export default {
|
||||
detail: null,
|
||||
curNode: null,
|
||||
feedback: null,
|
||||
showOptions: false,
|
||||
reply: null,
|
||||
choosing: false,
|
||||
showPinyin: getChildAge() === '4-6',
|
||||
routeSteps: [],
|
||||
@@ -162,6 +196,12 @@ export default {
|
||||
activeInteraction() {
|
||||
return this.curNode ? pickInteraction(this.curNode) : ''
|
||||
},
|
||||
storyScript() {
|
||||
return this.curNode ? parseScript(this.curNode.script) : null
|
||||
},
|
||||
isStoryNode() {
|
||||
return !!this.storyScript
|
||||
},
|
||||
introCharacter() {
|
||||
return this.detail ? entryCharacter(this.detail.entry) : null
|
||||
},
|
||||
@@ -210,18 +250,23 @@ export default {
|
||||
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 后弹点评
|
||||
// 分支演出:场景晃动 + 0.6s 后出反馈(剧情流=回应气泡,v1=弹窗)
|
||||
this.sceneFx = 'fx-shake'
|
||||
const fb = {
|
||||
text: o.text,
|
||||
textPinyin: o.text_pinyin,
|
||||
prop: o.prop,
|
||||
audio: o.audio,
|
||||
pros: o.feedback_pros,
|
||||
cons: o.feedback_cons,
|
||||
next: data.next
|
||||
}
|
||||
this.branchTimer = setTimeout(() => {
|
||||
this.sceneFx = ''
|
||||
this.feedback = {
|
||||
text: o.text,
|
||||
textPinyin: o.text_pinyin,
|
||||
prop: o.prop,
|
||||
audio: o.audio,
|
||||
pros: o.feedback_pros,
|
||||
cons: o.feedback_cons,
|
||||
next: data.next
|
||||
if (this.isStoryNode) {
|
||||
this.reply = fb
|
||||
} else {
|
||||
this.feedback = fb
|
||||
}
|
||||
this.choosing = false
|
||||
}, 600)
|
||||
@@ -243,8 +288,14 @@ export default {
|
||||
}
|
||||
},
|
||||
continuePlay() {
|
||||
this.curNode = this.feedback.next
|
||||
this.feedback = null
|
||||
if (this.isStoryNode) {
|
||||
this.curNode = this.reply.next
|
||||
this.reply = null
|
||||
this.showOptions = false
|
||||
} else {
|
||||
this.curNode = this.feedback.next
|
||||
this.feedback = null
|
||||
}
|
||||
this.mood = '🤔'
|
||||
}
|
||||
}
|
||||
@@ -304,6 +355,10 @@ 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; }
|
||||
.story-node { padding: 32rpx 28rpx 48rpx; }
|
||||
.story-options { margin-top: 8rpx; }
|
||||
.story-reply { margin-top: 24rpx; background: #fffdf6; animation: fb-in 0.4s ease; }
|
||||
.reply-char { font-size: 26rpx; color: #a08c74; margin-bottom: 16rpx; font-weight: 700; }
|
||||
.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>
|
||||
|
||||
Reference in New Issue
Block a user