mirror of
https://github.com/setube/ogame-vue-ts.git
synced 2026-05-12 07:55:11 +08:00
feat: 初始化项目结构与核心功能
引入项目基础目录结构,包含多语言支持、主要页面与组件、核心游戏逻辑、UI 组件库、加密与本地持久化、自动化 Docker 构建流程、GitHub issue 模板(中英文)、README(中英文)、LICENSE 及开发配置文件。实现 OGame 单机版主要功能模块,为后续开发和扩展奠定基础。
This commit is contained in:
569
src/views/BattleSimulatorView.vue
Normal file
569
src/views/BattleSimulatorView.vue
Normal file
@@ -0,0 +1,569 @@
|
||||
<template>
|
||||
<div class="container mx-auto p-4 sm:p-6 space-y-6">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('simulatorView.title') }}</h1>
|
||||
<!-- 标签切换 -->
|
||||
<div class="flex gap-2 border-b">
|
||||
<Button @click="activeTab = 'attacker'" :variant="activeTab === 'attacker' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
<Sword />
|
||||
{{ t('simulatorView.attacker') }}
|
||||
</Button>
|
||||
<Button @click="activeTab = 'defender'" :variant="activeTab === 'defender' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
<Shield />
|
||||
{{ t('simulatorView.defender') }}
|
||||
</Button>
|
||||
</div>
|
||||
<!-- 攻击方配置 -->
|
||||
<Card v-if="activeTab === 'attacker'">
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('simulatorView.attackerConfig') }}</CardTitle>
|
||||
<CardDescription>{{ t('simulatorView.attackerConfigDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<!-- 舰队配置 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-3">{{ t('simulatorView.fleet') }}</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div v-for="shipType in Object.values(ShipType)" :key="shipType" class="space-y-1">
|
||||
<Label :for="`attacker-${shipType}`" class="text-xs">{{ SHIPS[shipType].name }}</Label>
|
||||
<Input :id="`attacker-${shipType}`" v-model.number="attackerFleet[shipType]" type="number" min="0" class="h-8" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 科技等级 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-3">{{ t('simulatorView.techLevels') }}</h3>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div class="space-y-1">
|
||||
<Label for="attacker-weapon" class="text-xs">{{ t('simulatorView.weapon') }}</Label>
|
||||
<Input id="attacker-weapon" v-model.number="attackerTech.weapon" type="number" min="0" class="h-8" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="attacker-shield" class="text-xs">{{ t('simulatorView.shield') }}</Label>
|
||||
<Input id="attacker-shield" v-model.number="attackerTech.shield" type="number" min="0" class="h-8" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="attacker-armor" class="text-xs">{{ t('simulatorView.armor') }}</Label>
|
||||
<Input id="attacker-armor" v-model.number="attackerTech.armor" type="number" min="0" class="h-8" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<!-- 防守方配置 -->
|
||||
<Card v-else>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('simulatorView.defenderConfig') }}</CardTitle>
|
||||
<CardDescription>{{ t('simulatorView.defenderConfigDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<!-- 舰队配置 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-3">{{ t('simulatorView.fleet') }}</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div v-for="shipType in Object.values(ShipType)" :key="shipType" class="space-y-1">
|
||||
<Label :for="`defender-${shipType}`" class="text-xs">{{ SHIPS[shipType].name }}</Label>
|
||||
<Input :id="`defender-${shipType}`" v-model.number="defenderFleet[shipType]" type="number" min="0" class="h-8" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 防御设施 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-3">{{ t('simulatorView.defenseStructures') }}</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div v-for="defenseType in Object.values(DefenseType)" :key="defenseType" class="space-y-1">
|
||||
<Label :for="`defense-${defenseType}`" class="text-xs">{{ DEFENSES[defenseType].name }}</Label>
|
||||
<Input :id="`defense-${defenseType}`" v-model.number="defenderDefense[defenseType]" type="number" min="0" class="h-8" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 科技等级 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-3">{{ t('simulatorView.techLevels') }}</h3>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div class="space-y-1">
|
||||
<Label for="defender-weapon" class="text-xs">{{ t('simulatorView.weapon') }}</Label>
|
||||
<Input id="defender-weapon" v-model.number="defenderTech.weapon" type="number" min="0" class="h-8" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="defender-shield" class="text-xs">{{ t('simulatorView.shield') }}</Label>
|
||||
<Input id="defender-shield" v-model.number="defenderTech.shield" type="number" min="0" class="h-8" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="defender-armor" class="text-xs">{{ t('simulatorView.armor') }}</Label>
|
||||
<Input id="defender-armor" v-model.number="defenderTech.armor" type="number" min="0" class="h-8" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 防守方资源 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-3">{{ t('simulatorView.defenderResources') }}</h3>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div class="space-y-1">
|
||||
<Label for="defender-metal" class="text-xs flex items-center gap-1">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
{{ t('resources.metal') }}
|
||||
</Label>
|
||||
<Input id="defender-metal" v-model.number="defenderResources.metal" type="number" min="0" class="h-8" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="defender-crystal" class="text-xs flex items-center gap-1">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
{{ t('resources.crystal') }}
|
||||
</Label>
|
||||
<Input id="defender-crystal" v-model.number="defenderResources.crystal" type="number" min="0" class="h-8" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="defender-deuterium" class="text-xs flex items-center gap-1">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
{{ t('resources.deuterium') }}
|
||||
</Label>
|
||||
<Input id="defender-deuterium" v-model.number="defenderResources.deuterium" type="number" min="0" class="h-8" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex gap-2">
|
||||
<Button @click="runSimulation" class="flex-1" size="lg">
|
||||
<Zap class="h-4 w-4 mr-2" />
|
||||
{{ t('simulatorView.startSimulation') }}
|
||||
</Button>
|
||||
<Button @click="resetSimulation" variant="outline" size="lg">
|
||||
<RotateCcw class="h-4 w-4 mr-2" />
|
||||
{{ t('simulatorView.reset') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 战斗结果对话框 -->
|
||||
<Dialog v-model:open="showResultDialog">
|
||||
<DialogContent class="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle class="flex items-center gap-2">
|
||||
<Trophy class="h-5 w-5" />
|
||||
{{ t('simulatorView.battleResult') }}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div v-if="simulationResult" class="space-y-4">
|
||||
<!-- 胜利者 -->
|
||||
<div class="text-center p-4 rounded-lg" :class="getWinnerStyle(simulationResult.winner)">
|
||||
<p class="text-lg font-bold">
|
||||
{{
|
||||
simulationResult.winner === 'attacker'
|
||||
? t('simulatorView.attackerVictory')
|
||||
: simulationResult.winner === 'defender'
|
||||
? t('simulatorView.defenderVictory')
|
||||
: t('simulatorView.draw')
|
||||
}}
|
||||
</p>
|
||||
<p class="text-sm mt-1">{{ t('simulatorView.afterRounds').replace('{rounds}', String(battleRounds)) }}</p>
|
||||
</div>
|
||||
<!-- 损失对比 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- 攻击方损失 -->
|
||||
<div class="space-y-2">
|
||||
<p class="text-sm font-medium text-red-600 dark:text-red-400">{{ t('simulatorView.attackerLosses') }}</p>
|
||||
<div class="p-3 bg-muted rounded-lg space-y-1 text-xs">
|
||||
<div v-for="(count, shipType) in simulationResult.attackerLosses" :key="shipType">
|
||||
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
|
||||
<span class="ml-2 font-medium">{{ count }}</span>
|
||||
</div>
|
||||
<p v-if="Object.keys(simulationResult.attackerLosses).length === 0" class="text-muted-foreground">
|
||||
{{ t('simulatorView.noLosses') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 防守方损失 -->
|
||||
<div class="space-y-2">
|
||||
<p class="text-sm font-medium text-red-600 dark:text-red-400">{{ t('simulatorView.defenderLosses') }}</p>
|
||||
<div class="p-3 bg-muted rounded-lg space-y-1 text-xs">
|
||||
<div v-for="(count, shipType) in simulationResult.defenderLosses.fleet" :key="shipType">
|
||||
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
|
||||
<span class="ml-2 font-medium">{{ count }}</span>
|
||||
</div>
|
||||
<div v-for="(count, defenseType) in simulationResult.defenderLosses.defense" :key="defenseType">
|
||||
<span class="text-muted-foreground">{{ DEFENSES[defenseType].name }}:</span>
|
||||
<span class="ml-2 font-medium">{{ count }}</span>
|
||||
</div>
|
||||
<p
|
||||
v-if="
|
||||
Object.keys(simulationResult.defenderLosses.fleet).length === 0 &&
|
||||
Object.keys(simulationResult.defenderLosses.defense).length === 0
|
||||
"
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
{{ t('simulatorView.noLosses') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 剩余单位 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- 攻击方剩余 -->
|
||||
<div class="space-y-2">
|
||||
<p class="text-sm font-medium text-blue-600 dark:text-blue-400">{{ t('simulatorView.attackerRemaining') }}</p>
|
||||
<div class="p-3 bg-muted rounded-lg space-y-1 text-xs">
|
||||
<div v-for="(count, shipType) in attackerRemaining" :key="shipType">
|
||||
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
|
||||
<span class="ml-2 font-medium">{{ count }}</span>
|
||||
</div>
|
||||
<p v-if="Object.keys(attackerRemaining).length === 0" class="text-muted-foreground">
|
||||
{{ t('simulatorView.allDestroyed') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 防守方剩余 -->
|
||||
<div class="space-y-2">
|
||||
<p class="text-sm font-medium text-blue-600 dark:text-blue-400">{{ t('simulatorView.defenderRemaining') }}</p>
|
||||
<div class="p-3 bg-muted rounded-lg space-y-1 text-xs">
|
||||
<div v-for="(count, shipType) in defenderRemaining.fleet" :key="shipType">
|
||||
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
|
||||
<span class="ml-2 font-medium">{{ count }}</span>
|
||||
</div>
|
||||
<div v-for="(count, defenseType) in defenderRemaining.defense" :key="defenseType">
|
||||
<span class="text-muted-foreground">{{ DEFENSES[defenseType].name }}:</span>
|
||||
<span class="ml-2 font-medium">{{ count }}</span>
|
||||
</div>
|
||||
<p
|
||||
v-if="Object.keys(defenderRemaining.fleet).length === 0 && Object.keys(defenderRemaining.defense).length === 0"
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
{{ t('simulatorView.allDestroyed') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 战利品和残骸 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- 掠夺资源 -->
|
||||
<div
|
||||
v-if="plunder.metal > 0 || plunder.crystal > 0 || plunder.deuterium > 0"
|
||||
class="p-3 bg-green-50 dark:bg-green-950 rounded-lg"
|
||||
>
|
||||
<p class="text-sm font-medium mb-2 text-green-600 dark:text-green-400">{{ t('simulatorView.plunderableResources') }}</p>
|
||||
<div class="flex flex-wrap gap-3 text-xs">
|
||||
<span v-if="plunder.metal > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
{{ formatNumber(plunder.metal) }}
|
||||
</span>
|
||||
<span v-if="plunder.crystal > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
{{ formatNumber(plunder.crystal) }}
|
||||
</span>
|
||||
<span v-if="plunder.deuterium > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
{{ formatNumber(plunder.deuterium) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 残骸场 -->
|
||||
<div v-if="debrisField.metal > 0 || debrisField.crystal > 0" class="p-3 bg-muted rounded-lg">
|
||||
<p class="text-sm font-medium mb-2">{{ t('simulatorView.debrisField') }}</p>
|
||||
<div class="flex flex-wrap gap-3 text-xs">
|
||||
<span v-if="debrisField.metal > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
{{ formatNumber(debrisField.metal) }}
|
||||
</span>
|
||||
<span v-if="debrisField.crystal > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
{{ formatNumber(debrisField.crystal) }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- 月球生成概率 -->
|
||||
<p v-if="moonChance > 0" class="text-xs text-muted-foreground mt-2">{{ t('simulatorView.moonChance') }}: {{ moonChance }}%</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 回合详情 -->
|
||||
<div class="space-y-2">
|
||||
<Button @click="showRoundDetails = !showRoundDetails" variant="outline" size="sm" class="w-full">
|
||||
{{ showRoundDetails ? t('simulatorView.hideRoundDetails') : t('simulatorView.showRoundDetails') }}
|
||||
</Button>
|
||||
|
||||
<div v-if="showRoundDetails" class="relative pl-6 space-y-4">
|
||||
<!-- 时间线 -->
|
||||
<div class="absolute left-2 top-0 bottom-0 w-0.5 bg-border" />
|
||||
|
||||
<div v-for="detail in roundDetails" :key="detail.round" class="relative">
|
||||
<!-- 时间线节点 -->
|
||||
<div class="absolute -left-6 top-3 w-4 h-4 rounded-full bg-primary border-2 border-background" />
|
||||
|
||||
<!-- 回合内容卡片 -->
|
||||
<div class="border rounded-lg p-3 bg-card hover:shadow-md transition-shadow">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<p class="text-sm font-semibold">{{ t('simulatorView.round').replace('{round}', String(detail.round)) }}</p>
|
||||
<div class="flex gap-3 text-xs text-muted-foreground">
|
||||
<span class="flex items-center gap-1" :title="t('simulatorView.attackerRemainingPower')">
|
||||
<Sword class="h-3 w-3" />
|
||||
{{ formatNumber(detail.attackerRemainingPower) }}
|
||||
</span>
|
||||
<span class="flex items-center gap-1" :title="t('simulatorView.defenderRemainingPower')">
|
||||
<Shield class="h-3 w-3" />
|
||||
{{ formatNumber(detail.defenderRemainingPower) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<!-- 攻击方本回合损失 -->
|
||||
<div class="bg-red-50 dark:bg-red-950/20 rounded p-2">
|
||||
<p class="text-xs font-medium text-red-600 dark:text-red-400 mb-1.5">{{ t('simulatorView.attackerLosses') }}</p>
|
||||
<div class="text-xs space-y-0.5">
|
||||
<div v-for="(count, shipType) in detail.attackerLosses" :key="shipType" class="flex justify-between">
|
||||
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}</span>
|
||||
<span class="font-medium">-{{ count }}</span>
|
||||
</div>
|
||||
<p v-if="Object.keys(detail.attackerLosses).length === 0" class="text-muted-foreground italic">
|
||||
{{ t('simulatorView.noLosses') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 防守方本回合损失 -->
|
||||
<div class="bg-blue-50 dark:bg-blue-950/20 rounded p-2">
|
||||
<p class="text-xs font-medium text-blue-600 dark:text-blue-400 mb-1.5">{{ t('simulatorView.defenderLosses') }}</p>
|
||||
<div class="text-xs space-y-0.5">
|
||||
<div v-for="(count, shipType) in detail.defenderLosses.fleet" :key="shipType" class="flex justify-between">
|
||||
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}</span>
|
||||
<span class="font-medium">-{{ count }}</span>
|
||||
</div>
|
||||
<div v-for="(count, defenseType) in detail.defenderLosses.defense" :key="defenseType" class="flex justify-between">
|
||||
<span class="text-muted-foreground">{{ DEFENSES[defenseType].name }}</span>
|
||||
<span class="font-medium">-{{ count }}</span>
|
||||
</div>
|
||||
<p
|
||||
v-if="
|
||||
Object.keys(detail.defenderLosses.fleet).length === 0 && Object.keys(detail.defenderLosses.defense).length === 0
|
||||
"
|
||||
class="text-muted-foreground italic"
|
||||
>
|
||||
{{ t('simulatorView.noLosses') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { useGameConfig } from '@/composables/useGameConfig'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { ShipType, DefenseType } from '@/types/game'
|
||||
import type { Fleet, BattleResult, Resources } from '@/types/game'
|
||||
import { simulateBattle, calculatePlunder, calculateDebrisField } from '@/utils/battleSimulator'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import { Sword, Shield, Zap, RotateCcw, Trophy } from 'lucide-vue-next'
|
||||
import * as planetLogic from '@/logic/planetLogic'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { SHIPS, DEFENSES } = useGameConfig()
|
||||
|
||||
// 攻击方配置
|
||||
const attackerFleet = ref<Partial<Fleet>>({
|
||||
[ShipType.LightFighter]: 0,
|
||||
[ShipType.HeavyFighter]: 0,
|
||||
[ShipType.Cruiser]: 0,
|
||||
[ShipType.Battleship]: 0,
|
||||
[ShipType.SmallCargo]: 0,
|
||||
[ShipType.LargeCargo]: 0,
|
||||
[ShipType.ColonyShip]: 0,
|
||||
[ShipType.Recycler]: 0,
|
||||
[ShipType.EspionageProbe]: 0,
|
||||
[ShipType.DarkMatterHarvester]: 0
|
||||
})
|
||||
|
||||
const activeTab = ref('attacker')
|
||||
|
||||
const attackerTech = ref({
|
||||
weapon: 0,
|
||||
shield: 0,
|
||||
armor: 0
|
||||
})
|
||||
|
||||
// 防守方配置
|
||||
const defenderFleet = ref<Partial<Fleet>>({
|
||||
[ShipType.LightFighter]: 0,
|
||||
[ShipType.HeavyFighter]: 0,
|
||||
[ShipType.Cruiser]: 0,
|
||||
[ShipType.Battleship]: 0,
|
||||
[ShipType.SmallCargo]: 0,
|
||||
[ShipType.LargeCargo]: 0,
|
||||
[ShipType.ColonyShip]: 0,
|
||||
[ShipType.Recycler]: 0,
|
||||
[ShipType.EspionageProbe]: 0,
|
||||
[ShipType.DarkMatterHarvester]: 0
|
||||
})
|
||||
|
||||
const defenderDefense = ref<Partial<Record<DefenseType, number>>>({
|
||||
[DefenseType.RocketLauncher]: 0,
|
||||
[DefenseType.LightLaser]: 0,
|
||||
[DefenseType.HeavyLaser]: 0,
|
||||
[DefenseType.GaussCannon]: 0,
|
||||
[DefenseType.IonCannon]: 0,
|
||||
[DefenseType.PlasmaTurret]: 0,
|
||||
[DefenseType.SmallShieldDome]: 0,
|
||||
[DefenseType.LargeShieldDome]: 0
|
||||
})
|
||||
|
||||
const defenderTech = ref({
|
||||
weapon: 0,
|
||||
shield: 0,
|
||||
armor: 0
|
||||
})
|
||||
|
||||
const defenderResources = ref({
|
||||
metal: 100000,
|
||||
crystal: 50000,
|
||||
deuterium: 25000,
|
||||
darkMatter: 100,
|
||||
energy: 0
|
||||
})
|
||||
|
||||
// 模拟结果
|
||||
const simulationResult = ref<BattleResult | null>(null)
|
||||
const battleRounds = ref<number>(0)
|
||||
const attackerRemaining = ref<Partial<Fleet>>({})
|
||||
const defenderRemaining = ref<{ fleet: Partial<Fleet>; defense: Partial<Record<DefenseType, number>> }>({
|
||||
fleet: {},
|
||||
defense: {}
|
||||
})
|
||||
const roundDetails = ref<
|
||||
Array<{
|
||||
round: number
|
||||
attackerLosses: Partial<Fleet>
|
||||
defenderLosses: {
|
||||
fleet: Partial<Fleet>
|
||||
defense: Partial<Record<DefenseType, number>>
|
||||
}
|
||||
attackerRemainingPower: number
|
||||
defenderRemainingPower: number
|
||||
}>
|
||||
>([])
|
||||
const showRoundDetails = ref<boolean>(false)
|
||||
const showResultDialog = ref<boolean>(false)
|
||||
|
||||
// 计算掠夺资源
|
||||
const plunder = computed(() => {
|
||||
if (!simulationResult.value || simulationResult.value.winner !== 'attacker') {
|
||||
return { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 }
|
||||
}
|
||||
return calculatePlunder(defenderResources.value, attackerFleet.value)
|
||||
})
|
||||
|
||||
// 计算残骸场
|
||||
const debrisField = computed(() => {
|
||||
if (!simulationResult.value) {
|
||||
return { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 }
|
||||
}
|
||||
return calculateDebrisField(simulationResult.value.attackerLosses, simulationResult.value.defenderLosses)
|
||||
})
|
||||
|
||||
const calculateMoonChance = (debrisField: Resources): number => {
|
||||
return planetLogic.calculateMoonChance(debrisField)
|
||||
}
|
||||
|
||||
// 计算月球生成概率
|
||||
const moonChance = computed(() => {
|
||||
if (!debrisField.value) return 0
|
||||
return calculateMoonChance(debrisField.value)
|
||||
})
|
||||
|
||||
// 运行模拟
|
||||
const runSimulation = () => {
|
||||
const attackerSide = {
|
||||
ships: attackerFleet.value,
|
||||
weaponTech: attackerTech.value.weapon,
|
||||
shieldTech: attackerTech.value.shield,
|
||||
armorTech: attackerTech.value.armor
|
||||
}
|
||||
|
||||
const defenderSide = {
|
||||
ships: defenderFleet.value,
|
||||
defense: defenderDefense.value,
|
||||
weaponTech: defenderTech.value.weapon,
|
||||
shieldTech: defenderTech.value.shield,
|
||||
armorTech: defenderTech.value.armor
|
||||
}
|
||||
|
||||
const result = simulateBattle(attackerSide, defenderSide)
|
||||
|
||||
// 保存回合数和剩余单位
|
||||
battleRounds.value = result.rounds
|
||||
attackerRemaining.value = result.attackerRemaining
|
||||
defenderRemaining.value = result.defenderRemaining
|
||||
roundDetails.value = result.roundDetails
|
||||
showRoundDetails.value = false
|
||||
|
||||
simulationResult.value = {
|
||||
id: `sim_${Date.now()}`,
|
||||
timestamp: Date.now(),
|
||||
attackerId: 'simulator_attacker',
|
||||
defenderId: 'simulator_defender',
|
||||
attackerPlanetId: 'sim_attacker',
|
||||
defenderPlanetId: 'sim_defender',
|
||||
attackerFleet: attackerFleet.value,
|
||||
defenderFleet: defenderFleet.value,
|
||||
defenderDefense: defenderDefense.value,
|
||||
attackerLosses: result.attackerLosses,
|
||||
defenderLosses: result.defenderLosses,
|
||||
winner: result.winner,
|
||||
plunder: plunder.value,
|
||||
debrisField: debrisField.value
|
||||
}
|
||||
|
||||
// 显示结果对话框
|
||||
showResultDialog.value = true
|
||||
}
|
||||
|
||||
// 重置模拟
|
||||
const resetSimulation = () => {
|
||||
Object.keys(attackerFleet.value).forEach(key => {
|
||||
attackerFleet.value[key as ShipType] = 0
|
||||
})
|
||||
Object.keys(defenderFleet.value).forEach(key => {
|
||||
defenderFleet.value[key as ShipType] = 0
|
||||
})
|
||||
Object.keys(defenderDefense.value).forEach(key => {
|
||||
defenderDefense.value[key as DefenseType] = 0
|
||||
})
|
||||
attackerTech.value = { weapon: 0, shield: 0, armor: 0 }
|
||||
defenderTech.value = { weapon: 0, shield: 0, armor: 0 }
|
||||
simulationResult.value = null
|
||||
battleRounds.value = 0
|
||||
attackerRemaining.value = {}
|
||||
defenderRemaining.value = { fleet: {}, defense: {} }
|
||||
roundDetails.value = []
|
||||
showRoundDetails.value = false
|
||||
showResultDialog.value = false
|
||||
}
|
||||
|
||||
// 获取胜利者样式
|
||||
const getWinnerStyle = (winner: string) => {
|
||||
if (winner === 'attacker') return 'bg-green-50 dark:bg-green-950 text-green-700 dark:text-green-300'
|
||||
if (winner === 'defender') return 'bg-red-50 dark:bg-red-950 text-red-700 dark:text-red-300'
|
||||
return 'bg-gray-50 dark:bg-gray-950 text-gray-700 dark:text-gray-300'
|
||||
}
|
||||
</script>
|
||||
254
src/views/BuildingsView.vue
Normal file
254
src/views/BuildingsView.vue
Normal file
@@ -0,0 +1,254 @@
|
||||
<template>
|
||||
<div v-if="planet" class="container mx-auto p-4 sm:p-6">
|
||||
<div class="flex justify-between items-center mb-4 sm:mb-6 gap-2">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('buildingsView.title') }}</h1>
|
||||
<div class="text-xs sm:text-sm">
|
||||
<span class="flex items-center gap-1.5 text-muted-foreground">
|
||||
<Grid3x3 :size="14" />
|
||||
{{ getUsedSpace(planet) }} / {{ planet.maxSpace }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4">
|
||||
<Card v-for="buildingType in availableBuildings" :key="buildingType">
|
||||
<CardHeader>
|
||||
<div class="flex justify-between items-start gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<CardTitle
|
||||
class="text-base sm:text-lg cursor-pointer hover:text-primary transition-colors"
|
||||
@click="detailDialog.openBuilding(buildingType, getBuildingLevel(buildingType))"
|
||||
>
|
||||
{{ BUILDINGS[buildingType].name }}
|
||||
</CardTitle>
|
||||
<CardDescription class="text-xs sm:text-sm">{{ BUILDINGS[buildingType].description }}</CardDescription>
|
||||
</div>
|
||||
<Badge variant="secondary" class="text-xs whitespace-nowrap flex-shrink-0">Lv {{ getBuildingLevel(buildingType) }}</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="space-y-3">
|
||||
<div class="text-xs sm:text-sm space-y-1.5 sm:space-y-2">
|
||||
<p class="text-muted-foreground mb-1 sm:mb-2">{{ t('buildingsView.upgradeCost') }}:</p>
|
||||
<div class="space-y-1 sm:space-y-1.5">
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.metal') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="
|
||||
getResourceCostColor(planet.resources.metal, getBuildingCost(buildingType, getBuildingLevel(buildingType) + 1).metal)
|
||||
"
|
||||
>
|
||||
{{ formatNumber(getBuildingCost(buildingType, getBuildingLevel(buildingType) + 1).metal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.crystal') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="
|
||||
getResourceCostColor(
|
||||
planet.resources.crystal,
|
||||
getBuildingCost(buildingType, getBuildingLevel(buildingType) + 1).crystal
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ formatNumber(getBuildingCost(buildingType, getBuildingLevel(buildingType) + 1).crystal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.deuterium') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="
|
||||
getResourceCostColor(
|
||||
planet.resources.deuterium,
|
||||
getBuildingCost(buildingType, getBuildingLevel(buildingType) + 1).deuterium
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ formatNumber(getBuildingCost(buildingType, getBuildingLevel(buildingType) + 1).deuterium) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-xs sm:text-sm space-y-0.5 sm:space-y-1">
|
||||
<div class="flex items-center gap-1.5 text-muted-foreground">
|
||||
<Clock :size="14" class="flex-shrink-0" />
|
||||
<span>{{ formatTime(getBuildingTime(buildingType, getBuildingLevel(buildingType) + 1)) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 text-muted-foreground">
|
||||
<Grid3x3 :size="14" class="flex-shrink-0" />
|
||||
<span>{{ BUILDINGS[buildingType].spaceUsage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 升级按钮 -->
|
||||
<Button @click="handleUpgrade(buildingType)" :disabled="!canUpgrade(buildingType)" class="w-full">
|
||||
{{ t('buildingsView.upgrade') }}
|
||||
</Button>
|
||||
|
||||
<!-- 拆除按钮 -->
|
||||
<Button
|
||||
v-if="getBuildingLevel(buildingType) > 0"
|
||||
@click="handleDemolish(buildingType)"
|
||||
:disabled="!canDemolish(buildingType)"
|
||||
variant="destructive"
|
||||
class="w-full"
|
||||
>
|
||||
{{ t('buildingsView.demolish') }}
|
||||
</Button>
|
||||
|
||||
<!-- 拆除信息提示 -->
|
||||
<div v-if="getBuildingLevel(buildingType) > 0" class="text-xs text-muted-foreground">
|
||||
<p>{{ t('buildingsView.demolishRefund') }}:</p>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<span>{{ formatNumber(getDemolishRefund(buildingType).metal) }} {{ t('resources.metal') }}</span>
|
||||
<span>{{ formatNumber(getDemolishRefund(buildingType).crystal) }} {{ t('resources.crystal') }}</span>
|
||||
<span>{{ formatNumber(getDemolishRefund(buildingType).deuterium) }} {{ t('resources.deuterium') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- 提示对话框 -->
|
||||
<AlertDialog ref="alertDialog" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useDetailDialogStore } from '@/stores/detailDialogStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { useGameConfig } from '@/composables/useGameConfig'
|
||||
import { computed, ref } from 'vue'
|
||||
import { BuildingType } from '@/types/game'
|
||||
import type { Resources, Planet } from '@/types/game'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import AlertDialog from '@/components/AlertDialog.vue'
|
||||
import { Clock, Grid3x3 } from 'lucide-vue-next'
|
||||
import { formatNumber, formatTime, getResourceCostColor } from '@/utils/format'
|
||||
import * as buildingLogic from '@/logic/buildingLogic'
|
||||
import * as buildingValidation from '@/logic/buildingValidation'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const detailDialog = useDetailDialogStore()
|
||||
const { t } = useI18n()
|
||||
const { BUILDINGS } = useGameConfig()
|
||||
const planet = computed(() => gameStore.currentPlanet)
|
||||
const alertDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
|
||||
|
||||
// 根据星球类型过滤可用建筑
|
||||
const availableBuildings = computed(() => {
|
||||
if (!planet.value) return []
|
||||
return Object.values(BuildingType).filter(buildingType => {
|
||||
const config = BUILDINGS.value[buildingType]
|
||||
if (planet.value!.isMoon) {
|
||||
// 月球只能建造月球专属建筑
|
||||
return config.moonOnly === true
|
||||
} else {
|
||||
// 行星不能建造月球专属建筑
|
||||
return config.moonOnly !== true
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const upgradeBuilding = (buildingType: BuildingType): boolean => {
|
||||
if (!gameStore.currentPlanet) return false
|
||||
const validation = buildingValidation.validateBuildingUpgrade(
|
||||
gameStore.currentPlanet,
|
||||
buildingType,
|
||||
gameStore.player.technologies,
|
||||
gameStore.player.officers
|
||||
)
|
||||
if (!validation.valid) return false
|
||||
const queueItem = buildingValidation.executeBuildingUpgrade(gameStore.currentPlanet, buildingType, gameStore.player.officers)
|
||||
gameStore.currentPlanet.buildQueue.push(queueItem)
|
||||
return true
|
||||
}
|
||||
|
||||
const getUsedSpace = (planet: Planet): number => {
|
||||
return buildingLogic.calculateUsedSpace(planet)
|
||||
}
|
||||
|
||||
// 升级建筑
|
||||
const handleUpgrade = (buildingType: BuildingType) => {
|
||||
const success = upgradeBuilding(buildingType)
|
||||
if (!success) {
|
||||
alertDialog.value?.show({
|
||||
title: t('buildingsView.upgradeFailed'),
|
||||
message: t('buildingsView.upgradeFailedMessage')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 获取建筑等级
|
||||
const getBuildingLevel = (buildingType: BuildingType): number => {
|
||||
return planet.value?.buildings[buildingType] || 0
|
||||
}
|
||||
|
||||
// 检查是否可以升级
|
||||
const canUpgrade = (buildingType: BuildingType): boolean => {
|
||||
if (!planet.value) return false
|
||||
if (planet.value.buildQueue.length > 0) return false
|
||||
|
||||
const currentLevel = getBuildingLevel(buildingType)
|
||||
const cost = getBuildingCost(buildingType, currentLevel + 1)
|
||||
|
||||
return (
|
||||
planet.value.resources.metal >= cost.metal &&
|
||||
planet.value.resources.crystal >= cost.crystal &&
|
||||
planet.value.resources.deuterium >= cost.deuterium
|
||||
)
|
||||
}
|
||||
|
||||
const getBuildingCost = (buildingType: BuildingType, targetLevel: number): Resources => {
|
||||
return buildingLogic.calculateBuildingCost(buildingType, targetLevel)
|
||||
}
|
||||
|
||||
const getBuildingTime = (buildingType: BuildingType, targetLevel: number): number => {
|
||||
return buildingLogic.calculateBuildingTime(buildingType, targetLevel)
|
||||
}
|
||||
|
||||
// 拆除建筑
|
||||
const demolishBuilding = (buildingType: BuildingType): boolean => {
|
||||
if (!gameStore.currentPlanet) return false
|
||||
const validation = buildingValidation.validateBuildingDemolish(gameStore.currentPlanet, buildingType, gameStore.player.officers)
|
||||
if (!validation.valid) return false
|
||||
const queueItem = buildingValidation.executeBuildingDemolish(gameStore.currentPlanet, buildingType, gameStore.player.officers)
|
||||
gameStore.currentPlanet.buildQueue.push(queueItem)
|
||||
return true
|
||||
}
|
||||
|
||||
const handleDemolish = (buildingType: BuildingType) => {
|
||||
const success = demolishBuilding(buildingType)
|
||||
if (!success) {
|
||||
alertDialog.value?.show({
|
||||
title: t('buildingsView.demolishFailed'),
|
||||
message: t('buildingsView.demolishFailedMessage')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否可以拆除
|
||||
const canDemolish = (buildingType: BuildingType): boolean => {
|
||||
if (!planet.value) return false
|
||||
if (planet.value.buildQueue.length > 0) return false
|
||||
return getBuildingLevel(buildingType) > 0
|
||||
}
|
||||
|
||||
// 获取拆除返还资源
|
||||
const getDemolishRefund = (buildingType: BuildingType): Resources => {
|
||||
const currentLevel = getBuildingLevel(buildingType)
|
||||
return buildingLogic.calculateDemolishRefund(buildingType, currentLevel)
|
||||
}
|
||||
</script>
|
||||
263
src/views/DefenseView.vue
Normal file
263
src/views/DefenseView.vue
Normal file
@@ -0,0 +1,263 @@
|
||||
<template>
|
||||
<div v-if="planet" class="container mx-auto p-4 sm:p-6">
|
||||
<!-- 未解锁遮罩 -->
|
||||
<UnlockRequirement :required-building="BuildingType.Shipyard" :required-level="1" />
|
||||
|
||||
<h1 class="text-2xl sm:text-3xl font-bold mb-4 sm:mb-6">{{ t('defenseView.title') }}</h1>
|
||||
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3 sm:gap-4">
|
||||
<Card v-for="defenseType in Object.values(DefenseType)" :key="defenseType" class="relative">
|
||||
<CardUnlockOverlay :requirements="DEFENSES[defenseType].requirements" />
|
||||
<CardHeader>
|
||||
<div class="flex justify-between items-start gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<CardTitle
|
||||
class="text-base sm:text-lg cursor-pointer hover:text-primary transition-colors"
|
||||
@click="detailDialog.openDefense(defenseType)"
|
||||
>
|
||||
{{ DEFENSES[defenseType].name }}
|
||||
</CardTitle>
|
||||
<CardDescription class="text-xs sm:text-sm">{{ DEFENSES[defenseType].description }}</CardDescription>
|
||||
</div>
|
||||
<Badge variant="secondary" class="text-xs whitespace-nowrap flex-shrink-0">
|
||||
{{ planet.defense[defenseType] }}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="space-y-3 sm:space-y-4">
|
||||
<div class="grid grid-cols-2 gap-2 text-xs sm:text-sm">
|
||||
<div>
|
||||
<p class="text-muted-foreground">{{ t('defenseView.attack') }}</p>
|
||||
<p class="font-medium">{{ DEFENSES[defenseType].attack }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-muted-foreground">{{ t('defenseView.shield') }}</p>
|
||||
<p class="font-medium">{{ DEFENSES[defenseType].shield }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-muted-foreground">{{ t('defenseView.armor') }}</p>
|
||||
<p class="font-medium">{{ DEFENSES[defenseType].armor }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-muted-foreground">{{ t('defenseView.buildTime') }}</p>
|
||||
<p class="font-medium">{{ DEFENSES[defenseType].buildTime }}{{ t('defenseView.seconds') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-xs sm:text-sm space-y-1.5 sm:space-y-2">
|
||||
<p class="text-muted-foreground mb-1 sm:mb-2">{{ t('defenseView.unitCost') }}:</p>
|
||||
<div class="space-y-1 sm:space-y-1.5">
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.metal') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.metal, DEFENSES[defenseType].cost.metal)"
|
||||
>
|
||||
{{ formatNumber(DEFENSES[defenseType].cost.metal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.crystal') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.crystal, DEFENSES[defenseType].cost.crystal)"
|
||||
>
|
||||
{{ formatNumber(DEFENSES[defenseType].cost.crystal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.deuterium') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.deuterium, DEFENSES[defenseType].cost.deuterium)"
|
||||
>
|
||||
{{ formatNumber(DEFENSES[defenseType].cost.deuterium) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label :for="`quantity-${defenseType}`" class="text-xs sm:text-sm">{{ t('defenseView.buildQuantity') }}</Label>
|
||||
<Input
|
||||
:id="`quantity-${defenseType}`"
|
||||
v-model.number="quantities[defenseType]"
|
||||
type="number"
|
||||
min="0"
|
||||
:max="isShieldDome(defenseType) && planet.defense[defenseType] > 0 ? 0 : undefined"
|
||||
:disabled="isShieldDome(defenseType) && planet.defense[defenseType] > 0"
|
||||
placeholder="0"
|
||||
class="text-sm"
|
||||
/>
|
||||
<p v-if="isShieldDome(defenseType) && planet.defense[defenseType] > 0" class="text-xs text-muted-foreground">
|
||||
{{ t('defenseView.shieldDomeBuilt') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="quantities[defenseType] > 0" class="text-xs sm:text-sm space-y-1.5 sm:space-y-2 p-2.5 sm:p-3 bg-muted rounded-lg">
|
||||
<p class="font-medium text-muted-foreground">{{ t('defenseView.totalCost') }}:</p>
|
||||
<div class="space-y-1 sm:space-y-1.5">
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.metal') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.metal, getTotalCost(defenseType).metal)"
|
||||
>
|
||||
{{ formatNumber(getTotalCost(defenseType).metal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.crystal') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.crystal, getTotalCost(defenseType).crystal)"
|
||||
>
|
||||
{{ formatNumber(getTotalCost(defenseType).crystal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.deuterium') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.deuterium, getTotalCost(defenseType).deuterium)"
|
||||
>
|
||||
{{ formatNumber(getTotalCost(defenseType).deuterium) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button @click="handleBuild(defenseType)" :disabled="!canBuild(defenseType)" class="w-full">
|
||||
{{ t('defenseView.build') }}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- 提示对话框 -->
|
||||
<AlertDialog ref="alertDialog" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useDetailDialogStore } from '@/stores/detailDialogStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { useGameConfig } from '@/composables/useGameConfig'
|
||||
import { computed, ref } from 'vue'
|
||||
import { DefenseType, BuildingType } from '@/types/game'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import AlertDialog from '@/components/AlertDialog.vue'
|
||||
import UnlockRequirement from '@/components/UnlockRequirement.vue'
|
||||
import CardUnlockOverlay from '@/components/CardUnlockOverlay.vue'
|
||||
import { formatNumber, getResourceCostColor } from '@/utils/format'
|
||||
import * as publicLogic from '@/logic/publicLogic'
|
||||
import * as shipValidation from '@/logic/shipValidation'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const detailDialog = useDetailDialogStore()
|
||||
const { t } = useI18n()
|
||||
const { DEFENSES } = useGameConfig()
|
||||
const planet = computed(() => gameStore.currentPlanet)
|
||||
const alertDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
|
||||
|
||||
// 每种防御设施的建造数量
|
||||
const quantities = ref<Record<DefenseType, number>>({
|
||||
[DefenseType.RocketLauncher]: 0,
|
||||
[DefenseType.LightLaser]: 0,
|
||||
[DefenseType.HeavyLaser]: 0,
|
||||
[DefenseType.GaussCannon]: 0,
|
||||
[DefenseType.IonCannon]: 0,
|
||||
[DefenseType.PlasmaTurret]: 0,
|
||||
[DefenseType.SmallShieldDome]: 0,
|
||||
[DefenseType.LargeShieldDome]: 0
|
||||
})
|
||||
|
||||
// 判断是否为护盾罩
|
||||
const isShieldDome = (defenseType: DefenseType): boolean => {
|
||||
return defenseType === DefenseType.SmallShieldDome || defenseType === DefenseType.LargeShieldDome
|
||||
}
|
||||
|
||||
const buildDefense = (defenseType: DefenseType, quantity: number): boolean => {
|
||||
if (!gameStore.currentPlanet) return false
|
||||
const validation = shipValidation.validateDefenseBuild(gameStore.currentPlanet, defenseType, quantity, gameStore.player.technologies)
|
||||
if (!validation.valid) return false
|
||||
const queueItem = shipValidation.executeDefenseBuild(gameStore.currentPlanet, defenseType, quantity, gameStore.player.officers)
|
||||
gameStore.currentPlanet.buildQueue.push(queueItem)
|
||||
return true
|
||||
}
|
||||
|
||||
// 建造防御设施
|
||||
const handleBuild = (defenseType: DefenseType) => {
|
||||
const quantity = quantities.value[defenseType]
|
||||
if (quantity <= 0) {
|
||||
alertDialog.value?.show({
|
||||
title: t('defenseView.inputError'),
|
||||
message: t('defenseView.inputErrorMessage')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const success = buildDefense(defenseType, quantity)
|
||||
if (!success) {
|
||||
alertDialog.value?.show({
|
||||
title: t('defenseView.buildFailed'),
|
||||
message: t('defenseView.buildFailedMessage')
|
||||
})
|
||||
} else {
|
||||
quantities.value[defenseType] = 0
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否可以建造
|
||||
const canBuild = (defenseType: DefenseType): boolean => {
|
||||
if (!planet.value) return false
|
||||
|
||||
const quantity = quantities.value[defenseType]
|
||||
if (quantity <= 0) return false
|
||||
|
||||
// 护盾罩只能建造一个
|
||||
if (isShieldDome(defenseType)) {
|
||||
if (planet.value.defense[defenseType] > 0) return false
|
||||
if (quantity > 1) return false
|
||||
}
|
||||
|
||||
const config = DEFENSES.value[defenseType]
|
||||
const totalCost = {
|
||||
metal: config.cost.metal * quantity,
|
||||
crystal: config.cost.crystal * quantity,
|
||||
deuterium: config.cost.deuterium * quantity
|
||||
}
|
||||
|
||||
return (
|
||||
publicLogic.checkRequirements(planet.value, gameStore.player.technologies, config.requirements) &&
|
||||
planet.value.resources.metal >= totalCost.metal &&
|
||||
planet.value.resources.crystal >= totalCost.crystal &&
|
||||
planet.value.resources.deuterium >= totalCost.deuterium
|
||||
)
|
||||
}
|
||||
|
||||
// 计算总成本
|
||||
const getTotalCost = (defenseType: DefenseType) => {
|
||||
const quantity = quantities.value[defenseType]
|
||||
const config = DEFENSES.value[defenseType]
|
||||
return {
|
||||
metal: config.cost.metal * quantity,
|
||||
crystal: config.cost.crystal * quantity,
|
||||
deuterium: config.cost.deuterium * quantity
|
||||
}
|
||||
}
|
||||
</script>
|
||||
569
src/views/FleetView.vue
Normal file
569
src/views/FleetView.vue
Normal file
@@ -0,0 +1,569 @@
|
||||
<template>
|
||||
<div v-if="planet" class="container mx-auto p-4 sm:p-6 space-y-4 sm:space-y-6">
|
||||
<!-- 未解锁遮罩 -->
|
||||
<UnlockRequirement :required-building="BuildingType.Shipyard" :required-level="1" />
|
||||
|
||||
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('fleetView.title') }}</h1>
|
||||
|
||||
<!-- 标签切换 -->
|
||||
<div class="flex gap-2 border-b">
|
||||
<Button @click="activeTab = 'fleet'" :variant="activeTab === 'fleet' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
{{ t('fleetView.fleetOverview') }}
|
||||
</Button>
|
||||
<Button @click="activeTab = 'send'" :variant="activeTab === 'send' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
{{ t('fleetView.sendFleet') }}
|
||||
</Button>
|
||||
<Button @click="activeTab = 'missions'" :variant="activeTab === 'missions' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
{{ t('fleetView.flightMissions') }}
|
||||
<Badge v-if="gameStore.player.fleetMissions.length > 0" variant="secondary" class="ml-1">
|
||||
{{ gameStore.player.fleetMissions.length }}
|
||||
</Badge>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 舰队总览 -->
|
||||
<div v-if="activeTab === 'fleet'">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.currentPlanetFleet') }}</CardTitle>
|
||||
<CardDescription>
|
||||
{{ planet.name }} [{{ planet.position.galaxy }}:{{ planet.position.system }}:{{ planet.position.position }}]
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3 sm:gap-4">
|
||||
<div v-for="(count, shipType) in planet.fleet" :key="shipType" class="p-3 sm:p-4 border rounded-lg space-y-2">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 class="font-semibold text-sm sm:text-base">{{ SHIPS[shipType].name }}</h3>
|
||||
<p class="text-xl sm:text-2xl font-bold">{{ formatNumber(count) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs sm:text-sm text-muted-foreground space-y-1">
|
||||
<p>{{ t('fleetView.attack') }}: {{ SHIPS[shipType].attack }}</p>
|
||||
<p>{{ t('fleetView.shield') }}: {{ SHIPS[shipType].shield }}</p>
|
||||
<p>{{ t('fleetView.armor') }}: {{ SHIPS[shipType].armor }}</p>
|
||||
<p>{{ t('fleetView.speed') }}: {{ formatNumber(SHIPS[shipType].speed) }}</p>
|
||||
<p>{{ t('fleetView.cargo') }}: {{ formatNumber(SHIPS[shipType].cargoCapacity) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- 派遣舰队 -->
|
||||
<div v-if="activeTab === 'send'" class="space-y-4">
|
||||
<!-- 舰队任务槽位信息 -->
|
||||
<Card>
|
||||
<CardContent class="py-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm font-medium">{{ t('fleetView.fleetMissionSlots') }}:</span>
|
||||
<span class="text-sm font-bold">{{ gameStore.player.fleetMissions.length }} / {{ maxFleetMissions }}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 选择舰队 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.selectFleet') }}</CardTitle>
|
||||
<CardDescription>{{ t('fleetView.selectFleetDescription') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4">
|
||||
<div v-for="(count, shipType) in planet.fleet" :key="shipType" class="space-y-2">
|
||||
<Label :for="`ship-${shipType}`" class="text-xs sm:text-sm">
|
||||
{{ SHIPS[shipType].name }} ({{ t('fleetView.available') }}: {{ count }})
|
||||
</Label>
|
||||
<div class="flex gap-2">
|
||||
<Input
|
||||
:id="`ship-${shipType}`"
|
||||
v-model.number="selectedFleet[shipType]"
|
||||
type="number"
|
||||
min="0"
|
||||
:max="count"
|
||||
placeholder="0"
|
||||
class="text-sm"
|
||||
/>
|
||||
<Button @click="selectedFleet[shipType] = count" variant="outline" size="sm">{{ t('fleetView.all') }}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 目标坐标 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.targetCoordinates') }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-3 gap-2 sm:gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="galaxy" class="text-xs sm:text-sm">{{ t('fleetView.galaxy') }}</Label>
|
||||
<Input id="galaxy" v-model.number="targetPosition.galaxy" type="number" min="1" max="9" placeholder="1" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="system" class="text-xs sm:text-sm">{{ t('fleetView.system') }}</Label>
|
||||
<Input id="system" v-model.number="targetPosition.system" type="number" min="1" max="10" placeholder="1" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="position" class="text-xs sm:text-sm">{{ t('fleetView.position') }}</Label>
|
||||
<Input id="position" v-model.number="targetPosition.position" type="number" min="1" max="10" placeholder="1" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 任务类型 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.missionType') }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
<Button
|
||||
v-for="mission in availableMissions"
|
||||
:key="mission.type"
|
||||
@click="selectedMission = mission.type"
|
||||
:variant="selectedMission === mission.type ? 'default' : 'outline'"
|
||||
class="justify-start"
|
||||
>
|
||||
<component :is="mission.icon" class="h-4 w-4 mr-2" />
|
||||
{{ mission.name }}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 运输资源(仅运输任务) -->
|
||||
<Card v-if="selectedMission === MissionType.Transport">
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.transportResources') }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 sm:gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="cargo-metal" class="text-xs sm:text-sm flex items-center gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
{{ t('resources.metal') }} ({{ t('fleetView.available') }}: {{ formatNumber(planet.resources.metal) }})
|
||||
</Label>
|
||||
<Input id="cargo-metal" v-model.number="cargo.metal" type="number" min="0" :max="planet.resources.metal" placeholder="0" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="cargo-crystal" class="text-xs sm:text-sm flex items-center gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
{{ t('resources.crystal') }} ({{ t('fleetView.available') }}: {{ formatNumber(planet.resources.crystal) }})
|
||||
</Label>
|
||||
<Input
|
||||
id="cargo-crystal"
|
||||
v-model.number="cargo.crystal"
|
||||
type="number"
|
||||
min="0"
|
||||
:max="planet.resources.crystal"
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="cargo-deuterium" class="text-xs sm:text-sm flex items-center gap-2">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
{{ t('resources.deuterium') }} ({{ t('fleetView.available') }}: {{ formatNumber(planet.resources.deuterium) }})
|
||||
</Label>
|
||||
<Input
|
||||
id="cargo-deuterium"
|
||||
v-model.number="cargo.deuterium"
|
||||
type="number"
|
||||
min="0"
|
||||
:max="planet.resources.deuterium"
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs sm:text-sm text-muted-foreground mt-2">
|
||||
{{ t('fleetView.totalCargoCapacity') }}: {{ formatNumber(getTotalCargoCapacity()) }} | {{ t('fleetView.used') }}:
|
||||
{{ formatNumber(getTotalCargo()) }}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 任务信息 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.missionInfo') }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-2">
|
||||
<div class="flex justify-between text-xs sm:text-sm">
|
||||
<span class="text-muted-foreground">{{ t('fleetView.fuelConsumption') }}:</span>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
<span :class="getFuelConsumption() > planet.resources.deuterium ? 'text-red-600 dark:text-red-400 font-medium' : ''">
|
||||
{{ formatNumber(getFuelConsumption()) }}
|
||||
</span>
|
||||
<span class="text-muted-foreground">/ {{ formatNumber(planet.resources.deuterium) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="Object.values(selectedFleet).some(c => c > 0)" class="flex justify-between text-xs sm:text-sm">
|
||||
<span class="text-muted-foreground">{{ t('fleetView.flightTime') }}:</span>
|
||||
<span>{{ formatTime(getFlightTime()) }}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 派遣按钮 -->
|
||||
<Button @click="handleSendFleet" :disabled="!canSendFleet()" class="w-full" size="lg">{{ t('fleetView.sendFleet') }}</Button>
|
||||
</div>
|
||||
|
||||
<!-- 飞行任务 -->
|
||||
<div v-if="activeTab === 'missions'" class="space-y-4">
|
||||
<Card v-if="gameStore.player.fleetMissions.length === 0">
|
||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('fleetView.noFlightMissions') }}</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card v-for="mission in gameStore.player.fleetMissions" :key="mission.id">
|
||||
<CardHeader>
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<CardTitle class="text-base sm:text-lg">{{ getMissionName(mission.missionType) }}</CardTitle>
|
||||
<CardDescription class="text-xs sm:text-sm">
|
||||
{{ getPlanetName(mission.originPlanetId) }} → [{{ mission.targetPosition.galaxy }}:{{ mission.targetPosition.system }}:{{
|
||||
mission.targetPosition.position
|
||||
}}]
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge :variant="mission.status === 'outbound' ? 'default' : 'secondary'">
|
||||
{{ mission.status === 'outbound' ? t('fleetView.outbound') : t('fleetView.returning') }}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-3">
|
||||
<!-- 舰队组成 -->
|
||||
<div>
|
||||
<p class="text-xs sm:text-sm font-medium mb-2">{{ t('fleetView.fleetComposition') }}:</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Badge v-for="(count, shipType) in mission.fleet" :key="shipType" variant="outline">
|
||||
{{ SHIPS[shipType].name }}: {{ count }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 携带资源 -->
|
||||
<div v-if="mission.cargo.metal > 0 || mission.cargo.crystal > 0 || mission.cargo.deuterium > 0 || mission.cargo.darkMatter > 0">
|
||||
<p class="text-xs sm:text-sm font-medium mb-2">{{ t('fleetView.carryingResources') }}:</p>
|
||||
<div class="flex flex-wrap gap-2 text-xs">
|
||||
<span v-if="mission.cargo.metal > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
{{ formatNumber(mission.cargo.metal) }}
|
||||
</span>
|
||||
<span v-if="mission.cargo.crystal > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
{{ formatNumber(mission.cargo.crystal) }}
|
||||
</span>
|
||||
<span v-if="mission.cargo.deuterium > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
{{ formatNumber(mission.cargo.deuterium) }}
|
||||
</span>
|
||||
<span v-if="mission.cargo.darkMatter > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="darkMatter" size="sm" />
|
||||
{{ formatNumber(mission.cargo.darkMatter) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between text-xs sm:text-sm">
|
||||
<span>{{ mission.status === 'outbound' ? t('fleetView.arrivalTime') : t('fleetView.returnTime') }}:</span>
|
||||
<span>{{ formatTime(getRemainingTime(mission)) }}</span>
|
||||
</div>
|
||||
<Progress :model-value="getMissionProgress(mission)" />
|
||||
</div>
|
||||
|
||||
<!-- 操作 -->
|
||||
<div class="flex gap-2">
|
||||
<Button v-if="mission.status === 'outbound'" @click="handleRecallFleet(mission.id)" variant="outline" size="sm" class="w-full">
|
||||
{{ t('fleetView.recallFleet') }}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- 提示对话框 -->
|
||||
<AlertDialog ref="alertDialog" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { useGameConfig } from '@/composables/useGameConfig'
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ShipType, MissionType, BuildingType } from '@/types/game'
|
||||
import type { Fleet, Resources } from '@/types/game'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import AlertDialog from '@/components/AlertDialog.vue'
|
||||
import UnlockRequirement from '@/components/UnlockRequirement.vue'
|
||||
import { Sword, Package, Rocket as RocketIcon, Eye, Users } from 'lucide-vue-next'
|
||||
import { formatNumber, formatTime } from '@/utils/format'
|
||||
import * as shipValidation from '@/logic/shipValidation'
|
||||
import * as fleetLogic from '@/logic/fleetLogic'
|
||||
import * as shipLogic from '@/logic/shipLogic'
|
||||
import * as officerLogic from '@/logic/officerLogic'
|
||||
import * as publicLogic from '@/logic/publicLogic'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const gameStore = useGameStore()
|
||||
const { t } = useI18n()
|
||||
const { SHIPS } = useGameConfig()
|
||||
const planet = computed(() => gameStore.currentPlanet)
|
||||
const alertDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
|
||||
|
||||
// 计算最大舰队任务槽位
|
||||
const maxFleetMissions = computed(() => {
|
||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
|
||||
return publicLogic.getMaxFleetMissions(bonuses.additionalFleetSlots)
|
||||
})
|
||||
|
||||
const activeTab = ref<'fleet' | 'send' | 'missions'>('fleet')
|
||||
|
||||
// 选择的舰队
|
||||
const selectedFleet = ref<Partial<Fleet>>({
|
||||
[ShipType.LightFighter]: 0,
|
||||
[ShipType.HeavyFighter]: 0,
|
||||
[ShipType.Cruiser]: 0,
|
||||
[ShipType.Battleship]: 0,
|
||||
[ShipType.SmallCargo]: 0,
|
||||
[ShipType.LargeCargo]: 0,
|
||||
[ShipType.ColonyShip]: 0,
|
||||
[ShipType.Recycler]: 0,
|
||||
[ShipType.EspionageProbe]: 0
|
||||
})
|
||||
|
||||
// 目标坐标
|
||||
const targetPosition = ref({ galaxy: 1, system: 1, position: 1 })
|
||||
|
||||
// 选择的任务类型
|
||||
const selectedMission = ref<MissionType>(MissionType.Attack)
|
||||
|
||||
// 运输资源
|
||||
const cargo = ref({ metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 })
|
||||
|
||||
// 从 URL query 参数初始化
|
||||
onMounted(() => {
|
||||
const { galaxy, system, position, mission } = route.query
|
||||
|
||||
// 如果有参数,填充数据
|
||||
if (galaxy || system || position) {
|
||||
// 设置目标坐标
|
||||
if (galaxy) targetPosition.value.galaxy = Number(galaxy)
|
||||
if (system) targetPosition.value.system = Number(system)
|
||||
if (position) targetPosition.value.position = Number(position)
|
||||
|
||||
// 设置任务类型
|
||||
if (mission === 'spy') {
|
||||
selectedMission.value = MissionType.Spy
|
||||
} else if (mission === 'attack') {
|
||||
selectedMission.value = MissionType.Attack
|
||||
} else if (mission === 'colonize') {
|
||||
selectedMission.value = MissionType.Colonize
|
||||
}
|
||||
|
||||
// 自动切换到派遣舰队标签
|
||||
activeTab.value = 'send'
|
||||
|
||||
// 清除 URL 参数,保持 URL 整洁
|
||||
router.replace({ path: '/fleet' })
|
||||
}
|
||||
})
|
||||
|
||||
// 可用任务类型
|
||||
const availableMissions = computed(() => [
|
||||
{ type: MissionType.Attack, name: t('fleetView.attackMission'), icon: Sword },
|
||||
{ type: MissionType.Transport, name: t('fleetView.transport'), icon: Package },
|
||||
{ type: MissionType.Colonize, name: t('fleetView.colonize'), icon: RocketIcon },
|
||||
{ type: MissionType.Spy, name: t('fleetView.spy'), icon: Eye },
|
||||
{ type: MissionType.Deploy, name: t('fleetView.deploy'), icon: Users }
|
||||
])
|
||||
|
||||
// 获取任务名称
|
||||
const getMissionName = (type: MissionType): string => {
|
||||
const mission = availableMissions.value.find(m => m.type === type)
|
||||
return mission?.name || type
|
||||
}
|
||||
|
||||
// 获取星球名称
|
||||
const getPlanetName = (planetId: string): string => {
|
||||
const p = gameStore.player.planets.find(p => p.id === planetId)
|
||||
return p?.name || t('fleetView.unknownPlanet')
|
||||
}
|
||||
|
||||
// 计算总载货量
|
||||
const getTotalCargoCapacity = (): number => {
|
||||
let total = 0
|
||||
for (const [shipType, count] of Object.entries(selectedFleet.value)) {
|
||||
if (count > 0) {
|
||||
const config = SHIPS.value[shipType as ShipType]
|
||||
total += config.cargoCapacity * count
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// 计算总货物
|
||||
const getTotalCargo = (): number => {
|
||||
return cargo.value.metal + cargo.value.crystal + cargo.value.deuterium + cargo.value.darkMatter
|
||||
}
|
||||
|
||||
// 计算燃料消耗(包含货物重量影响)
|
||||
const getFuelConsumption = (): number => {
|
||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
|
||||
return shipLogic.calculateFleetFuelConsumption(selectedFleet.value, bonuses.fuelConsumptionReduction, cargo.value)
|
||||
}
|
||||
|
||||
// 计算飞行时间
|
||||
const getFlightTime = (): number => {
|
||||
if (!planet.value) return 0
|
||||
const distance = fleetLogic.calculateDistance(planet.value.position, targetPosition.value)
|
||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
|
||||
const minSpeed = shipLogic.calculateFleetMinSpeed(selectedFleet.value, bonuses.fleetSpeedBonus)
|
||||
return fleetLogic.calculateFlightTime(distance, minSpeed)
|
||||
}
|
||||
|
||||
// 检查是否可以派遣
|
||||
const canSendFleet = (): boolean => {
|
||||
// 检查是否选择了舰船
|
||||
const hasShips = Object.values(selectedFleet.value).some(count => count > 0)
|
||||
if (!hasShips) return false
|
||||
|
||||
// 检查载货量
|
||||
if (selectedMission.value === MissionType.Transport) {
|
||||
if (getTotalCargo() > getTotalCargoCapacity()) return false
|
||||
}
|
||||
|
||||
// 检查殖民船
|
||||
if (selectedMission.value === MissionType.Colonize) {
|
||||
if (!selectedFleet.value[ShipType.ColonyShip] || (selectedFleet.value[ShipType.ColonyShip] ?? 0) < 1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const sendFleet = (
|
||||
targetPosition: { galaxy: number; system: number; position: number },
|
||||
missionType: MissionType,
|
||||
fleet: Partial<Fleet>,
|
||||
cargo: Resources = { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 }
|
||||
): boolean => {
|
||||
if (!gameStore.currentPlanet) return false
|
||||
const currentMissions = gameStore.player.fleetMissions.length
|
||||
const validation = shipValidation.validateFleetDispatch(
|
||||
gameStore.currentPlanet,
|
||||
fleet,
|
||||
cargo,
|
||||
gameStore.player.officers,
|
||||
currentMissions
|
||||
)
|
||||
if (!validation.valid) return false
|
||||
const shouldDeductCargo = missionType === MissionType.Transport
|
||||
shipValidation.executeFleetDispatch(gameStore.currentPlanet, fleet, validation.fuelNeeded!, shouldDeductCargo, cargo)
|
||||
const distance = fleetLogic.calculateDistance(gameStore.currentPlanet.position, targetPosition)
|
||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
|
||||
const minSpeed = shipLogic.calculateFleetMinSpeed(fleet, bonuses.fleetSpeedBonus)
|
||||
const flightTime = fleetLogic.calculateFlightTime(distance, minSpeed)
|
||||
const mission = fleetLogic.createFleetMission(
|
||||
gameStore.player.id,
|
||||
gameStore.currentPlanet.id,
|
||||
targetPosition,
|
||||
missionType,
|
||||
fleet,
|
||||
cargo,
|
||||
flightTime
|
||||
)
|
||||
gameStore.player.fleetMissions.push(mission)
|
||||
return true
|
||||
}
|
||||
|
||||
// 派遣舰队
|
||||
const handleSendFleet = () => {
|
||||
if (!planet.value) return
|
||||
|
||||
// 过滤出实际选择的舰船
|
||||
const fleet: Partial<Fleet> = {}
|
||||
for (const [shipType, count] of Object.entries(selectedFleet.value)) {
|
||||
if (count > 0) {
|
||||
fleet[shipType as ShipType] = count
|
||||
}
|
||||
}
|
||||
|
||||
const success = sendFleet(
|
||||
targetPosition.value,
|
||||
selectedMission.value,
|
||||
fleet,
|
||||
selectedMission.value === MissionType.Transport ? cargo.value : undefined
|
||||
)
|
||||
|
||||
if (success) {
|
||||
// 重置选择
|
||||
Object.keys(selectedFleet.value).forEach(key => {
|
||||
selectedFleet.value[key as ShipType] = 0
|
||||
})
|
||||
cargo.value = { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 }
|
||||
activeTab.value = 'missions'
|
||||
} else {
|
||||
alertDialog.value?.show({
|
||||
title: t('fleetView.sendFailed'),
|
||||
message: t('fleetView.sendFailedMessage')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const recallFleet = (missionId: string): boolean => {
|
||||
const mission = gameStore.player.fleetMissions.find(m => m.id === missionId)
|
||||
if (!mission) return false
|
||||
return fleetLogic.recallFleetMission(mission, Date.now())
|
||||
}
|
||||
|
||||
// 召回舰队
|
||||
const handleRecallFleet = (missionId: string) => {
|
||||
const success = recallFleet(missionId)
|
||||
if (!success) {
|
||||
alertDialog.value?.show({
|
||||
title: t('fleetView.recallFailed'),
|
||||
message: t('fleetView.recallFailedMessage')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 获取任务剩余时间
|
||||
const getRemainingTime = (mission: any): number => {
|
||||
const now = Date.now()
|
||||
const targetTime = mission.status === 'outbound' ? mission.arrivalTime : mission.returnTime
|
||||
return Math.max(0, (targetTime - now) / 1000)
|
||||
}
|
||||
|
||||
// 获取任务进度
|
||||
const getMissionProgress = (mission: any): number => {
|
||||
const now = Date.now()
|
||||
if (mission.status === 'outbound') {
|
||||
const total = mission.arrivalTime - mission.departureTime
|
||||
const elapsed = now - mission.departureTime
|
||||
return Math.min(100, (elapsed / total) * 100)
|
||||
} else {
|
||||
const departTime = mission.arrivalTime
|
||||
const total = mission.returnTime - departTime
|
||||
const elapsed = now - departTime
|
||||
return Math.min(100, (elapsed / total) * 100)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
264
src/views/GalaxyView.vue
Normal file
264
src/views/GalaxyView.vue
Normal file
@@ -0,0 +1,264 @@
|
||||
<template>
|
||||
<div class="container mx-auto p-4 sm:p-6 space-y-4 sm:space-y-6">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('galaxyView.title') }}</h1>
|
||||
|
||||
<!-- 坐标选择器 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('galaxyView.selectCoordinates') }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3 sm:gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="select-galaxy" class="text-xs sm:text-sm">{{ t('galaxyView.galaxy') }}</Label>
|
||||
<Select
|
||||
:key="gameStore.locale"
|
||||
:model-value="String(selectedGalaxy)"
|
||||
@update:model-value="
|
||||
val => {
|
||||
selectedGalaxy = Number(val)
|
||||
loadSystem()
|
||||
}
|
||||
"
|
||||
>
|
||||
<SelectTrigger id="select-galaxy" class="w-full">
|
||||
<SelectValue :placeholder="t('galaxyView.selectGalaxy')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="g in 9" :key="g" :value="String(g)">{{ t('galaxyView.galaxy') }} {{ g }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="select-system" class="text-xs sm:text-sm">{{ t('galaxyView.system') }}</Label>
|
||||
<Select
|
||||
:key="`${gameStore.locale}-system`"
|
||||
:model-value="String(selectedSystem)"
|
||||
@update:model-value="
|
||||
val => {
|
||||
selectedSystem = Number(val)
|
||||
loadSystem()
|
||||
}
|
||||
"
|
||||
>
|
||||
<SelectTrigger id="select-system" class="w-full">
|
||||
<SelectValue :placeholder="t('galaxyView.selectSystem')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="s in 10" :key="s" :value="String(s)">{{ t('galaxyView.system') }} {{ s }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="col-span-2 sm:col-span-1 flex items-end">
|
||||
<Button @click="goToCurrentPlanet" variant="outline" class="w-full">
|
||||
<Home class="h-4 w-4 mr-2" />
|
||||
{{ t('galaxyView.myPlanet') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 星系视图 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('galaxyView.galaxy') }} {{ currentGalaxy }}:{{ currentSystem }}</CardTitle>
|
||||
<CardDescription>{{ t('galaxyView.totalPositions') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="slot in systemSlots"
|
||||
:key="slot.position"
|
||||
class="flex items-center gap-2 sm:gap-4 p-2 sm:p-3 border rounded-lg hover:bg-muted/50 transition-colors"
|
||||
:class="{
|
||||
'bg-blue-50 dark:bg-blue-950 border-blue-300 dark:border-blue-700': isMyPlanet(slot.planet),
|
||||
'bg-muted/30': !slot.planet
|
||||
}"
|
||||
>
|
||||
<!-- 位置编号 -->
|
||||
<div class="w-8 sm:w-12 text-center">
|
||||
<Badge variant="outline" class="text-xs sm:text-sm">{{ slot.position }}</Badge>
|
||||
</div>
|
||||
|
||||
<!-- 星球信息 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div v-if="slot.planet" class="space-y-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="font-semibold text-sm sm:text-base truncate">{{ slot.planet.name }}</h3>
|
||||
<Badge v-if="isMyPlanet(slot.planet)" variant="default" class="text-xs">{{ t('galaxyView.mine') }}</Badge>
|
||||
<Badge v-else variant="secondary" class="text-xs">{{ t('galaxyView.hostile') }}</Badge>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
[{{ slot.planet.position.galaxy }}:{{ slot.planet.position.system }}:{{ slot.planet.position.position }}]
|
||||
</p>
|
||||
</div>
|
||||
<div v-else class="text-sm text-muted-foreground">{{ t('galaxyView.emptySlot') }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex gap-1 sm:gap-2 flex-shrink-0">
|
||||
<Button
|
||||
v-if="slot.planet && !isMyPlanet(slot.planet)"
|
||||
@click="showPlanetActions(slot.planet, 'spy')"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-8 w-8 p-0"
|
||||
:title="t('galaxyView.scout')"
|
||||
>
|
||||
<Eye class="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
v-if="slot.planet && !isMyPlanet(slot.planet)"
|
||||
@click="showPlanetActions(slot.planet, 'attack')"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-8 w-8 p-0"
|
||||
:title="t('galaxyView.attack')"
|
||||
>
|
||||
<Sword class="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
v-if="!slot.planet"
|
||||
@click="showPlanetActions(null, 'colonize', slot.position)"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-8 w-8 p-0"
|
||||
:title="t('galaxyView.colonize')"
|
||||
>
|
||||
<Rocket class="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
v-if="slot.planet && isMyPlanet(slot.planet)"
|
||||
@click="switchToPlanet(slot.planet.id)"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-8 w-8 p-0"
|
||||
:title="t('galaxyView.switch')"
|
||||
>
|
||||
<Home class="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 快速派遣对话框 -->
|
||||
<AlertDialog ref="actionDialog" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import type { Planet } from '@/types/game'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select'
|
||||
import AlertDialog from '@/components/AlertDialog.vue'
|
||||
import { Home, Eye, Sword, Rocket } from 'lucide-vue-next'
|
||||
import { useRouter } from 'vue-router'
|
||||
import * as gameLogic from '@/logic/gameLogic'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
const actionDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
|
||||
|
||||
const selectedGalaxy = ref(1)
|
||||
const selectedSystem = ref(1)
|
||||
const currentGalaxy = ref(1)
|
||||
const currentSystem = ref(1)
|
||||
|
||||
const systemSlots = ref<Array<{ position: number; planet: Planet | null }>>([])
|
||||
|
||||
onMounted(() => {
|
||||
// 默认显示当前星球所在的星系
|
||||
if (gameStore.currentPlanet) {
|
||||
currentGalaxy.value = gameStore.currentPlanet.position.galaxy
|
||||
currentSystem.value = gameStore.currentPlanet.position.system
|
||||
selectedGalaxy.value = currentGalaxy.value
|
||||
selectedSystem.value = currentSystem.value
|
||||
loadSystem()
|
||||
}
|
||||
})
|
||||
|
||||
const getSystemPlanets = (galaxy: number, system: number): Array<{ position: number; planet: Planet | null }> => {
|
||||
const positions = gameLogic.generateSystemPositions(galaxy, system)
|
||||
return positions.map(pos => {
|
||||
const key = gameLogic.generatePositionKey(galaxy, system, pos.position)
|
||||
const planet = gameStore.universePlanets[key] || null
|
||||
return { position: pos.position, planet }
|
||||
})
|
||||
}
|
||||
|
||||
// 加载星系
|
||||
const loadSystem = () => {
|
||||
currentGalaxy.value = selectedGalaxy.value
|
||||
currentSystem.value = selectedSystem.value
|
||||
systemSlots.value = getSystemPlanets(currentGalaxy.value, currentSystem.value)
|
||||
}
|
||||
|
||||
// 跳转到当前星球
|
||||
const goToCurrentPlanet = () => {
|
||||
if (gameStore.currentPlanet) {
|
||||
currentGalaxy.value = gameStore.currentPlanet.position.galaxy
|
||||
currentSystem.value = gameStore.currentPlanet.position.system
|
||||
selectedGalaxy.value = currentGalaxy.value
|
||||
selectedSystem.value = currentSystem.value
|
||||
loadSystem()
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否为我的星球
|
||||
const isMyPlanet = (planet: Planet | null): boolean => {
|
||||
if (!planet) return false
|
||||
return planet.ownerId === gameStore.player.id
|
||||
}
|
||||
|
||||
// 切换到指定星球
|
||||
const switchToPlanet = (planetId: string) => {
|
||||
gameStore.currentPlanetId = planetId
|
||||
router.push('/overview')
|
||||
}
|
||||
|
||||
// 显示星球操作
|
||||
const showPlanetActions = (planet: Planet | null, action: 'spy' | 'attack' | 'colonize', position?: number) => {
|
||||
const targetPos = planet ? planet.position : { galaxy: currentGalaxy.value, system: currentSystem.value, position: position! }
|
||||
const coordinates = `${targetPos.galaxy}:${targetPos.system}:${targetPos.position}`
|
||||
|
||||
let message = ''
|
||||
let title = ''
|
||||
if (action === 'spy') {
|
||||
title = t('galaxyView.scoutPlanetTitle')
|
||||
message = t('galaxyView.scoutPlanetMessage').replace('{coordinates}', coordinates)
|
||||
} else if (action === 'attack') {
|
||||
title = t('galaxyView.attackPlanetTitle')
|
||||
message = t('galaxyView.attackPlanetMessage').replace('{coordinates}', coordinates)
|
||||
} else if (action === 'colonize') {
|
||||
title = t('galaxyView.colonizePlanetTitle')
|
||||
message = t('galaxyView.colonizePlanetMessage').replace('{coordinates}', coordinates)
|
||||
}
|
||||
|
||||
actionDialog.value?.show({
|
||||
title,
|
||||
message,
|
||||
onConfirm: () => {
|
||||
// 跳转到舰队页面并填充目标坐标
|
||||
router.push({
|
||||
path: '/fleet',
|
||||
query: {
|
||||
galaxy: targetPos.galaxy,
|
||||
system: targetPos.system,
|
||||
position: targetPos.position,
|
||||
mission: action
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
252
src/views/MessagesView.vue
Normal file
252
src/views/MessagesView.vue
Normal file
@@ -0,0 +1,252 @@
|
||||
<template>
|
||||
<div class="container mx-auto p-4 sm:p-6 space-y-4 sm:space-y-6">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('messagesView.title') }}</h1>
|
||||
|
||||
<!-- 标签切换 -->
|
||||
<div class="flex gap-2 border-b">
|
||||
<Button @click="activeTab = 'battles'" :variant="activeTab === 'battles' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
{{ t('messagesView.battleReports') }}
|
||||
<Badge v-if="gameStore.player.battleReports.length > 0" variant="secondary" class="ml-1">
|
||||
{{ gameStore.player.battleReports.length }}
|
||||
</Badge>
|
||||
</Button>
|
||||
<Button @click="activeTab = 'spy'" :variant="activeTab === 'spy' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
{{ t('messagesView.spyReports') }}
|
||||
<Badge v-if="gameStore.player.spyReports.length > 0" variant="secondary" class="ml-1">
|
||||
{{ gameStore.player.spyReports.length }}
|
||||
</Badge>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 战斗报告 -->
|
||||
<div v-if="activeTab === 'battles'" class="space-y-4">
|
||||
<Card v-if="gameStore.player.battleReports.length === 0">
|
||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('messagesView.noBattleReports') }}</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card v-for="report in sortedBattleReports" :key="report.id">
|
||||
<CardHeader>
|
||||
<div class="flex justify-between items-start gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<CardTitle class="text-base sm:text-lg">{{ t('messagesView.battleReport') }}</CardTitle>
|
||||
<CardDescription class="text-xs sm:text-sm">
|
||||
{{ formatDate(report.timestamp) }}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge :variant="report.winner === 'attacker' ? 'default' : report.winner === 'defender' ? 'destructive' : 'secondary'">
|
||||
{{ report.winner === 'attacker' ? t('messagesView.victory') : report.winner === 'defender' ? t('messagesView.defeat') : t('messagesView.draw') }}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<!-- 攻击方舰队 -->
|
||||
<div>
|
||||
<p class="text-sm font-medium mb-2">{{ t('messagesView.attackerFleet') }}:</p>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
<div v-for="(count, shipType) in report.attackerFleet" :key="shipType" class="text-xs sm:text-sm">
|
||||
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
|
||||
<span class="ml-1 font-medium">{{ count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 防守方舰队 -->
|
||||
<div>
|
||||
<p class="text-sm font-medium mb-2">{{ t('messagesView.defenderFleet') }}:</p>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
<div v-for="(count, shipType) in report.defenderFleet" :key="shipType" class="text-xs sm:text-sm">
|
||||
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
|
||||
<span class="ml-1 font-medium">{{ count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 防守方防御 -->
|
||||
<div v-if="hasDefense(report.defenderDefense)">
|
||||
<p class="text-sm font-medium mb-2">{{ t('messagesView.defenderDefense') }}:</p>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
<div v-for="(count, defenseType) in report.defenderDefense" :key="defenseType" class="text-xs sm:text-sm">
|
||||
<span class="text-muted-foreground">{{ DEFENSES[defenseType].name }}:</span>
|
||||
<span class="ml-1 font-medium">{{ count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 损失 -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div class="p-3 bg-muted rounded-lg">
|
||||
<p class="text-sm font-medium mb-2 text-red-600">{{ t('messagesView.attackerLosses') }}:</p>
|
||||
<div class="space-y-1 text-xs">
|
||||
<div v-for="(count, shipType) in report.attackerLosses" :key="shipType">{{ SHIPS[shipType].name }}: {{ count }}</div>
|
||||
<p v-if="Object.keys(report.attackerLosses).length === 0" class="text-muted-foreground">{{ t('messagesView.noLosses') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3 bg-muted rounded-lg">
|
||||
<p class="text-sm font-medium mb-2 text-red-600">{{ t('messagesView.defenderLosses') }}:</p>
|
||||
<div class="space-y-1 text-xs">
|
||||
<div v-for="(count, shipType) in report.defenderLosses.fleet" :key="shipType">{{ SHIPS[shipType].name }}: {{ count }}</div>
|
||||
<div v-for="(count, defenseType) in report.defenderLosses.defense" :key="defenseType">
|
||||
{{ DEFENSES[defenseType].name }}: {{ count }}
|
||||
</div>
|
||||
<p
|
||||
v-if="Object.keys(report.defenderLosses.fleet).length === 0 && Object.keys(report.defenderLosses.defense).length === 0"
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
{{ t('messagesView.noLosses') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 掠夺资源 -->
|
||||
<div
|
||||
v-if="report.plunder.metal > 0 || report.plunder.crystal > 0 || report.plunder.deuterium > 0"
|
||||
class="p-3 bg-green-50 dark:bg-green-950 rounded-lg"
|
||||
>
|
||||
<p class="text-sm font-medium mb-2 text-green-600">{{ t('messagesView.plunder') }}:</p>
|
||||
<div class="flex flex-wrap gap-3 text-xs sm:text-sm">
|
||||
<span v-if="report.plunder.metal > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
{{ formatNumber(report.plunder.metal) }}
|
||||
</span>
|
||||
<span v-if="report.plunder.crystal > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
{{ formatNumber(report.plunder.crystal) }}
|
||||
</span>
|
||||
<span v-if="report.plunder.deuterium > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
{{ formatNumber(report.plunder.deuterium) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 残骸场 -->
|
||||
<div v-if="report.debrisField.metal > 0 || report.debrisField.crystal > 0" class="p-3 bg-muted rounded-lg">
|
||||
<p class="text-sm font-medium mb-2">{{ t('messagesView.debrisField') }}:</p>
|
||||
<div class="flex flex-wrap gap-3 text-xs sm:text-sm">
|
||||
<span v-if="report.debrisField.metal > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
{{ formatNumber(report.debrisField.metal) }}
|
||||
</span>
|
||||
<span v-if="report.debrisField.crystal > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
{{ formatNumber(report.debrisField.crystal) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- 间谍报告 -->
|
||||
<div v-if="activeTab === 'spy'" class="space-y-4">
|
||||
<Card v-if="gameStore.player.spyReports.length === 0">
|
||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('messagesView.noSpyReports') }}</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card v-for="report in sortedSpyReports" :key="report.id">
|
||||
<CardHeader>
|
||||
<div class="flex justify-between items-start gap-2">
|
||||
<div>
|
||||
<CardTitle class="text-base sm:text-lg">{{ t('messagesView.spyReport') }}</CardTitle>
|
||||
<CardDescription class="text-xs sm:text-sm">
|
||||
{{ formatDate(report.timestamp) }}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge variant="outline">
|
||||
{{ report.targetPlanetId }}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<!-- 资源 -->
|
||||
<div>
|
||||
<p class="text-sm font-medium mb-2">{{ t('messagesView.resources') }}:</p>
|
||||
<div class="flex flex-wrap gap-3 text-xs sm:text-sm">
|
||||
<span class="flex items-center gap-1">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
{{ formatNumber(report.resources.metal) }}
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
{{ formatNumber(report.resources.crystal) }}
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
{{ formatNumber(report.resources.deuterium) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 舰队 -->
|
||||
<div v-if="report.fleet">
|
||||
<p class="text-sm font-medium mb-2">{{ t('messagesView.fleet') }}:</p>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
<div v-for="(count, shipType) in report.fleet" :key="shipType" class="text-xs sm:text-sm">
|
||||
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
|
||||
<span class="ml-1 font-medium">{{ count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 防御 -->
|
||||
<div v-if="report.defense && hasDefense(report.defense)">
|
||||
<p class="text-sm font-medium mb-2">{{ t('messagesView.defense') }}:</p>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
<div v-for="(count, defenseType) in report.defense" :key="defenseType" class="text-xs sm:text-sm">
|
||||
<span class="text-muted-foreground">{{ DEFENSES[defenseType].name }}:</span>
|
||||
<span class="ml-1 font-medium">{{ count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 建筑 -->
|
||||
<div v-if="report.buildings">
|
||||
<p class="text-sm font-medium mb-2">{{ t('messagesView.buildings') }}:</p>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
<template v-for="(level, buildingType) in report.buildings" :key="buildingType">
|
||||
<div v-if="level && level > 0" class="text-xs sm:text-sm">
|
||||
<span class="text-muted-foreground">{{ BUILDINGS[buildingType].name }}:</span>
|
||||
<span class="ml-1 font-medium">Lv {{ level }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { useGameConfig } from '@/composables/useGameConfig'
|
||||
import { computed, ref } from 'vue'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import { formatNumber, formatDate } from '@/utils/format'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const { t } = useI18n()
|
||||
const { SHIPS, DEFENSES, BUILDINGS } = useGameConfig()
|
||||
const activeTab = ref<'battles' | 'spy'>('battles')
|
||||
|
||||
// 排序后的战斗报告(最新的在前)
|
||||
const sortedBattleReports = computed(() => {
|
||||
return [...gameStore.player.battleReports].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
// 排序后的间谍报告(最新的在前)
|
||||
const sortedSpyReports = computed(() => {
|
||||
return [...gameStore.player.spyReports].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
// 检查是否有防御设施
|
||||
const hasDefense = (defense: any): boolean => {
|
||||
if (!defense) return false
|
||||
return Object.values(defense).some((count: any) => count > 0)
|
||||
}
|
||||
</script>
|
||||
269
src/views/OfficersView.vue
Normal file
269
src/views/OfficersView.vue
Normal file
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<div class="container mx-auto p-4 sm:p-6 space-y-4 sm:space-y-6">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('officersView.title') }}</h1>
|
||||
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-4 sm:gap-6">
|
||||
<Card v-for="officerType in Object.values(OfficerType)" :key="officerType">
|
||||
<CardHeader>
|
||||
<div class="flex justify-between items-start gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<CardTitle class="text-lg sm:text-xl">{{ OFFICERS[officerType].name }}</CardTitle>
|
||||
<CardDescription class="text-xs sm:text-sm">{{ OFFICERS[officerType].description }}</CardDescription>
|
||||
</div>
|
||||
<Badge v-if="isOfficerActive(officerType)" variant="default" class="text-xs">{{ t('officersView.activated') }}</Badge>
|
||||
<Badge v-else variant="outline" class="text-xs">{{ t('officersView.inactive') }}</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<!-- 状态信息 -->
|
||||
<div v-if="isOfficerActive(officerType)" class="p-3 bg-blue-50 dark:bg-blue-950 rounded-lg">
|
||||
<div class="space-y-1 text-xs sm:text-sm">
|
||||
<p class="font-medium text-blue-700 dark:text-blue-300">{{ t('officersView.activeStatus') }}</p>
|
||||
<p class="text-muted-foreground">
|
||||
{{ t('officersView.expirationTime') }}: {{ formatDate(getOfficerExpiration(officerType)) }}
|
||||
</p>
|
||||
<p class="text-muted-foreground">{{ t('officersView.remainingTime') }}: {{ formatTime(getRemainingTime(officerType)) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 招募成本 -->
|
||||
<div class="space-y-2">
|
||||
<p class="text-sm font-medium text-muted-foreground">{{ t('officersView.recruitCost') }} (7{{ t('officersView.days') }}):</p>
|
||||
<div class="space-y-1.5">
|
||||
<div class="flex items-center gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.metal') }}:</span>
|
||||
<span
|
||||
class="font-medium text-sm"
|
||||
:class="planet ? getResourceCostColor(planet.resources.metal, OFFICERS[officerType].cost.metal) : ''"
|
||||
>
|
||||
{{ formatNumber(OFFICERS[officerType].cost.metal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.crystal') }}:</span>
|
||||
<span
|
||||
class="font-medium text-sm"
|
||||
:class="planet ? getResourceCostColor(planet.resources.crystal, OFFICERS[officerType].cost.crystal) : ''"
|
||||
>
|
||||
{{ formatNumber(OFFICERS[officerType].cost.crystal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.deuterium') }}:</span>
|
||||
<span
|
||||
class="font-medium text-sm"
|
||||
:class="planet ? getResourceCostColor(planet.resources.deuterium, OFFICERS[officerType].cost.deuterium) : ''"
|
||||
>
|
||||
{{ formatNumber(OFFICERS[officerType].cost.deuterium) }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="OFFICERS[officerType].cost.darkMatter > 0" class="flex items-center gap-2">
|
||||
<ResourceIcon type="darkMatter" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.darkMatter') }}:</span>
|
||||
<span
|
||||
class="font-medium text-sm"
|
||||
:class="planet ? getResourceCostColor(planet.resources.darkMatter, OFFICERS[officerType].cost.darkMatter) : ''"
|
||||
>
|
||||
{{ formatNumber(OFFICERS[officerType].cost.darkMatter) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 效果加成 -->
|
||||
<div class="space-y-2">
|
||||
<p class="text-sm font-medium text-muted-foreground">{{ t('officersView.benefitsBonus') }}:</p>
|
||||
<div class="space-y-1 text-xs sm:text-sm">
|
||||
<div v-if="OFFICERS[officerType].benefits.resourceProductionBonus" class="flex items-center gap-2">
|
||||
<span class="text-green-600 dark:text-green-400">↑</span>
|
||||
<span>{{ t('officersView.resourceProduction') }} +{{ OFFICERS[officerType].benefits.resourceProductionBonus }}%</span>
|
||||
</div>
|
||||
<div v-if="OFFICERS[officerType].benefits.darkMatterProductionBonus" class="flex items-center gap-2">
|
||||
<span class="text-green-600 dark:text-green-400">↑</span>
|
||||
<span>{{ t('officersView.darkMatterProduction') }} +{{ OFFICERS[officerType].benefits.darkMatterProductionBonus }}%</span>
|
||||
</div>
|
||||
<div v-if="OFFICERS[officerType].benefits.buildingSpeedBonus" class="flex items-center gap-2">
|
||||
<span class="text-green-600 dark:text-green-400">↑</span>
|
||||
<span>{{ t('officersView.buildingSpeed') }} +{{ OFFICERS[officerType].benefits.buildingSpeedBonus }}%</span>
|
||||
</div>
|
||||
<div v-if="OFFICERS[officerType].benefits.researchSpeedBonus" class="flex items-center gap-2">
|
||||
<span class="text-green-600 dark:text-green-400">↑</span>
|
||||
<span>{{ t('officersView.researchSpeed') }} +{{ OFFICERS[officerType].benefits.researchSpeedBonus }}%</span>
|
||||
</div>
|
||||
<div v-if="OFFICERS[officerType].benefits.fleetSpeedBonus" class="flex items-center gap-2">
|
||||
<span class="text-green-600 dark:text-green-400">↑</span>
|
||||
<span>{{ t('officersView.fleetSpeed') }} +{{ OFFICERS[officerType].benefits.fleetSpeedBonus }}%</span>
|
||||
</div>
|
||||
<div v-if="OFFICERS[officerType].benefits.fuelConsumptionReduction" class="flex items-center gap-2">
|
||||
<span class="text-green-600 dark:text-green-400">↓</span>
|
||||
<span>{{ t('officersView.fuelConsumption') }} -{{ OFFICERS[officerType].benefits.fuelConsumptionReduction }}%</span>
|
||||
</div>
|
||||
<div v-if="OFFICERS[officerType].benefits.defenseBonus" class="flex items-center gap-2">
|
||||
<span class="text-green-600 dark:text-green-400">↑</span>
|
||||
<span>{{ t('officersView.defense') }} +{{ OFFICERS[officerType].benefits.defenseBonus }}%</span>
|
||||
</div>
|
||||
<div v-if="OFFICERS[officerType].benefits.storageCapacityBonus" class="flex items-center gap-2">
|
||||
<span class="text-green-600 dark:text-green-400">↑</span>
|
||||
<span>{{ t('officersView.storageCapacity') }} +{{ OFFICERS[officerType].benefits.storageCapacityBonus }}%</span>
|
||||
</div>
|
||||
<div v-if="OFFICERS[officerType].benefits.additionalBuildQueue" class="flex items-center gap-2">
|
||||
<span class="text-green-600 dark:text-green-400">+</span>
|
||||
<span>{{ t('officersView.buildQueue') }} +{{ OFFICERS[officerType].benefits.additionalBuildQueue }}</span>
|
||||
</div>
|
||||
<div v-if="OFFICERS[officerType].benefits.additionalFleetSlots" class="flex items-center gap-2">
|
||||
<span class="text-green-600 dark:text-green-400">+</span>
|
||||
<span>{{ t('officersView.fleetSlots') }} +{{ OFFICERS[officerType].benefits.additionalFleetSlots }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex gap-2">
|
||||
<Button v-if="!isOfficerActive(officerType)" @click="handleHire(officerType)" :disabled="!canHire(officerType)" class="flex-1">
|
||||
{{ t('officersView.hire') }} (7{{ t('officersView.days') }})
|
||||
</Button>
|
||||
<Button v-if="isOfficerActive(officerType)" @click="handleRenew(officerType)" :disabled="!canHire(officerType)" class="flex-1">
|
||||
{{ t('officersView.renew') }} (7{{ t('officersView.days') }})
|
||||
</Button>
|
||||
<Button v-if="isOfficerActive(officerType)" @click="handleDismiss(officerType)" variant="outline" size="sm">
|
||||
{{ t('officersView.dismiss') }}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- 确认对话框 -->
|
||||
<AlertDialog ref="alertDialog" />
|
||||
<ConfirmDialog ref="confirmDialog" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { computed, ref } from 'vue'
|
||||
import { OfficerType } from '@/types/game'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import AlertDialog from '@/components/AlertDialog.vue'
|
||||
import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
||||
import { formatNumber, formatTime, formatDate, getResourceCostColor } from '@/utils/format'
|
||||
import * as officerLogic from '@/logic/officerLogic'
|
||||
import * as resourceLogic from '@/logic/resourceLogic'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { useGameConfig } from '@/composables/useGameConfig'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { OFFICERS } = useGameConfig()
|
||||
const gameStore = useGameStore()
|
||||
const planet = computed(() => gameStore.currentPlanet)
|
||||
const alertDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
|
||||
const confirmDialog = ref<InstanceType<typeof ConfirmDialog> | null>(null)
|
||||
|
||||
// 检查军官是否激活
|
||||
const isOfficerActive = (officerType: OfficerType): boolean => {
|
||||
const officer = gameStore.player.officers[officerType]
|
||||
const now = Date.now()
|
||||
return officer.active && (!officer.expiresAt || officer.expiresAt > now)
|
||||
}
|
||||
|
||||
// 获取军官到期时间
|
||||
const getOfficerExpiration = (officerType: OfficerType): number => {
|
||||
const officer = gameStore.player.officers[officerType]
|
||||
return officer.expiresAt || 0
|
||||
}
|
||||
|
||||
// 获取剩余时间(秒)
|
||||
const getRemainingTime = (officerType: OfficerType): number => {
|
||||
const officer = gameStore.player.officers[officerType]
|
||||
if (!officer.expiresAt) return 0
|
||||
const now = Date.now()
|
||||
return Math.max(0, Math.floor((officer.expiresAt - now) / 1000))
|
||||
}
|
||||
|
||||
// 检查是否可以招募
|
||||
const canHire = (officerType: OfficerType): boolean => {
|
||||
if (!planet.value) return false
|
||||
|
||||
const config = OFFICERS.value[officerType]
|
||||
return (
|
||||
planet.value.resources.metal >= config.cost.metal &&
|
||||
planet.value.resources.crystal >= config.cost.crystal &&
|
||||
planet.value.resources.deuterium >= config.cost.deuterium &&
|
||||
planet.value.resources.darkMatter >= config.cost.darkMatter
|
||||
)
|
||||
}
|
||||
|
||||
const hireOfficer = (officerType: OfficerType, duration: number = 7): boolean => {
|
||||
if (!gameStore.currentPlanet) return false
|
||||
const cost = officerLogic.getOfficerCost(officerType)
|
||||
if (!resourceLogic.checkResourcesAvailable(gameStore.currentPlanet.resources, cost)) {
|
||||
return false
|
||||
}
|
||||
resourceLogic.deductResources(gameStore.currentPlanet.resources, cost)
|
||||
gameStore.player.officers[officerType] = officerLogic.createActiveOfficer(officerType, duration)
|
||||
return true
|
||||
}
|
||||
|
||||
// 招募军官
|
||||
const handleHire = (officerType: OfficerType) => {
|
||||
confirmDialog.value?.show({
|
||||
title: t('officersView.hireTitle'),
|
||||
message: t('officersView.hireMessage').replace('{name}', OFFICERS.value[officerType].name),
|
||||
onConfirm: () => {
|
||||
const success = hireOfficer(officerType, 7)
|
||||
if (!success) {
|
||||
alertDialog.value?.show({
|
||||
title: t('officersView.hireFailed'),
|
||||
message: t('officersView.insufficientResources')
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const renewOfficer = (officerType: OfficerType, duration: number = 7): boolean => {
|
||||
if (!gameStore.currentPlanet) return false
|
||||
const cost = officerLogic.getOfficerCost(officerType)
|
||||
if (!resourceLogic.checkResourcesAvailable(gameStore.currentPlanet.resources, cost)) {
|
||||
return false
|
||||
}
|
||||
resourceLogic.deductResources(gameStore.currentPlanet.resources, cost)
|
||||
const now = Date.now()
|
||||
gameStore.player.officers[officerType] = officerLogic.renewOfficerExpiration(gameStore.player.officers[officerType], duration, now)
|
||||
return true
|
||||
}
|
||||
|
||||
// 续约军官
|
||||
const handleRenew = (officerType: OfficerType) => {
|
||||
confirmDialog.value?.show({
|
||||
title: t('officersView.renewTitle'),
|
||||
message: t('officersView.renewMessage').replace('{name}', OFFICERS.value[officerType].name),
|
||||
onConfirm: () => {
|
||||
const success = renewOfficer(officerType, 7)
|
||||
if (!success) {
|
||||
alertDialog.value?.show({
|
||||
title: t('officersView.renewFailed'),
|
||||
message: t('officersView.insufficientResources')
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 解雇军官
|
||||
const handleDismiss = (officerType: OfficerType): void => {
|
||||
confirmDialog.value?.show({
|
||||
title: t('officersView.dismissTitle'),
|
||||
message: t('officersView.dismissMessage').replace('{name}', OFFICERS.value[officerType].name),
|
||||
onConfirm: () => {
|
||||
gameStore.player.officers[officerType] = officerLogic.createInactiveOfficer(officerType)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
166
src/views/OverviewView.vue
Normal file
166
src/views/OverviewView.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div v-if="planet" class="container mx-auto p-4 sm:p-6 space-y-4 sm:space-y-6">
|
||||
<!-- 星球信息 -->
|
||||
<div class="text-center">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold mb-1 sm:mb-2 flex items-center justify-center gap-2">
|
||||
{{ planet.name }}
|
||||
<Badge v-if="planet.isMoon" variant="secondary">{{ t('planet.moon') }}</Badge>
|
||||
</h1>
|
||||
<p class="text-xs sm:text-sm text-muted-foreground">
|
||||
{{ t('planet.position') }}: [{{ planet.position.galaxy }}:{{ planet.position.system }}:{{ planet.position.position }}]
|
||||
</p>
|
||||
<!-- 月球信息 -->
|
||||
<div v-if="!planet.isMoon && moon" class="mt-2">
|
||||
<Button @click="switchToMoon" variant="outline" size="sm">
|
||||
<span class="mr-2">🌙</span>
|
||||
{{ t('planet.switchToMoon') }}
|
||||
</Button>
|
||||
</div>
|
||||
<div v-if="planet.isMoon" class="mt-2">
|
||||
<Button @click="switchToParentPlanet" variant="outline" size="sm">{{ t('planet.backToPlanet') }}</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 资源显示 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('overview.resourceOverview') }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{{ t('common.resourceType') }}</TableHead>
|
||||
<TableHead class="text-right">{{ t('resources.current') }}</TableHead>
|
||||
<TableHead class="text-right">{{ t('resources.max') }}</TableHead>
|
||||
<TableHead class="text-right">{{ t('resources.production') }}{{ t('resources.perHour') }}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-for="resourceType in resourceTypes" :key="resourceType.key">
|
||||
<TableCell class="font-medium">
|
||||
<div class="flex items-center gap-2">
|
||||
<ResourceIcon :type="resourceType.key" size="sm" />
|
||||
{{ t(`resources.${resourceType.key}`) }}
|
||||
</div>
|
||||
</TableCell>
|
||||
<!-- 电量特殊显示 -->
|
||||
<template v-if="resourceType.key === 'energy'">
|
||||
<TableCell
|
||||
class="text-right"
|
||||
:class="planet.resources[resourceType.key] >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'"
|
||||
>
|
||||
{{ formatNumber(planet.resources[resourceType.key]) }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right text-muted-foreground">-</TableCell>
|
||||
<TableCell class="text-right text-muted-foreground">
|
||||
{{ formatNumber(energyProduction) }} / {{ formatNumber(energyConsumption) }}
|
||||
</TableCell>
|
||||
</template>
|
||||
<!-- 其他资源正常显示 -->
|
||||
<template v-else>
|
||||
<TableCell
|
||||
class="text-right"
|
||||
:class="getResourceColor(planet.resources[resourceType.key], capacity?.[resourceType.key] || Infinity)"
|
||||
>
|
||||
{{ formatNumber(planet.resources[resourceType.key]) }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right text-muted-foreground">
|
||||
{{ formatNumber(capacity?.[resourceType.key] || 0) }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right text-muted-foreground">
|
||||
{{ formatNumber(production?.[resourceType.key] || 0) }}
|
||||
</TableCell>
|
||||
</template>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 舰队信息 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('overview.fleetInfo') }}</CardTitle>
|
||||
<CardDescription>{{ t('overview.currentShips') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3 sm:gap-4">
|
||||
<div v-for="(count, shipType) in planet.fleet" :key="shipType">
|
||||
<p class="text-xs sm:text-sm text-muted-foreground">{{ SHIPS[shipType].name }}</p>
|
||||
<p class="text-lg sm:text-xl font-bold">{{ count }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { useGameConfig } from '@/composables/useGameConfig'
|
||||
import { computed } from 'vue'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import { formatNumber, getResourceColor } from '@/utils/format'
|
||||
import type { Planet } from '@/types/game'
|
||||
import * as publicLogic from '@/logic/publicLogic'
|
||||
import * as officerLogic from '@/logic/officerLogic'
|
||||
import * as resourceLogic from '@/logic/resourceLogic'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const { t } = useI18n()
|
||||
const { SHIPS } = useGameConfig()
|
||||
const planet = computed(() => gameStore.currentPlanet)
|
||||
const production = computed(() => (planet.value ? publicLogic.getResourceProduction(planet.value, gameStore.player.officers) : null))
|
||||
const capacity = computed(() => (planet.value ? publicLogic.getResourceCapacity(planet.value, gameStore.player.officers) : null))
|
||||
|
||||
// 电量产出和消耗
|
||||
const energyProduction = computed(() => {
|
||||
if (!planet.value) return 0
|
||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
|
||||
return resourceLogic.calculateEnergyProduction(planet.value, { energyProductionBonus: bonuses.energyProductionBonus })
|
||||
})
|
||||
|
||||
const energyConsumption = computed(() => {
|
||||
if (!planet.value) return 0
|
||||
return resourceLogic.calculateEnergyConsumption(planet.value)
|
||||
})
|
||||
|
||||
// 资源类型配置
|
||||
const resourceTypes = [
|
||||
{ key: 'metal' as const },
|
||||
{ key: 'crystal' as const },
|
||||
{ key: 'deuterium' as const },
|
||||
{ key: 'darkMatter' as const },
|
||||
{ key: 'energy' as const }
|
||||
]
|
||||
|
||||
// 月球相关
|
||||
const moon = computed(() => {
|
||||
if (!planet.value || planet.value.isMoon) return null
|
||||
return getMoonForPlanet(planet.value.id)
|
||||
})
|
||||
|
||||
const getMoonForPlanet = (planetId: string): Planet | null => {
|
||||
return gameStore.player.planets.find(p => p.isMoon && p.parentPlanetId === planetId) || null
|
||||
}
|
||||
|
||||
// 切换到月球
|
||||
const switchToMoon = () => {
|
||||
if (moon.value) {
|
||||
gameStore.currentPlanetId = moon.value.id
|
||||
}
|
||||
}
|
||||
|
||||
// 切换回母星
|
||||
const switchToParentPlanet = () => {
|
||||
if (planet.value?.parentPlanetId) {
|
||||
gameStore.currentPlanetId = planet.value.parentPlanetId
|
||||
}
|
||||
}
|
||||
</script>
|
||||
161
src/views/ResearchView.vue
Normal file
161
src/views/ResearchView.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<div v-if="planet" class="container mx-auto p-4 sm:p-6">
|
||||
<!-- 未解锁遮罩 -->
|
||||
<UnlockRequirement :required-building="BuildingType.ResearchLab" :required-level="1" />
|
||||
|
||||
<h1 class="text-2xl sm:text-3xl font-bold mb-4 sm:mb-6">{{ t('researchView.title') }}</h1>
|
||||
|
||||
<div class="grid grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4">
|
||||
<Card v-for="techType in Object.values(TechnologyType)" :key="techType" class="relative">
|
||||
<CardUnlockOverlay :requirements="TECHNOLOGIES[techType].requirements" />
|
||||
<CardHeader>
|
||||
<div class="flex justify-between items-start gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<CardTitle
|
||||
class="text-base sm:text-lg cursor-pointer hover:text-primary transition-colors"
|
||||
@click="detailDialog.openTechnology(techType, getTechLevel(techType))"
|
||||
>
|
||||
{{ TECHNOLOGIES[techType].name }}
|
||||
</CardTitle>
|
||||
<CardDescription class="text-xs sm:text-sm">{{ TECHNOLOGIES[techType].description }}</CardDescription>
|
||||
</div>
|
||||
<Badge variant="secondary" class="text-xs whitespace-nowrap flex-shrink-0">Lv {{ getTechLevel(techType) }}</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="space-y-2.5 sm:space-y-3">
|
||||
<div class="text-xs sm:text-sm space-y-1.5 sm:space-y-2">
|
||||
<p class="text-muted-foreground mb-1 sm:mb-2">{{ t('researchView.researchCost') }}:</p>
|
||||
<div class="space-y-1 sm:space-y-1.5">
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.metal') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.metal, getTechnologyCost(techType, getTechLevel(techType) + 1).metal)"
|
||||
>
|
||||
{{ formatNumber(getTechnologyCost(techType, getTechLevel(techType) + 1).metal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.crystal') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.crystal, getTechnologyCost(techType, getTechLevel(techType) + 1).crystal)"
|
||||
>
|
||||
{{ formatNumber(getTechnologyCost(techType, getTechLevel(techType) + 1).crystal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.deuterium') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="
|
||||
getResourceCostColor(planet.resources.deuterium, getTechnologyCost(techType, getTechLevel(techType) + 1).deuterium)
|
||||
"
|
||||
>
|
||||
{{ formatNumber(getTechnologyCost(techType, getTechLevel(techType) + 1).deuterium) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button @click="handleResearch(techType)" :disabled="!canResearch(techType)" class="w-full">
|
||||
{{ t('researchView.research') }}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- 提示对话框 -->
|
||||
<AlertDialog ref="alertDialog" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useDetailDialogStore } from '@/stores/detailDialogStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { useGameConfig } from '@/composables/useGameConfig'
|
||||
import { computed, ref } from 'vue'
|
||||
import { TechnologyType, BuildingType } from '@/types/game'
|
||||
import type { Resources } from '@/types/game'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import AlertDialog from '@/components/AlertDialog.vue'
|
||||
import UnlockRequirement from '@/components/UnlockRequirement.vue'
|
||||
import CardUnlockOverlay from '@/components/CardUnlockOverlay.vue'
|
||||
import { formatNumber, getResourceCostColor } from '@/utils/format'
|
||||
import * as publicLogic from '@/logic/publicLogic'
|
||||
import * as researchLogic from '@/logic/researchLogic'
|
||||
import * as researchValidation from '@/logic/researchValidation'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const detailDialog = useDetailDialogStore()
|
||||
const { t } = useI18n()
|
||||
const { TECHNOLOGIES } = useGameConfig()
|
||||
const planet = computed(() => gameStore.currentPlanet)
|
||||
const player = computed(() => gameStore.player)
|
||||
const alertDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
|
||||
|
||||
const researchTechnology = (techType: TechnologyType): boolean => {
|
||||
if (!gameStore.currentPlanet) return false
|
||||
const validation = researchValidation.validateTechnologyResearch(
|
||||
gameStore.currentPlanet,
|
||||
techType,
|
||||
gameStore.player.technologies,
|
||||
gameStore.player.researchQueue
|
||||
)
|
||||
if (!validation.valid) return false
|
||||
const currentLevel = gameStore.player.technologies[techType] || 0
|
||||
const { queueItem } = researchValidation.executeTechnologyResearch(
|
||||
gameStore.currentPlanet,
|
||||
techType,
|
||||
currentLevel,
|
||||
gameStore.player.officers
|
||||
)
|
||||
gameStore.player.researchQueue.push(queueItem)
|
||||
return true
|
||||
}
|
||||
|
||||
// 研究科技
|
||||
const handleResearch = (techType: TechnologyType) => {
|
||||
const success = researchTechnology(techType)
|
||||
if (!success) {
|
||||
alertDialog.value?.show({
|
||||
title: t('researchView.researchFailed'),
|
||||
message: t('researchView.researchFailedMessage')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 获取科技等级
|
||||
const getTechLevel = (techType: TechnologyType): number => {
|
||||
return player.value.technologies[techType] || 0
|
||||
}
|
||||
|
||||
// 检查是否可以研究
|
||||
const canResearch = (techType: TechnologyType): boolean => {
|
||||
if (!planet.value || player.value.researchQueue.length > 0) return false
|
||||
|
||||
const config = TECHNOLOGIES.value[techType]
|
||||
const currentLevel = getTechLevel(techType)
|
||||
const cost = getTechnologyCost(techType, currentLevel + 1)
|
||||
|
||||
return (
|
||||
publicLogic.checkRequirements(planet.value, gameStore.player.technologies, config.requirements) &&
|
||||
planet.value.resources.metal >= cost.metal &&
|
||||
planet.value.resources.crystal >= cost.crystal &&
|
||||
planet.value.resources.deuterium >= cost.deuterium
|
||||
)
|
||||
}
|
||||
|
||||
const getTechnologyCost = (techType: TechnologyType, targetLevel: number): Resources => {
|
||||
return researchLogic.calculateTechnologyCost(techType, targetLevel)
|
||||
}
|
||||
</script>
|
||||
281
src/views/SettingsView.vue
Normal file
281
src/views/SettingsView.vue
Normal file
@@ -0,0 +1,281 @@
|
||||
<template>
|
||||
<div class="container mx-auto p-4 sm:p-6 space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-2xl font-bold">{{ t('nav.settings') }}</h1>
|
||||
</div>
|
||||
|
||||
<!-- 数据管理 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('settings.dataManagement') }}</CardTitle>
|
||||
<CardDescription>{{ t('settings.dataManagementDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<!-- 导出数据 -->
|
||||
<div class="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div class="space-y-1">
|
||||
<h3 class="font-medium">{{ t('settings.exportData') }}</h3>
|
||||
<p class="text-sm text-muted-foreground">{{ t('settings.exportDataDesc') }}</p>
|
||||
</div>
|
||||
<Button @click="handleExport" :disabled="isExporting">
|
||||
<Download class="mr-2 h-4 w-4" />
|
||||
{{ isExporting ? t('settings.exporting') : t('settings.export') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 导入数据 -->
|
||||
<div class="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div class="space-y-1">
|
||||
<h3 class="font-medium">{{ t('settings.importData') }}</h3>
|
||||
<p class="text-sm text-muted-foreground">{{ t('settings.importDataDesc') }}</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<input ref="fileInputRef" type="file" accept=".json" class="hidden" @change="handleFileSelect" />
|
||||
<Button @click="triggerFileInput" variant="outline">
|
||||
<Upload class="mr-2 h-4 w-4" />
|
||||
{{ t('settings.selectFile') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 清除数据 -->
|
||||
<div class="flex items-center justify-between p-4 border rounded-lg border-destructive/50">
|
||||
<div class="space-y-1">
|
||||
<h3 class="font-medium text-destructive">{{ t('settings.clearData') }}</h3>
|
||||
<p class="text-sm text-muted-foreground">{{ t('settings.clearDataDesc') }}</p>
|
||||
</div>
|
||||
<Button @click="handleClearData" variant="destructive">
|
||||
<Trash2 class="mr-2 h-4 w-4" />
|
||||
{{ t('settings.clear') }}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 游戏设置 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('settings.gameSettings') }}</CardTitle>
|
||||
<CardDescription>{{ t('settings.gameSettingsDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<!-- 玩家名称 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<Label for="player-name">{{ t('settings.playerName') }}</Label>
|
||||
<Input id="player-name" v-model="playerName" @blur="updatePlayerName" class="max-w-xs" />
|
||||
</div>
|
||||
|
||||
<!-- 游戏速度 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<Label>{{ t('settings.gameSpeed') }}</Label>
|
||||
<p class="text-sm text-muted-foreground">{{ t('settings.gameSpeedDesc') }}</p>
|
||||
</div>
|
||||
<div class="text-2xl font-bold">1x</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 关于 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('settings.about') }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('settings.version') }}:</span>
|
||||
<span class="font-medium">{{ pkg.version }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('settings.buildDate') }}:</span>
|
||||
<span class="font-medium">{{ new Date().toLocaleDateString() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 社区链接 -->
|
||||
<div class="pt-2 border-t space-y-2">
|
||||
<h3 class="text-sm font-medium">{{ t('settings.community') }}</h3>
|
||||
<div class="flex flex-col gap-2">
|
||||
<!-- GitHub -->
|
||||
<Button variant="outline" class="w-full justify-start" @click="openGithub">
|
||||
<ExternalLink class="mr-2 h-4 w-4" />
|
||||
{{ t('settings.github') }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="gameStore.locale === 'zh-CN' || gameStore.locale === 'zh-TW'"
|
||||
variant="outline"
|
||||
class="w-full justify-start"
|
||||
@click="openQQGroup"
|
||||
>
|
||||
<MessagesSquare class="mr-2 h-4 w-4" />
|
||||
{{ t('settings.qqGroup') }}
|
||||
<span class="ml-auto text-xs text-muted-foreground">{{ pkg.qq }}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 确认对话框 -->
|
||||
<AlertDialog :open="showConfirmDialog" @update:open="showConfirmDialog = $event">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ confirmTitle }}</AlertDialogTitle>
|
||||
<AlertDialogDescription>{{ confirmMessage }}</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel @click="cancelAction">{{ t('common.cancel') }}</AlertDialogCancel>
|
||||
<AlertDialogAction @click="confirmAction">{{ t('common.confirm') }}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Download, Upload, Trash2, ExternalLink, MessagesSquare } from 'lucide-vue-next'
|
||||
import { saveAs } from 'file-saver'
|
||||
import { toast } from 'vue-sonner'
|
||||
import pkg from '../../package.json'
|
||||
|
||||
const { t } = useI18n()
|
||||
const gameStore = useGameStore()
|
||||
|
||||
const fileInputRef = ref<HTMLInputElement>()
|
||||
const isExporting = ref(false)
|
||||
const playerName = ref(gameStore.player.name)
|
||||
|
||||
const showConfirmDialog = ref(false)
|
||||
const confirmTitle = ref('')
|
||||
const confirmMessage = ref('')
|
||||
let confirmCallback: (() => void) | null = null
|
||||
|
||||
const openGithub = () => {
|
||||
window.open(`https://github.com/${pkg.author}/${pkg.name}`, '_blank')
|
||||
}
|
||||
|
||||
const openQQGroup = () => {
|
||||
window.open(`https://qm.qq.com/q/${pkg.id}`, '_blank')
|
||||
}
|
||||
|
||||
// 导出数据
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
isExporting.value = true
|
||||
const data = localStorage.getItem(pkg.name)
|
||||
if (!data) {
|
||||
toast.error(t('settings.exportFailed'))
|
||||
return
|
||||
}
|
||||
const fileName = `${pkg.name}-${new Date().toISOString().slice(0, 10)}-${Date.now()}.json`
|
||||
saveAs(new Blob([data], { type: 'application/json' }), fileName)
|
||||
toast.success(t('settings.exportSuccess'))
|
||||
} catch (error) {
|
||||
console.error('Export failed:', error)
|
||||
toast.error(t('settings.exportFailed'))
|
||||
} finally {
|
||||
isExporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 触发文件选择
|
||||
const triggerFileInput = () => {
|
||||
fileInputRef.value?.click()
|
||||
}
|
||||
|
||||
// 处理文件选择
|
||||
const handleFileSelect = (event: Event) => {
|
||||
const file = (event.target as HTMLInputElement).files?.[0]
|
||||
if (!file) return
|
||||
|
||||
confirmTitle.value = t('settings.importConfirmTitle')
|
||||
confirmMessage.value = t('settings.importConfirmMessage')
|
||||
showConfirmDialog.value = true
|
||||
confirmCallback = () => importData(file)
|
||||
}
|
||||
|
||||
// 导入数据
|
||||
const importData = async (file: File) => {
|
||||
try {
|
||||
const reader = new FileReader()
|
||||
reader.onload = e => {
|
||||
try {
|
||||
const result = e.target?.result
|
||||
if (typeof result === 'string') {
|
||||
localStorage.setItem(pkg.name, result)
|
||||
toast.success(t('settings.importSuccess'))
|
||||
setTimeout(() => location.reload(), 500)
|
||||
} else {
|
||||
toast.error(t('settings.importFailed'))
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
toast.error(t('settings.importFailed') + ': ' + message)
|
||||
}
|
||||
}
|
||||
reader.readAsText(file)
|
||||
} catch (error) {
|
||||
console.error('Import failed:', error)
|
||||
toast.error(t('settings.importFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
// 清除数据
|
||||
const handleClearData = () => {
|
||||
confirmTitle.value = t('settings.clearConfirmTitle')
|
||||
confirmMessage.value = t('settings.clearConfirmMessage')
|
||||
showConfirmDialog.value = true
|
||||
confirmCallback = clearData
|
||||
}
|
||||
|
||||
const clearData = () => {
|
||||
// 清除localStorage
|
||||
localStorage.clear()
|
||||
// 重新加载页面
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
// 更新玩家名称
|
||||
const updatePlayerName = () => {
|
||||
if (playerName.value.trim()) {
|
||||
gameStore.player.name = playerName.value.trim()
|
||||
}
|
||||
}
|
||||
|
||||
// 确认操作
|
||||
const confirmAction = () => {
|
||||
if (confirmCallback) {
|
||||
confirmCallback()
|
||||
confirmCallback = null
|
||||
}
|
||||
showConfirmDialog.value = false
|
||||
}
|
||||
|
||||
// 取消操作
|
||||
const cancelAction = () => {
|
||||
confirmCallback = null
|
||||
showConfirmDialog.value = false
|
||||
// 重置文件输入
|
||||
if (fileInputRef.value) {
|
||||
fileInputRef.value.value = ''
|
||||
}
|
||||
}
|
||||
</script>
|
||||
239
src/views/ShipyardView.vue
Normal file
239
src/views/ShipyardView.vue
Normal file
@@ -0,0 +1,239 @@
|
||||
<template>
|
||||
<div v-if="planet" class="container mx-auto p-4 sm:p-6">
|
||||
<!-- 未解锁遮罩 -->
|
||||
<UnlockRequirement :required-building="BuildingType.Shipyard" :required-level="1" />
|
||||
|
||||
<h1 class="text-2xl sm:text-3xl font-bold mb-4 sm:mb-6">{{ t('shipyardView.title') }}</h1>
|
||||
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3 sm:gap-4">
|
||||
<Card v-for="shipType in Object.values(ShipType)" :key="shipType" class="relative">
|
||||
<CardUnlockOverlay :requirements="SHIPS[shipType].requirements" />
|
||||
<CardHeader>
|
||||
<CardTitle
|
||||
class="text-base sm:text-lg cursor-pointer hover:text-primary transition-colors"
|
||||
@click="detailDialog.openShip(shipType)"
|
||||
>
|
||||
{{ SHIPS[shipType].name }}
|
||||
</CardTitle>
|
||||
<CardDescription class="text-xs sm:text-sm">{{ SHIPS[shipType].description }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="space-y-3 sm:space-y-4">
|
||||
<div class="grid grid-cols-2 gap-2 text-xs sm:text-sm">
|
||||
<div>
|
||||
<p class="text-muted-foreground">{{ t('shipyardView.attack') }}</p>
|
||||
<p class="font-medium">{{ SHIPS[shipType].attack }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-muted-foreground">{{ t('shipyardView.shield') }}</p>
|
||||
<p class="font-medium">{{ SHIPS[shipType].shield }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-muted-foreground">{{ t('shipyardView.speed') }}</p>
|
||||
<p class="font-medium">{{ SHIPS[shipType].speed }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-muted-foreground">{{ t('shipyardView.cargoCapacity') }}</p>
|
||||
<p class="font-medium">{{ formatNumber(SHIPS[shipType].cargoCapacity) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-xs sm:text-sm space-y-1.5 sm:space-y-2">
|
||||
<p class="text-muted-foreground mb-1 sm:mb-2">{{ t('shipyardView.unitCost') }}:</p>
|
||||
<div class="space-y-1 sm:space-y-1.5">
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.metal') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.metal, SHIPS[shipType].cost.metal)"
|
||||
>
|
||||
{{ formatNumber(SHIPS[shipType].cost.metal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.crystal') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.crystal, SHIPS[shipType].cost.crystal)"
|
||||
>
|
||||
{{ formatNumber(SHIPS[shipType].cost.crystal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.deuterium') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.deuterium, SHIPS[shipType].cost.deuterium)"
|
||||
>
|
||||
{{ formatNumber(SHIPS[shipType].cost.deuterium) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label :for="`quantity-${shipType}`" class="text-xs sm:text-sm">{{ t('shipyardView.buildQuantity') }}</Label>
|
||||
<Input
|
||||
:id="`quantity-${shipType}`"
|
||||
v-model.number="quantities[shipType]"
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder="0"
|
||||
class="text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="quantities[shipType] > 0" class="text-xs sm:text-sm space-y-1.5 sm:space-y-2 p-2.5 sm:p-3 bg-muted rounded-lg">
|
||||
<p class="font-medium text-muted-foreground">{{ t('shipyardView.totalCost') }}:</p>
|
||||
<div class="space-y-1 sm:space-y-1.5">
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.metal') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.metal, getTotalCost(shipType).metal)"
|
||||
>
|
||||
{{ formatNumber(getTotalCost(shipType).metal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.crystal') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.crystal, getTotalCost(shipType).crystal)"
|
||||
>
|
||||
{{ formatNumber(getTotalCost(shipType).crystal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.deuterium') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.deuterium, getTotalCost(shipType).deuterium)"
|
||||
>
|
||||
{{ formatNumber(getTotalCost(shipType).deuterium) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button @click="handleBuild(shipType)" :disabled="!canBuild(shipType)" class="w-full">{{ t('shipyardView.build') }}</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- 提示对话框 -->
|
||||
<AlertDialog ref="alertDialog" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useDetailDialogStore } from '@/stores/detailDialogStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { useGameConfig } from '@/composables/useGameConfig'
|
||||
import { computed, ref } from 'vue'
|
||||
import { ShipType, BuildingType } from '@/types/game'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import AlertDialog from '@/components/AlertDialog.vue'
|
||||
import UnlockRequirement from '@/components/UnlockRequirement.vue'
|
||||
import CardUnlockOverlay from '@/components/CardUnlockOverlay.vue'
|
||||
import { formatNumber, getResourceCostColor } from '@/utils/format'
|
||||
import * as shipValidation from '@/logic/shipValidation'
|
||||
import * as publicLogic from '@/logic/publicLogic'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const detailDialog = useDetailDialogStore()
|
||||
const { t } = useI18n()
|
||||
const { SHIPS } = useGameConfig()
|
||||
const planet = computed(() => gameStore.currentPlanet)
|
||||
const alertDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
|
||||
|
||||
// 每种舰船的建造数量
|
||||
const quantities = ref<Record<ShipType, number>>({
|
||||
[ShipType.LightFighter]: 0,
|
||||
[ShipType.HeavyFighter]: 0,
|
||||
[ShipType.Cruiser]: 0,
|
||||
[ShipType.Battleship]: 0,
|
||||
[ShipType.SmallCargo]: 0,
|
||||
[ShipType.LargeCargo]: 0,
|
||||
[ShipType.ColonyShip]: 0,
|
||||
[ShipType.Recycler]: 0,
|
||||
[ShipType.EspionageProbe]: 0,
|
||||
[ShipType.DarkMatterHarvester]: 0
|
||||
})
|
||||
|
||||
const buildShip = (shipType: ShipType, quantity: number): boolean => {
|
||||
if (!gameStore.currentPlanet) return false
|
||||
const validation = shipValidation.validateShipBuild(gameStore.currentPlanet, shipType, quantity, gameStore.player.technologies)
|
||||
if (!validation.valid) return false
|
||||
const queueItem = shipValidation.executeShipBuild(gameStore.currentPlanet, shipType, quantity, gameStore.player.officers)
|
||||
gameStore.currentPlanet.buildQueue.push(queueItem)
|
||||
return true
|
||||
}
|
||||
|
||||
// 建造舰船
|
||||
const handleBuild = (shipType: ShipType) => {
|
||||
const quantity = quantities.value[shipType]
|
||||
if (quantity <= 0) {
|
||||
alertDialog.value?.show({
|
||||
title: t('shipyardView.inputError'),
|
||||
message: t('shipyardView.inputErrorMessage')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const success = buildShip(shipType, quantity)
|
||||
if (!success) {
|
||||
alertDialog.value?.show({
|
||||
title: t('shipyardView.buildFailed'),
|
||||
message: t('shipyardView.buildFailedMessage')
|
||||
})
|
||||
} else {
|
||||
quantities.value[shipType] = 0
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否可以建造
|
||||
const canBuild = (shipType: ShipType): boolean => {
|
||||
if (!planet.value) return false
|
||||
|
||||
const quantity = quantities.value[shipType]
|
||||
if (quantity <= 0) return false
|
||||
|
||||
const config = SHIPS.value[shipType]
|
||||
const totalCost = {
|
||||
metal: config.cost.metal * quantity,
|
||||
crystal: config.cost.crystal * quantity,
|
||||
deuterium: config.cost.deuterium * quantity
|
||||
}
|
||||
|
||||
return (
|
||||
publicLogic.checkRequirements(planet.value, gameStore.player.technologies, config.requirements) &&
|
||||
planet.value.resources.metal >= totalCost.metal &&
|
||||
planet.value.resources.crystal >= totalCost.crystal &&
|
||||
planet.value.resources.deuterium >= totalCost.deuterium
|
||||
)
|
||||
}
|
||||
|
||||
// 计算总成本
|
||||
const getTotalCost = (shipType: ShipType) => {
|
||||
const quantity = quantities.value[shipType]
|
||||
const config = SHIPS.value[shipType]
|
||||
return {
|
||||
metal: config.cost.metal * quantity,
|
||||
crystal: config.cost.crystal * quantity,
|
||||
deuterium: config.cost.deuterium * quantity
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user