Files
36Wisdom/ui-src/src/pages/play/play.vue
T

266 lines
12 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="page">
<view class="topbar">
<view class="back" @click="goBack"></view>
<view class="top-title">{{ detail ? detail.title : '' }}</view>
<view class="top-actions">
<view class="py-toggle" @click="showPinyin = !showPinyin">{{ showPinyin ? '拼音开' : '拼音关' }}</view>
<view class="sound-toggle" @click="toggleSound">{{ soundOn ? '🔊' : '🔇' }}</view>
</view>
</view>
<view v-if="detail" class="content">
<!-- 进度点每走一步点亮一个 -->
<view class="progress-dots">
<view v-for="i in 6" :key="i" class="dot" :class="{ on: i <= stepCount }"></view>
</view>
<!-- 场景面板环境 + 情境描述 + 朗读 + 漂浮装饰 -->
<view class="scene-card card" :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>
<view class="scene-head">
<view class="scene-emoji">{{ scene.emoji }}</view>
<view class="scene-name">{{ detail.scene ? detail.scene.name : '' }}</view>
<view v-if="combo > 1" class="combo-badge">🔥 ×{{ combo }}</view>
</view>
<RubyText class="scene-text" :text="detail.scene_content" :pinyin="detail.scene_content_pinyin" :show="showPinyin" />
<SpeakButton class="scene-speak" :text="detail.scene_content" :file="detail.scene_audio" label="读一读情境" />
</view>
<!-- 节点互动分发 -->
<view v-if="curNode" class="node card">
<view class="node-title">{{ curNode.title }}</view>
<view v-if="curNode.character" class="character">
<view class="char-card">
<image v-if="charImg" class="char-img" :src="charImg" mode="aspectFit" />
<view v-else class="char-avatar" :style="{ background: personColor(curNode.character.id) }">{{ curNode.character.name.slice(0, 1) }}</view>
<view class="char-mood">{{ mood }}</view>
</view>
<view class="char-info">
<view class="char-name">{{ curNode.character.name }}</view>
<SpeakButton :text="curNode.character.name" :file="curNode.character.audio" label="读一读" />
</view>
</view>
<view class="node-content">
<RubyText :text="curNode.content" :pinyin="curNode.content_pinyin" :show="showPinyin" />
</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" />
<!-- 选项选择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>
<view v-else class="soon card">该互动玩法类型 {{ curNode.interaction_type }}敬请期待</view>
</view>
</view>
<!-- 反馈面板你的选择 + 好处 + 对比对比点评三段式 -->
<view v-if="feedback" class="mask">
<view class="feedback card">
<view class="fb-title">你的选择</view>
<view class="fb-option"><RubyText :text="feedback.text" :pinyin="feedback.textPinyin" :show="showPinyin" /></view>
<view v-if="feedback.pros" class="fb-line fb-pros fb-in"><text class="fb-mark"></text>{{ feedback.pros }}</view>
<view v-if="feedback.cons" class="fb-line fb-cons fb-in d2"><text class="fb-mark"></text>{{ feedback.cons }}</view>
<view v-if="feedback.prop" class="fb-prop">
<view class="fb-prop-name">{{ feedback.prop.name }}</view>
<view v-if="feedback.prop.description" class="fb-prop-desc">{{ feedback.prop.description }}</view>
</view>
<SpeakButton :text="feedback.text" :file="feedback.audio" label="听点评" />
<view class="btn-primary fb-continue" @click="continuePlay">继续 </view>
</view>
</view>
</view>
</template>
<script>
import { api } from '../../api/index.js'
import { getChildId, getChildAge } from '../../store/user.js'
import { sceneVisual, personVisual, propVisual } from '../../utils/visual.js'
import { playSound, isSoundEnabled, setSoundEnabled } from '../../utils/sound.js'
import RubyText from '../../components/RubyText.vue'
import SpeakButton from '../../components/SpeakButton.vue'
import PropPick from '../../components/interactions/PropPick.vue'
import FindSpot from '../../components/interactions/FindSpot.vue'
import DragPlace from '../../components/interactions/DragPlace.vue'
import StepSort from '../../components/interactions/StepSort.vue'
import LinkMatch from '../../components/interactions/LinkMatch.vue'
const RESULT_KEY = '36wisdom_result'
export default {
components: { RubyText, SpeakButton, PropPick, FindSpot, DragPlace, StepSort, LinkMatch },
data() {
return {
childId: getChildId(),
levelId: 0,
detail: null,
curNode: null,
feedback: null,
choosing: false,
showPinyin: getChildAge() === '4-6',
routeSteps: [],
mood: '🤔',
combo: 0,
soundOn: isSoundEnabled()
}
},
onLoad(options) {
this.levelId = Number(options.level_id || 0)
this.load()
},
computed: {
scene() {
return sceneVisual(this.detail && this.detail.scene ? this.detail.scene.name : '')
},
sceneImg() {
return this.detail && this.detail.scene && this.detail.scene.image ? this.detail.scene.image : ''
},
charImg() {
return this.curNode && this.curNode.character && this.curNode.character.image ? this.curNode.character.image : ''
},
stepCount() {
return Math.min(this.routeSteps.length + 1, 6)
}
},
methods: {
async load() {
try {
const data = await api.levelDetail(this.childId, this.levelId)
this.detail = data
this.curNode = data.entry
} catch (e) {
setTimeout(() => this.goBack(), 800)
}
},
goBack() {
uni.navigateBack({ fail: () => uni.reLaunch({ url: '/pages/map/map' }) })
},
personColor(id) {
return personVisual(id).color
},
// 触控互动判定完成:提交成功/失败出口选项
interactionDone({ success }) {
this.mood = success ? '😊' : '😢'
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)
},
choose(o) {
if (this.choosing) return
playSound('click')
this.submit(o)
},
async submit(o) {
this.choosing = true
try {
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.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
}
this.mood = o.feedback_cons ? '😢' : '😊'
this.combo = o.feedback_cons ? 0 : Math.min(this.combo + 1, 9)
playSound(o.feedback_cons ? 'bad' : 'good')
} else if (data.final) {
uni.setStorageSync(RESULT_KEY, {
level_id: this.levelId,
title: this.detail.title,
scene_name: this.detail.scene ? this.detail.scene.name : '',
route_steps: this.routeSteps,
final: data.final
})
playSound('finish')
uni.redirectTo({ url: '/pages/result/result' })
}
} catch (e) {
} finally {
this.choosing = false
}
},
continuePlay() {
this.curNode = this.feedback.next
this.feedback = null
this.mood = '🤔'
}
}
}
</script>
<style scoped>
.page { min-height: 100vh; display: flex; flex-direction: column; }
.topbar { display: flex; align-items: center; padding: 24rpx 32rpx; padding-top: calc(24rpx + env(safe-area-inset-top)); }
.back { font-size: 56rpx; color: #ff6b35; width: 72rpx; height: 72rpx; display: flex; align-items: center; }
.top-title { flex: 1; text-align: center; font-size: 32rpx; font-weight: 700; color: #5b4636; }
.py-toggle { font-size: 24rpx; color: #ff6b35; background: #fff0e5; border-radius: 30rpx; padding: 8rpx 20rpx; }
.content { flex: 1; padding: 24rpx 32rpx 60rpx; }
.scene-card { padding: 32rpx; margin-bottom: 24rpx; }
.scene-head { display: flex; align-items: center; margin-bottom: 16rpx; }
.scene-emoji { font-size: 64rpx; margin-right: 20rpx; }
.scene-name { font-size: 32rpx; font-weight: 800; color: #5b4636; }
.scene-text { margin-bottom: 20rpx; }
.scene-speak { margin-top: 8rpx; }
.node { padding: 48rpx 40rpx; }
.node-title { font-size: 38rpx; font-weight: 800; color: #ff6b35; margin-bottom: 24rpx; }
.character { display: flex; align-items: center; margin-bottom: 24rpx; }
.char-avatar { width: 72rpx; height: 72rpx; border-radius: 50%; color: #fff; font-size: 32rpx; font-weight: 700; display: flex; align-items: center; justify-content: center; margin-right: 20rpx; }
.char-name { font-size: 28rpx; font-weight: 700; color: #7a6248; margin-right: 20rpx; }
.node-content { font-size: 32rpx; line-height: 1.9; color: #5b4636; margin-bottom: 24rpx; }
.node-speak { margin-bottom: 32rpx; }
.options { display: flex; flex-direction: column; gap: 20rpx; }
.option { display: flex; align-items: center; background: #f8f2e8; border: 4rpx solid transparent; border-radius: 24rpx; padding: 26rpx 28rpx; }
.option.disabled { opacity: 0.6; }
.option:active { border-color: #ff9a3d; }
.option-mark { width: 56rpx; height: 56rpx; border-radius: 50%; background: #ff9a3d; color: #fff; font-weight: 800; display: flex; align-items: center; justify-content: center; margin-right: 24rpx; flex-shrink: 0; }
.option-text { font-size: 30rpx; font-weight: 600; color: #5b4636; }
.soon { padding: 40rpx; text-align: center; color: #a08c74; background: #f8f2e8; }
.mask { position: fixed; inset: 0; background: rgba(91, 70, 54, 0.5); display: flex; align-items: center; justify-content: center; z-index: 10; }
.feedback { width: 82%; padding: 48rpx 40rpx; }
.fb-title { font-size: 26rpx; color: #a08c74; margin-bottom: 16rpx; }
.fb-option { font-size: 34rpx; font-weight: 800; color: #ff6b35; margin-bottom: 24rpx; }
.fb-line { font-size: 28rpx; line-height: 1.7; border-radius: 16rpx; padding: 16rpx 24rpx; margin-bottom: 16rpx; }
.fb-pros { background: #e8f7f3; color: #1fa98f; }
.fb-cons { background: #fff0e5; color: #e07b39; }
.fb-mark { font-weight: 800; margin-right: 8rpx; }
.fb-prop { background: #fff7ec; border-radius: 20rpx; padding: 24rpx 28rpx; margin-bottom: 32rpx; }
.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; }
</style>