feat: 地图闯关——知识岛地图+知识关问答页,对比度微调,SEO路由,v1.2.7

This commit is contained in:
Yuzhiran Dev
2026-09-12 18:55:45 +08:00
parent 1f066a09cc
commit 92d54adabb
7 changed files with 321 additions and 110 deletions
+106
View File
@@ -0,0 +1,106 @@
<template>
<view class="map-page">
<view class="status-bar"></view>
<view class="top-bar">
<view class="user-info">
<text class="lv-icon">{{ level.icon }}</text>
<text class="lv-name">{{ level.name }} Lv{{ level.level }}</text>
</view>
<view class="coin-box">
<text class="coin-icon">💎</text>
<text class="coin-num">{{ credits }}</text>
</view>
</view>
<scroll-view scroll-y class="map-scroll">
<view class="map-body" :style="{ height: mapHeight + rpx }">
<view class="map-path-line" :style="{ height: mapHeight + rpx }"></view>
<view v-for="(island, i) in islands" :key="i"
class="island-node"
:class="{ active: island.status === current, done: island.status === done, locked: island.status === locked }"
:style="{ top: island.y + rpx, left: island.x + rpx }"
@tap="goIsland(island)">
<view class="island-icon">{{ island.icon }}</view>
<text class="island-name">{{ island.name }}</text>
<view v-if="island.status === done" class="done-star">⭐</view>
<view v-if="island.status === locked" class="lock-icon">🔒</view>
</view>
</view>
</scroll-view>
<view class="bottom-bar">
<button class="ai-btn" :disabled="!aiUnlocked" @tap="goCreate">✨ AI 生成</button>
<text v-if="!aiUnlocked" class="ai-hint">完成一个知识关后解锁</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
credits: 0,
level: { level: 1, name: "画童", icon: "🖌️" },
islands: [
{ id: 1, name: "AI 起源", icon: "🏛️", x: 100, y: 180, status: "current" },
{ id: 2, name: "发展脉络", icon: "📈", x: 380, y: 360, status: "locked" },
{ id: 3, name: "行业格局", icon: "🌍", x: 100, y: 540, status: "locked" },
{ id: 4, name: "学习工具", icon: "🛠️", x: 380, y: 720, status: "locked" },
{ id: 5, name: "趋势前沿", icon: "🚀", x: 100, y: 900, status: "locked" },
],
aiUnlocked: false,
mapHeight: 1100,
}
},
onShow() { this.loadUser() },
methods: {
loadUser() {
var token = uni.getStorageSync("dim_admin_token")
if (!token) return
uni.request({
url: "/api/me",
header: { Authorization: "Bearer " + token },
success: function(res) {
if (res.data && res.data.user) {
this.credits = res.data.user.credits || 0
}
}.bind(this)
})
},
goIsland(island) {
if (island.status !== "current") {
uni.showToast({ title: "先完成前面的知识关", icon: "none" })
return
}
uni.navigateTo({ url: "/pages/map/quiz?islandId=" + island.id + "&name=" + encodeURIComponent(island.name) })
},
goCreate() {
uni.switchTab({ url: "/pages/index/index" })
}
}
}
</script>
<style>
.map-page { background: linear-gradient(180deg, #1a1a3e 0%, #2d1b69 50%, #0d0d1a 100%); min-height: 100vh; }
.status-bar { height: 80rpx; }
.top-bar { display: flex; justify-content: space-between; align-items: center; padding: 16rpx 30rpx; }
.user-info { display: flex; align-items: center; gap: 8rpx; }
.lv-icon { font-size: 32rpx; }
.lv-name { color: #fff; font-size: 26rpx; font-weight: 600; }
.coin-box { display: flex; align-items: center; gap: 6rpx; background: rgba(255,255,255,0.1); border-radius: 999rpx; padding: 6rpx 20rpx; }
.coin-icon { font-size: 26rpx; }
.coin-num { color: #ffd700; font-size: 26rpx; font-weight: 700; }
.map-scroll { height: calc(100vh - 80rpx - 70rpx - 120rpx); }
.map-body { position: relative; width: 100%; }
.map-path-line { position: absolute; left: 50%; top: 100rpx; width: 4rpx; height: 900rpx; background: rgba(255,255,255,0.1); transform: translateX(-50%); }
.island-node { position: absolute; width: 180rpx; text-align: center; }
.island-icon { font-size: 56rpx; display: block; margin-bottom: 8rpx; }
.island-name { color: #fff; font-size: 24rpx; display: block; }
.done-star { position: absolute; top: -10rpx; right: 0; font-size: 28rpx; }
.lock-icon { position: absolute; top: -8rpx; right: -8rpx; font-size: 28rpx; }
.island-node.active .island-icon { animation: bounce 2s infinite; }
@keyframes bounce { 0%,100% { transform: translateY(0) } 50% { transform: translateY(-16rpx) } }
.bottom-bar { position: fixed; bottom: 0; left: 0; right: 0; padding: 20rpx 30rpx calc(20rpx + env(safe-area-inset-bottom)); background: rgba(0,0,0,0.5); display: flex; flex-direction: column; align-items: center; gap: 8rpx; z-index: 100; }
.ai-btn { width: 100%; background: linear-gradient(135deg, #6366f1, #a855f7); color: #fff; font-size: 32rpx; font-weight: 700; border-radius: 999rpx; padding: 20rpx; border: none; }
.ai-btn[disabled] { opacity: 0.4; }
.ai-hint { color: rgba(255,255,255,0.5); font-size: 22rpx; }
</style>
+89
View File
@@ -0,0 +1,89 @@
<template>
<view class="quiz-page">
<view class="quiz-header">
<text class="quiz-title">{{ islandName }} · 知识关</text>
<text class="quiz-progress">{{ current + 1 }} / {{ questions.length }}</text>
</view>
<view class="quiz-body" v-if="currentQ">
<text class="quiz-q">{{ currentQ.q }}</text>
<view v-for="(opt, i) in currentQ.options" :key="i"
class="quiz-opt"
:class="{ selected: selected === i, correct: submitted && i === currentQ.answer, wrong: submitted && selected === i && i !== currentQ.answer }"
@tap="pick(i)">
<text>{{ labels[i] }}. {{ opt }}</text>
</view>
<view v-if="submitted" class="quiz-result">
<text :class="isCorrect ? 'ok' : 'fail'">{{ resultText }}</text>
</view>
</view>
<view class="quiz-footer">
<button v-if="!submitted && currentQ" class="submit-btn" @tap="submit">提交答案</button>
<button v-if="submitted && current < questions.length - 1" class="next-btn" @tap="nextQ">下一题 →</button>
<button v-if="submitted && current >= questions.length - 1" class="next-btn done-btn" @tap="finish">完成知识关</button>
</view>
</view>
</template>
<script>
export default {
data() {
return {
islandName: '',
current: 0,
selected: -1,
submitted: false,
isCorrect: false,
resultText: '',
labels: ['A', 'B', 'C', 'D'],
questions: [
{ q: 'AI 的英文全称是什么?', options: ['Artificial Intelligence', 'Automatic Integration', 'Advanced Internet', 'Applied Innovation'], answer: 0 },
{ q: '深度学习属于 AI 的哪个领域?', options: ['规则推理', '机器学习', '硬件设计', '网络协议'], answer: 1 },
{ q: '以下哪个是当前主流的大语言模型?', options: ['Photoshop', 'GPT', 'Excel', 'Bluetooth'], answer: 1 },
],
}
},
computed: {
currentQ() { return this.questions[this.current] || null },
isLast() { return this.current >= this.questions.length - 1 },
},
onLoad(options) {
if (options.name) {
this.islandName = decodeURIComponent(options.name)
uni.setNavigationBarTitle({ title: this.islandName + ' · 知识关' })
}
},
methods: {
pick(i) {
if (this.submitted) return
this.selected = i
this.submitted = true
this.isCorrect = this.selected === this.questions[this.current].answer
},
nextQ() {
if (this.current < this.questions.length - 1) {
this.current++
this.selected = -1
this.submitted = false
} else {
uni.showToast({ title: '知识关完成!', icon: 'success' })
setTimeout(() => uni.navigateBack(), 1500)
}
},
}
}
</script>
<style>
.quiz-page { background: linear-gradient(180deg, #1a1a3e, #0d0d1a); min-height: 100vh; padding: 30rpx; box-sizing: border-box; }
.quiz-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30rpx; }
.quiz-title { color: #fff; font-size: 32rpx; font-weight: 700; }
.quiz-progress { color: rgba(255,255,255,0.5); font-size: 24rpx; }
.quiz-q { color: #fff; font-size: 32rpx; font-weight: 600; margin-bottom: 30rpx; display: block; }
.quiz-opt { background: rgba(255,255,255,0.08); border: 2rpx solid rgba(255,255,255,0.15); border-radius: 16rpx; padding: 24rpx 28rpx; margin-bottom: 16rpx; color: #fff; font-size: 28rpx; transition: all 0.2s; }
.quiz-opt.selected { border-color: #6366f1; background: rgba(99,102,241,0.15); }
.quiz-opt.correct { border-color: #22c55e; background: rgba(34,197,94,0.15); }
.quiz-opt.wrong { border-color: #ef4444; background: rgba(239,68,68,0.15); }
.quiz-result { text-align: center; margin-top: 20rpx; font-size: 30rpx; }
.quiz-footer { margin-top: 30rpx; }
.submit-btn { width: 100%; background: linear-gradient(135deg, #6366f1, #a855f7); color: #fff; font-size: 30rpx; font-weight: 700; border-radius: 16rpx; padding: 24rpx; border: none; margin-top: 20rpx; }
</style>