文字游戏
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

630 lines
14 KiB

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useCharacterStore } from '@/stores/character'
import { scrapApi, type ScrapScanResultDto } from '@/api/scrap'
import Particles from '@/components/Particles/Particles.vue'
import GlareHover from '@/components/GlareHover/GlareHover.vue'
const router = useRouter()
const characterStore = useCharacterStore()
const isScanning = ref(false)
const countdown = ref(0)
const countdownInterval = ref<number | null>(null)
const scanResult = ref<ScrapScanResultDto | null>(null)
const showStory = ref(false)
const isLoading = ref(false)
const errorMessage = ref('')
const characterId = computed(() => characterStore.currentCharacter?.id ?? 0)
const startScanning = async () => {
if (isScanning.value || !characterId.value) return
isLoading.value = true
errorMessage.value = ''
scanResult.value = null
showStory.value = false
const minSeconds = 5
const maxSeconds = 10
countdown.value = Math.floor(Math.random() * (maxSeconds - minSeconds + 1)) + minSeconds
isScanning.value = true
countdownInterval.value = window.setInterval(async () => {
countdown.value--
if (countdown.value <= 0) {
if (countdownInterval.value) {
clearInterval(countdownInterval.value)
countdownInterval.value = null
}
await performScan()
}
}, 1000)
}
const performScan = async () => {
try {
const result = await scrapApi.scanScrap(characterId.value)
scanResult.value = result
await characterStore.fetchCharacters()
const updatedChar = characterStore.characters.find(c => c.id === characterId.value)
if (updatedChar) {
characterStore.currentCharacter = updatedChar
localStorage.setItem('current_character', JSON.stringify(updatedChar))
}
} catch (error: unknown) {
errorMessage.value = error instanceof Error ? error.message : String(error)
} finally {
isScanning.value = false
isLoading.value = false
}
}
const cancelScanning = () => {
if (countdownInterval.value) {
clearInterval(countdownInterval.value)
countdownInterval.value = null
}
isScanning.value = false
countdown.value = 0
}
const viewStory = () => {
showStory.value = true
}
const hideStory = () => {
showStory.value = false
}
const goBack = () => {
router.push('/game')
}
const getLevelColor = (color: string) => {
const colorMap: Record<string, string> = {
'#FFFFFF': '#cccccc',
'#00FF00': '#00ff00',
'#0077FF': '#0077ff',
'#9932CC': '#9932cc',
'#FF8C00': '#ff8c00'
}
return colorMap[color] || '#ffffff'
}
</script>
<template>
<div class="scrap-page">
<Particles
:particle-count="50"
:particle-colors="['#ffffff', '#aaaaaa']"
class="particles-bg"
/>
<div class="scrap-container">
<div class="header">
<button class="back-btn" @click="goBack">
<span class="back-arrow"></span>
<span>返回</span>
</button>
<h1 class="title">捡垃圾</h1>
<div class="spacer"></div>
</div>
<div class="content">
<div v-if="errorMessage" class="error-message">
{{ errorMessage }}
</div>
<div v-if="!scanResult && !isScanning" class="scan-prompt">
<div class="scan-icon">🔍</div>
<p class="scan-text">在废墟中搜寻有价值的物品...</p>
<GlareHover
width="200px"
height="50px"
background="rgba(255,255,255,0.05)"
border-radius="25px"
border-color="rgba(255,255,255,0.15)"
glare-color="#ffffff"
:glare-opacity="0.15"
class="scan-btn"
@click="startScanning"
>
<span class="scan-btn-text">开始扫描</span>
</GlareHover>
</div>
<div v-if="isScanning" class="scanning-state">
<div class="scan-animation">🔍</div>
<p class="scanning-text">扫描中...</p>
<div class="countdown-ring">
<svg class="countdown-svg" viewBox="0 0 100 100">
<circle
class="countdown-bg"
cx="50"
cy="50"
r="45"
/>
<circle
class="countdown-progress"
cx="50"
cy="50"
r="45"
:style="{ strokeDashoffset: 283 - (283 * countdown / 10) }"
/>
</svg>
<span class="countdown-number">{{ countdown }}</span>
</div>
<GlareHover
width="150px"
height="40px"
background="rgba(255,100,100,0.1)"
border-radius="20px"
border-color="rgba(255,100,100,0.3)"
glare-color="#ff6666"
:glare-opacity="0.1"
class="cancel-btn"
@click="cancelScanning"
>
<span class="cancel-btn-text">取消</span>
</GlareHover>
</div>
<div v-if="scanResult && !showStory" class="result-card">
<div class="result-header">
<span
class="scrap-name"
:style="{ color: getLevelColor(scanResult.scrap.levelColor) }"
>
{{ scanResult.scrap.name }}
</span>
<span
class="scrap-level"
:style="{ color: getLevelColor(scanResult.scrap.levelColor) }"
>
{{ scanResult.scrap.levelName }}
</span>
</div>
<div class="result-desc">
{{ scanResult.scrap.description }}
</div>
<div class="bonus-section">
<div class="bonus-title">属性加成</div>
<div class="bonus-list">
<div v-if="scanResult.attackGain > 0" class="bonus-item attack">
<span class="bonus-icon">⚔️</span>
<span class="bonus-value">+{{ scanResult.attackGain }}</span>
<span class="bonus-label">攻击</span>
</div>
<div v-if="scanResult.defenseGain > 0" class="bonus-item defense">
<span class="bonus-icon">🛡️</span>
<span class="bonus-value">+{{ scanResult.defenseGain }}</span>
<span class="bonus-label">防御</span>
</div>
<div v-if="scanResult.hpGain > 0" class="bonus-item hp">
<span class="bonus-icon">❤️</span>
<span class="bonus-value">+{{ scanResult.hpGain }}</span>
<span class="bonus-label">生命</span>
</div>
<div v-if="scanResult.magicGain > 0" class="bonus-item magic">
<span class="bonus-icon">✨</span>
<span class="bonus-value">+{{ scanResult.magicGain }}</span>
<span class="bonus-label">魔力</span>
</div>
</div>
</div>
<div class="result-actions">
<GlareHover
width="100%"
height="44px"
background="rgba(255,255,255,0.05)"
border-radius="22px"
border-color="rgba(255,255,255,0.1)"
glare-color="#ffffff"
:glare-opacity="0.1"
class="story-btn"
@click="viewStory"
>
<span class="story-btn-text">查看故事</span>
</GlareHover>
<GlareHover
width="100%"
height="44px"
background="rgba(255,140,0,0.1)"
border-radius="22px"
border-color="rgba(255,140,0,0.3)"
glare-color="#ff8c00"
:glare-opacity="0.1"
class="again-btn"
@click="startScanning"
>
<span class="again-btn-text">继续捡</span>
</GlareHover>
</div>
</div>
<div v-if="showStory && scanResult" class="story-card">
<div class="story-header">
<span class="story-title">物品故事</span>
<button class="close-story" @click="hideStory">×</button>
</div>
<div class="story-content">
<p class="story-name" :style="{ color: getLevelColor(scanResult.scrap.levelColor) }">
{{ scanResult.scrap.name }}
</p>
<p class="story-text">{{ scanResult.scrap.story }}</p>
</div>
<div class="story-footer">
<GlareHover
width="100%"
height="44px"
background="rgba(255,255,255,0.05)"
border-radius="22px"
border-color="rgba(255,255,255,0.1)"
glare-color="#ffffff"
:glare-opacity="0.1"
class="back-to-result"
@click="hideStory"
>
<span class="back-btn-text">返回</span>
</GlareHover>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.scrap-page {
min-height: 100vh;
background: #000000;
padding: 20px;
position: relative;
overflow: hidden;
}
.particles-bg {
position: fixed !important;
top: 0;
left: 0;
width: 100% !important;
height: 100% !important;
}
.scrap-container {
max-width: 480px;
margin: 0 auto;
position: relative;
z-index: 10;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.back-btn {
display: flex;
align-items: center;
gap: 6px;
background: none;
border: none;
color: #888888;
font-size: 0.9rem;
cursor: pointer;
transition: color 0.2s;
}
.back-btn:hover {
color: #ffffff;
}
.back-arrow {
font-size: 1.2rem;
}
.title {
font-size: 1.3rem;
font-weight: 500;
color: #ffffff;
letter-spacing: 0.1em;
}
.spacer {
width: 60px;
}
.content {
display: flex;
flex-direction: column;
align-items: center;
}
.error-message {
width: 100%;
padding: 16px;
background: rgba(255, 68, 68, 0.1);
border: 1px solid rgba(255, 68, 68, 0.3);
border-radius: 12px;
color: #ff6666;
font-size: 0.9rem;
text-align: center;
margin-bottom: 20px;
}
.scan-prompt {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
padding: 40px 20px;
}
.scan-icon {
font-size: 4rem;
opacity: 0.6;
}
.scan-text {
color: #888888;
font-size: 1rem;
text-align: center;
}
.scan-btn {
cursor: pointer;
}
.scan-btn-text {
color: #ffffff;
font-size: 1rem;
font-weight: 500;
}
.scanning-state {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
padding: 40px 20px;
}
.scan-animation {
font-size: 4rem;
animation: pulse 1s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 0.6; }
50% { transform: scale(1.1); opacity: 1; }
}
.scanning-text {
color: #888888;
font-size: 1rem;
}
.countdown-ring {
position: relative;
width: 120px;
height: 120px;
}
.countdown-svg {
width: 100%;
height: 100%;
transform: rotate(-90deg);
}
.countdown-bg {
fill: none;
stroke: rgba(255, 255, 255, 0.1);
stroke-width: 6;
}
.countdown-progress {
fill: none;
stroke: #ff8c00;
stroke-width: 6;
stroke-linecap: round;
stroke-dasharray: 283;
transition: stroke-dashoffset 1s linear;
}
.countdown-number {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 2.5rem;
font-weight: 300;
color: #ffffff;
}
.cancel-btn {
cursor: pointer;
}
.cancel-btn-text {
color: #ff6666;
font-size: 0.9rem;
}
.result-card {
width: 100%;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
padding: 24px;
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.scrap-name {
font-size: 1.3rem;
font-weight: 600;
}
.scrap-level {
font-size: 0.85rem;
font-weight: 500;
padding: 4px 12px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.05);
}
.result-desc {
color: #888888;
font-size: 0.9rem;
line-height: 1.6;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.bonus-section {
margin-bottom: 24px;
}
.bonus-title {
color: #666666;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 12px;
}
.bonus-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.bonus-item {
display: flex;
align-items: center;
gap: 8px;
padding: 12px;
background: rgba(255, 255, 255, 0.02);
border-radius: 8px;
}
.bonus-icon {
font-size: 1.2rem;
}
.bonus-value {
font-size: 1.1rem;
font-weight: 600;
color: #ffffff;
}
.bonus-label {
font-size: 0.8rem;
color: #666666;
}
.result-actions {
display: flex;
flex-direction: column;
gap: 12px;
}
.story-btn {
cursor: pointer;
}
.story-btn-text {
color: #cccccc;
font-size: 0.9rem;
}
.again-btn {
cursor: pointer;
}
.again-btn-text {
color: #ff8c00;
font-size: 0.9rem;
font-weight: 500;
}
.story-card {
width: 100%;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
padding: 24px;
}
.story-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.story-title {
color: #888888;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.1em;
}
.close-story {
background: none;
border: none;
color: #666666;
font-size: 1.5rem;
cursor: pointer;
line-height: 1;
padding: 0;
}
.close-story:hover {
color: #ffffff;
}
.story-content {
margin-bottom: 24px;
}
.story-name {
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 16px;
}
.story-text {
color: #aaaaaa;
font-size: 0.95rem;
line-height: 1.8;
}
.story-footer {
display: flex;
justify-content: center;
}
.back-to-result {
cursor: pointer;
}
.back-btn-text {
color: #cccccc;
font-size: 0.9rem;
}
</style>