feat: main menu component, roadmap update
This commit is contained in:
42
ROADMAP.md
42
ROADMAP.md
@@ -896,36 +896,34 @@ QTE 成功 / 到达隐藏结局 / 通关等"事件型"成就,通过在对应 e
|
||||
- [x] `src/App.vue` — 主菜单 + 游戏内"设置"按钮;Space 暂停/恢复带遮罩;QTE 参数传入引擎
|
||||
- [x] 验证:TypeScript + Vite build 通过
|
||||
|
||||
### P17 全局统计 + 主菜单 — 通关数据展示 + 统一入口(待实现)
|
||||
### P17 主菜单统一化 — 游戏入口整理 ✅ 已完成 2026-06-09
|
||||
|
||||
目标:通关后展示统计数据(线索数、结局数、QTE 成功/失败次数),所有入口整合到统一主菜单。
|
||||
目标:将当前散落在 `start-overlay` 中的 7 个按钮整合到一个 `MainMenu.vue` 统一组件中,
|
||||
游戏结束后不再只用"游戏结束"大字,而是显示同样的入口栏。
|
||||
|
||||
**全局统计数据定义:**
|
||||
**设计决策:** 全局统计面板废弃。P14 成就系统和 P15 章节回顾已覆盖玩家行为追踪需求,
|
||||
业界交互式电影游戏(Detroit / Dark Pictures / Telltale)也不做全局数字统计面板。
|
||||
|
||||
```json
|
||||
{
|
||||
"stats": [
|
||||
{ "id": "clues_found", "label": "线索发现数", "variable": "investigation", "icon": "🔍" },
|
||||
{ "id": "qte_wins", "label": "QTE 成功次数", "variable": "qte_succeeded", "icon": "🎯" },
|
||||
{ "id": "endings_count", "label": "达成结局数", "type": "endings", "icon": "🏁" }
|
||||
]
|
||||
}
|
||||
**菜单结构:**
|
||||
|
||||
```
|
||||
┌──────────────────────────────┐
|
||||
│ [中文] [English] │ ← 语言切换
|
||||
│ │
|
||||
│ [开始游戏] [继续上次进度] │
|
||||
│ │
|
||||
│ [章节] [成就] [画廊] [设置] │
|
||||
│ │
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
数据从 `StateManager.variables` 读取。`type: "endings"` 的统计项从 `visitedSceneIds ∩ endings[].sceneId` 计算。
|
||||
|
||||
**主菜单界面:**
|
||||
|
||||
统一入口组件 `MainMenu.vue`,整合:新游戏 / 继续 / 章节选择 / 成就 / 画廊 / 设置 / 语言切换。
|
||||
游戏结束后展示同样的按钮栏 + 标题"游戏结束"。
|
||||
|
||||
**实现清单:**
|
||||
|
||||
- [ ] `engine/types.ts` — `GameData.stats: StatDef[]`
|
||||
- [ ] `src/components/MainMenu.vue` — 主菜单统一入口,所有按钮整齐排列
|
||||
- [ ] `src/components/StatsPanel.vue` — 通关后统计面板
|
||||
- [ ] `src/App.vue` — 游戏结束后展示 StatsPanel;主菜单用 MainMenu 替代当前散装按钮
|
||||
- [ ] `public/scenes/demo.json` — `stats` 定义
|
||||
- [ ] 验证:主菜单入口完整、通关后统计数据正确、统计刷新后仍然准确
|
||||
- [x] `src/components/MainMenu.vue` — 统一主菜单组件:两行按钮(开始/继续 + 章节/成就/画廊/设置)+ 语言切换 + 游戏结束标题模式
|
||||
- [x] `src/App.vue` — 使用 `MainMenu` 替代散装 `start-overlay` 按钮 + `game-end-overlay`;移除 60 行旧 CSS
|
||||
- [x] 验证:TypeScript + Vite build 通过
|
||||
|
||||
## 依赖清单
|
||||
|
||||
|
||||
123
src/App.vue
123
src/App.vue
@@ -8,7 +8,7 @@ import HotspotLayer from '@/components/HotspotLayer.vue'
|
||||
import SaveLoadMenu from '@/components/SaveLoadMenu.vue'
|
||||
import ChapterSelect from '@/components/ChapterSelect.vue'
|
||||
import PlaybackBar from '@/components/PlaybackBar.vue'
|
||||
import LangSwitch from '@/components/LangSwitch.vue'
|
||||
import MainMenu from '@/components/MainMenu.vue'
|
||||
import AchievementToast from '@/components/AchievementToast.vue'
|
||||
import AchievementPanel from '@/components/AchievementPanel.vue'
|
||||
import EndingGallery from '@/components/EndingGallery.vue'
|
||||
@@ -238,21 +238,20 @@ init()
|
||||
<button class="top-btn" @click="store.setShowSettings(true)">设置</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!started" class="start-overlay">
|
||||
<LangSwitch />
|
||||
<button class="start-btn" @click="handleStart">{{ t('ui.start') }}</button>
|
||||
<button v-if="hasAutoSave" class="start-btn resume-btn" @click="handleResume">{{ t('ui.resume') }}</button>
|
||||
<button v-if="store.chapters.length > 0" class="start-btn chapters-btn" @click="openChapterSelect">{{ t('ui.chapters') }}</button>
|
||||
<button v-if="store.achievementDefs.length > 0" class="start-btn achievement-btn" @click="showAchievements = true">成就</button>
|
||||
<button v-if="store.endings.length > 0" class="start-btn gallery-btn" @click="showEndingGallery = true">画廊</button>
|
||||
<button class="start-btn settings-btn" @click="store.setShowSettings(true)">设置</button>
|
||||
</div>
|
||||
<div v-if="store.gameEnded" class="game-end-overlay">
|
||||
<div class="game-end-text">{{ t('ui.gameEnd') }}</div>
|
||||
<div v-if="store.chapters.length > 0" class="game-end-actions">
|
||||
<button class="end-btn" @click="openChapterSelect">{{ t('ui.chapters') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<MainMenu
|
||||
v-if="!started || store.gameEnded"
|
||||
:show-resume="!store.gameEnded && hasAutoSave"
|
||||
:show-chapters="store.chapters.length > 0"
|
||||
:show-achievements="store.achievementDefs.length > 0"
|
||||
:show-gallery="store.endings.length > 0"
|
||||
:is-game-end="store.gameEnded"
|
||||
@start="handleStart"
|
||||
@resume="handleResume"
|
||||
@chapters="openChapterSelect"
|
||||
@achievements="showAchievements = true"
|
||||
@gallery="showEndingGallery = true"
|
||||
@settings="store.setShowSettings(true)"
|
||||
/>
|
||||
<SaveLoadMenu
|
||||
v-if="showMenu"
|
||||
:saves="store.saves"
|
||||
@@ -372,75 +371,6 @@ html, body {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.game-end-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.game-end-text {
|
||||
font-size: 36px;
|
||||
letter-spacing: 4px;
|
||||
}
|
||||
|
||||
.start-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.start-btn {
|
||||
padding: 18px 48px;
|
||||
font-size: 20px;
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
letter-spacing: 4px;
|
||||
transition: background 0.2s;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.start-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.resume-btn {
|
||||
border-color: rgba(100, 200, 255, 0.3);
|
||||
color: #8cf;
|
||||
}
|
||||
|
||||
.chapters-btn {
|
||||
border-color: rgba(255, 200, 100, 0.3);
|
||||
color: #fc8;
|
||||
}
|
||||
|
||||
.achievement-btn {
|
||||
border-color: rgba(255, 193, 7, 0.3);
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.gallery-btn {
|
||||
border-color: rgba(156, 39, 176, 0.3);
|
||||
color: #ce93d8;
|
||||
}
|
||||
|
||||
.settings-btn {
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.pause-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@@ -465,29 +395,6 @@ html, body {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.game-end-actions {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.end-btn {
|
||||
padding: 14px 32px;
|
||||
font-size: 18px;
|
||||
color: #fc8;
|
||||
background: rgba(255, 200, 100, 0.08);
|
||||
border: 1px solid rgba(255, 200, 100, 0.2);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
letter-spacing: 2px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.end-btn:hover {
|
||||
background: rgba(255, 200, 100, 0.15);
|
||||
}
|
||||
|
||||
.prompt-toast {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
|
||||
135
src/components/MainMenu.vue
Normal file
135
src/components/MainMenu.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import LangSwitch from '@/components/LangSwitch.vue'
|
||||
|
||||
defineProps<{
|
||||
showResume: boolean
|
||||
showChapters: boolean
|
||||
showAchievements: boolean
|
||||
showGallery: boolean
|
||||
isGameEnd: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
start: []
|
||||
resume: []
|
||||
chapters: []
|
||||
achievements: []
|
||||
gallery: []
|
||||
settings: []
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="main-menu-overlay">
|
||||
<div class="main-menu">
|
||||
<div class="menu-top-bar">
|
||||
<LangSwitch />
|
||||
</div>
|
||||
|
||||
<h1 v-if="isGameEnd" class="menu-title">{{ t('ui.gameEnd') }}</h1>
|
||||
|
||||
<div class="menu-row primary-row">
|
||||
<button class="menu-btn primary" @click="emit('start')">{{ t('ui.start') }}</button>
|
||||
<button v-if="showResume" class="menu-btn secondary" @click="emit('resume')">{{ t('ui.resume') }}</button>
|
||||
</div>
|
||||
|
||||
<div class="menu-row secondary-row">
|
||||
<button v-if="showChapters" class="menu-btn tertiary" @click="emit('chapters')">{{ t('ui.chapters') }}</button>
|
||||
<button v-if="showAchievements" class="menu-btn tertiary" @click="emit('achievements')">成就</button>
|
||||
<button v-if="showGallery" class="menu-btn tertiary" @click="emit('gallery')">画廊</button>
|
||||
<button class="menu-btn tertiary" @click="emit('settings')">设置</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.main-menu-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.88);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.main-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.menu-top-bar {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
font-size: 36px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 6px;
|
||||
color: #ddd;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.menu-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.menu-btn {
|
||||
padding: 16px 40px;
|
||||
font-size: 18px;
|
||||
color: #fff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
letter-spacing: 2px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.menu-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.menu-btn.primary {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 18px 56px;
|
||||
font-size: 22px;
|
||||
letter-spacing: 4px;
|
||||
}
|
||||
|
||||
.menu-btn.primary:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.menu-btn.secondary {
|
||||
background: rgba(100, 200, 255, 0.08);
|
||||
border-color: rgba(100, 200, 255, 0.3);
|
||||
color: #8cf;
|
||||
font-size: 16px;
|
||||
padding: 16px 36px;
|
||||
}
|
||||
|
||||
.menu-btn.secondary:hover {
|
||||
background: rgba(100, 200, 255, 0.15);
|
||||
}
|
||||
|
||||
.menu-btn.tertiary {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
color: #aaa;
|
||||
font-size: 14px;
|
||||
padding: 12px 28px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.menu-btn.tertiary:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #ddd;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user