feat: P1 core - seamless video switching, conditional branches, save/load
- VideoManager: A/B dual-buffered video with crossfade transitions and candidate preloading - Engine: condition-based choice filtering, ChoiceSystem timer, resumeScene for save/load - SceneManager: getCandidateUrls for preloading next scenes - SaveSystem: Dexie.js IndexedDB multi-slot save/load - ChoiceSystem: timed choices with countdown and auto-default on timeout - GamePlayer: dual video elements with crossfade CSS - ChoicePanel: timer progress bar and countdown text - SaveLoadMenu: save/load UI component - App.vue: menu trigger, dual video refs, save/load integration - gameStore: timer state, saves list - demo.json: conditional choice example (secret ending, requires trust >= 80) - ROADMAP: mark P1 as completed
This commit is contained in:
157
src/components/SaveLoadMenu.vue
Normal file
157
src/components/SaveLoadMenu.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import type { SlotInfo } from '@/stores/gameStore'
|
||||
|
||||
const props = defineProps<{
|
||||
saves: SlotInfo[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
save: [slot: number]
|
||||
load: [slot: number]
|
||||
close: []
|
||||
}>()
|
||||
|
||||
const maxSlots = 5
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="save-overlay" @click.self="emit('close')">
|
||||
<div class="save-panel">
|
||||
<h2 class="save-title">存档 / 读档</h2>
|
||||
|
||||
<div class="slot-list">
|
||||
<div
|
||||
v-for="slot in maxSlots"
|
||||
:key="slot"
|
||||
class="save-slot"
|
||||
>
|
||||
<div class="slot-label">存档 {{ slot }}</div>
|
||||
<div class="slot-info" v-if="saves.find(s => s.slot === slot)">
|
||||
{{ saves.find(s => s.slot === slot)!.sceneLabel }}
|
||||
</div>
|
||||
<div class="slot-info empty" v-else>空</div>
|
||||
<div class="slot-actions">
|
||||
<button class="slot-btn save-btn" @click="emit('save', slot)">保存</button>
|
||||
<button
|
||||
class="slot-btn load-btn"
|
||||
:disabled="!saves.find(s => s.slot === slot)"
|
||||
@click="emit('load', slot)"
|
||||
>
|
||||
读取
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="close-btn" @click="emit('close')">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.save-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.save-panel {
|
||||
background: #1a1a2e;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 32px;
|
||||
min-width: 400px;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.save-title {
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 2px;
|
||||
margin-bottom: 24px;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.slot-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.save-slot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.slot-label {
|
||||
font-size: 14px;
|
||||
color: #aaa;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.slot-info {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
color: #ccc;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.slot-info.empty {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.slot-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.slot-btn {
|
||||
padding: 6px 14px;
|
||||
font-size: 13px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #ddd;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.slot-btn:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.slot-btn:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
display: block;
|
||||
margin: 24px auto 0;
|
||||
padding: 10px 32px;
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
color: #ccc;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user