mirror of
https://github.com/setube/ogame-vue-ts.git
synced 2026-05-12 07:55:11 +08:00
feat: 新增战报弹窗与舰队模拟器,重构UI组件
新增 BattleReportDialog、SpyReportDialog、NumberWithTooltip 等组件,完善舰队模拟器功能。重构并引入 Sheet、Sidebar、Tooltip、Skeleton 等 UI 组件,优化界面结构。实现 battle.worker 支持战斗计算,增加 universeStore、fleetStorageLogic 等核心逻辑,完善多语言与类型定义。
This commit is contained in:
@@ -140,242 +140,24 @@
|
||||
</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>
|
||||
<BattleReportDialog v-model:open="showResultDialog" :report="simulationResult" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, toRaw } 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 type { Fleet, BattleResult } from '@/types/game'
|
||||
import { workerManager } from '@/workers/workerManager'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import { Sword, Shield, Zap, RotateCcw, Trophy } from 'lucide-vue-next'
|
||||
import BattleReportDialog from '@/components/BattleReportDialog.vue'
|
||||
import { Sword, Shield, Zap, RotateCcw } from 'lucide-vue-next'
|
||||
import * as planetLogic from '@/logic/planetLogic'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -444,78 +226,45 @@
|
||||
|
||||
// 模拟结果
|
||||
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 = () => {
|
||||
// 运行模拟(使用 Web Worker 进行计算)
|
||||
const runSimulation = async () => {
|
||||
// 使用 toRaw 将 Vue 响应式对象转换为普通对象,以便传递给 Worker
|
||||
const attackerSide = {
|
||||
ships: attackerFleet.value,
|
||||
ships: toRaw(attackerFleet.value),
|
||||
weaponTech: attackerTech.value.weapon,
|
||||
shieldTech: attackerTech.value.shield,
|
||||
armorTech: attackerTech.value.armor
|
||||
}
|
||||
|
||||
const defenderSide = {
|
||||
ships: defenderFleet.value,
|
||||
defense: defenderDefense.value,
|
||||
ships: toRaw(defenderFleet.value),
|
||||
defense: toRaw(defenderDefense.value),
|
||||
weaponTech: defenderTech.value.weapon,
|
||||
shieldTech: defenderTech.value.shield,
|
||||
armorTech: defenderTech.value.armor
|
||||
}
|
||||
|
||||
const result = simulateBattle(attackerSide, defenderSide)
|
||||
// 使用 Worker 执行战斗模拟
|
||||
const result = await workerManager.simulateBattle({
|
||||
attacker: attackerSide,
|
||||
defender: defenderSide
|
||||
})
|
||||
|
||||
// 保存回合数和剩余单位
|
||||
battleRounds.value = result.rounds
|
||||
attackerRemaining.value = result.attackerRemaining
|
||||
defenderRemaining.value = result.defenderRemaining
|
||||
roundDetails.value = result.roundDetails
|
||||
showRoundDetails.value = false
|
||||
// 计算掠夺和残骸场
|
||||
const plunder =
|
||||
result.winner === 'attacker'
|
||||
? await workerManager.calculatePlunder({
|
||||
defenderResources: toRaw(defenderResources.value),
|
||||
attackerFleet: result.attackerRemaining
|
||||
})
|
||||
: { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 }
|
||||
const debrisField = await workerManager.calculateDebris({
|
||||
attackerLosses: result.attackerLosses,
|
||||
defenderLosses: result.defenderLosses
|
||||
})
|
||||
const moonChance = planetLogic.calculateMoonChance(debrisField) / 100 // 转换为 0-1 范围
|
||||
|
||||
simulationResult.value = {
|
||||
id: `sim_${Date.now()}`,
|
||||
@@ -530,8 +279,13 @@
|
||||
attackerLosses: result.attackerLosses,
|
||||
defenderLosses: result.defenderLosses,
|
||||
winner: result.winner,
|
||||
plunder: plunder.value,
|
||||
debrisField: debrisField.value
|
||||
plunder,
|
||||
debrisField,
|
||||
rounds: result.rounds,
|
||||
attackerRemaining: result.attackerRemaining,
|
||||
defenderRemaining: result.defenderRemaining,
|
||||
roundDetails: result.roundDetails,
|
||||
moonChance
|
||||
}
|
||||
|
||||
// 显示结果对话框
|
||||
@@ -552,18 +306,6 @@
|
||||
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>
|
||||
|
||||
@@ -11,7 +11,10 @@
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4">
|
||||
<Card v-for="buildingType in availableBuildings" :key="buildingType">
|
||||
<Card v-for="buildingType in availableBuildings" :key="buildingType" class="relative">
|
||||
<!-- 前置条件遮罩 -->
|
||||
<CardUnlockOverlay :requirements="BUILDINGS[buildingType].requirements" :currentLevel="getBuildingLevel(buildingType)" />
|
||||
|
||||
<CardHeader>
|
||||
<div class="flex justify-between items-start gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
@@ -89,7 +92,7 @@
|
||||
|
||||
<!-- 升级按钮 -->
|
||||
<Button @click="handleUpgrade(buildingType)" :disabled="!canUpgrade(buildingType)" class="w-full">
|
||||
{{ t('buildingsView.upgrade') }}
|
||||
{{ getUpgradeButtonText(buildingType) }}
|
||||
</Button>
|
||||
|
||||
<!-- 拆除按钮 -->
|
||||
@@ -128,22 +131,24 @@
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { useGameConfig } from '@/composables/useGameConfig'
|
||||
import { computed, ref } from 'vue'
|
||||
import { BuildingType } from '@/types/game'
|
||||
import { BuildingType, TechnologyType } 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 CardUnlockOverlay from '@/components/CardUnlockOverlay.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'
|
||||
import * as publicLogic from '@/logic/publicLogic'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const detailDialog = useDetailDialogStore()
|
||||
const { t } = useI18n()
|
||||
const { BUILDINGS } = useGameConfig()
|
||||
const { BUILDINGS, TECHNOLOGIES } = useGameConfig()
|
||||
const planet = computed(() => gameStore.currentPlanet)
|
||||
const alertDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
|
||||
|
||||
@@ -182,6 +187,15 @@
|
||||
|
||||
// 升级建筑
|
||||
const handleUpgrade = (buildingType: BuildingType) => {
|
||||
// 检查前置条件
|
||||
if (!checkUpgradeRequirements(buildingType)) {
|
||||
alertDialog.value?.show({
|
||||
title: t('common.requirementsNotMet'),
|
||||
message: getRequirementsList(buildingType)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const success = upgradeBuilding(buildingType)
|
||||
if (!success) {
|
||||
alertDialog.value?.show({
|
||||
@@ -196,12 +210,98 @@
|
||||
return planet.value?.buildings[buildingType] || 0
|
||||
}
|
||||
|
||||
// 检查升级前置条件是否满足
|
||||
const checkUpgradeRequirements = (buildingType: BuildingType): boolean => {
|
||||
if (!planet.value) return false
|
||||
const config = BUILDINGS.value[buildingType]
|
||||
const currentLevel = getBuildingLevel(buildingType)
|
||||
const targetLevel = currentLevel + 1
|
||||
|
||||
// 获取目标等级的所有前置条件(包括等级门槛)
|
||||
const requirements = publicLogic.getLevelRequirements(config, targetLevel)
|
||||
|
||||
if (!requirements || Object.keys(requirements).length === 0) return true
|
||||
return publicLogic.checkRequirements(planet.value, gameStore.player.technologies, requirements)
|
||||
}
|
||||
|
||||
// 获取升级按钮文本
|
||||
const getUpgradeButtonText = (buildingType: BuildingType): string => {
|
||||
if (!planet.value) return t('buildingsView.upgrade')
|
||||
|
||||
const config = BUILDINGS.value[buildingType]
|
||||
const currentLevel = getBuildingLevel(buildingType)
|
||||
|
||||
// 检查是否达到等级上限
|
||||
if (config.maxLevel !== undefined && currentLevel >= config.maxLevel) {
|
||||
return t('buildingsView.maxLevelReached') // "等级已满"
|
||||
}
|
||||
|
||||
if (planet.value.buildQueue.length > 0) return t('buildingsView.upgrade')
|
||||
|
||||
// 检查前置条件
|
||||
if (!checkUpgradeRequirements(buildingType)) {
|
||||
return t('buildingsView.requirementsNotMet')
|
||||
}
|
||||
|
||||
return t('buildingsView.upgrade')
|
||||
}
|
||||
|
||||
// 获取前置条件列表文本
|
||||
const getRequirementsList = (buildingType: BuildingType): string => {
|
||||
const config = BUILDINGS.value[buildingType]
|
||||
const currentLevel = getBuildingLevel(buildingType)
|
||||
const targetLevel = currentLevel + 1
|
||||
|
||||
// 获取目标等级的所有前置条件(包括等级门槛)
|
||||
const requirements = publicLogic.getLevelRequirements(config, targetLevel)
|
||||
|
||||
if (!requirements || !planet.value) return ''
|
||||
|
||||
const lines: string[] = []
|
||||
for (const [key, requiredLevel] of Object.entries(requirements)) {
|
||||
// 检查是否为建筑类型
|
||||
if (Object.values(BuildingType).includes(key as BuildingType)) {
|
||||
const bt = key as BuildingType
|
||||
const currentLevel = planet.value.buildings[bt] || 0
|
||||
const name = BUILDINGS.value[bt]?.name || bt
|
||||
const status = currentLevel >= requiredLevel ? '✓' : '✗'
|
||||
lines.push(`${status} ${name}: Lv ${requiredLevel} (${t('common.current')}: Lv ${currentLevel})`)
|
||||
}
|
||||
// 检查是否为科技类型
|
||||
else if (Object.values(TechnologyType).includes(key as TechnologyType)) {
|
||||
const tt = key as TechnologyType
|
||||
const currentLevel = gameStore.player.technologies[tt] || 0
|
||||
const name = TECHNOLOGIES.value[tt]?.name || tt
|
||||
const status = currentLevel >= requiredLevel ? '✓' : '✗'
|
||||
lines.push(`${status} ${name}: Lv ${requiredLevel} (${t('common.current')}: Lv ${currentLevel})`)
|
||||
}
|
||||
}
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
// 检查是否可以升级
|
||||
const canUpgrade = (buildingType: BuildingType): boolean => {
|
||||
if (!planet.value) return false
|
||||
|
||||
const config = BUILDINGS.value[buildingType]
|
||||
const currentLevel = getBuildingLevel(buildingType)
|
||||
|
||||
// 检查是否达到等级上限
|
||||
if (config.maxLevel !== undefined && currentLevel >= config.maxLevel) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (planet.value.buildQueue.length > 0) return false
|
||||
|
||||
const currentLevel = getBuildingLevel(buildingType)
|
||||
// 检查前置条件
|
||||
const validation = buildingValidation.validateBuildingUpgrade(
|
||||
planet.value,
|
||||
buildingType,
|
||||
gameStore.player.technologies,
|
||||
gameStore.player.officers
|
||||
)
|
||||
if (!validation.valid) return false
|
||||
|
||||
const cost = getBuildingCost(buildingType, currentLevel + 1)
|
||||
|
||||
return (
|
||||
|
||||
@@ -183,7 +183,8 @@
|
||||
[DefenseType.IonCannon]: 0,
|
||||
[DefenseType.PlasmaTurret]: 0,
|
||||
[DefenseType.SmallShieldDome]: 0,
|
||||
[DefenseType.LargeShieldDome]: 0
|
||||
[DefenseType.LargeShieldDome]: 0,
|
||||
[DefenseType.PlanetaryShield]: 0
|
||||
})
|
||||
|
||||
// 判断是否为护盾罩
|
||||
|
||||
@@ -296,9 +296,10 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useUniverseStore } from '@/stores/universeStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { useGameConfig } from '@/composables/useGameConfig'
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ShipType, MissionType, BuildingType } from '@/types/game'
|
||||
import type { Fleet, Resources } from '@/types/game'
|
||||
@@ -311,7 +312,7 @@
|
||||
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 { Sword, Package, Rocket as RocketIcon, Eye, Users, Recycle, Skull } from 'lucide-vue-next'
|
||||
import { formatNumber, formatTime } from '@/utils/format'
|
||||
import * as shipValidation from '@/logic/shipValidation'
|
||||
import * as fleetLogic from '@/logic/fleetLogic'
|
||||
@@ -322,11 +323,16 @@
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const gameStore = useGameStore()
|
||||
const universeStore = useUniverseStore()
|
||||
const { t } = useI18n()
|
||||
const { SHIPS } = useGameConfig()
|
||||
const planet = computed(() => gameStore.currentPlanet)
|
||||
const alertDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
|
||||
|
||||
// 当前时间(响应式)
|
||||
const currentTime = ref(Date.now())
|
||||
let timeInterval: number | null = null
|
||||
|
||||
// 计算最大舰队任务槽位
|
||||
const maxFleetMissions = computed(() => {
|
||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
|
||||
@@ -345,7 +351,9 @@
|
||||
[ShipType.LargeCargo]: 0,
|
||||
[ShipType.ColonyShip]: 0,
|
||||
[ShipType.Recycler]: 0,
|
||||
[ShipType.EspionageProbe]: 0
|
||||
[ShipType.EspionageProbe]: 0,
|
||||
[ShipType.DarkMatterHarvester]: 0,
|
||||
[ShipType.Deathstar]: 0
|
||||
})
|
||||
|
||||
// 目标坐标
|
||||
@@ -359,6 +367,11 @@
|
||||
|
||||
// 从 URL query 参数初始化
|
||||
onMounted(() => {
|
||||
// 启动定时器更新当前时间
|
||||
timeInterval = window.setInterval(() => {
|
||||
currentTime.value = Date.now()
|
||||
}, 1000) // 每秒更新一次
|
||||
|
||||
const { galaxy, system, position, mission } = route.query
|
||||
|
||||
// 如果有参数,填充数据
|
||||
@@ -385,13 +398,22 @@
|
||||
}
|
||||
})
|
||||
|
||||
// 清理定时器
|
||||
onUnmounted(() => {
|
||||
if (timeInterval) {
|
||||
clearInterval(timeInterval)
|
||||
}
|
||||
})
|
||||
|
||||
// 可用任务类型
|
||||
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 }
|
||||
{ type: MissionType.Deploy, name: t('fleetView.deploy'), icon: Users },
|
||||
{ type: MissionType.Recycle, name: t('fleetView.recycle'), icon: Recycle },
|
||||
{ type: MissionType.Destroy, name: t('fleetView.destroy'), icon: Skull }
|
||||
])
|
||||
|
||||
// 获取任务名称
|
||||
@@ -439,24 +461,53 @@
|
||||
}
|
||||
|
||||
// 检查是否可以派遣
|
||||
const canSendFleet = (): boolean => {
|
||||
const canSendFleet = (): { valid: boolean; errorKey?: string } => {
|
||||
// 检查是否选择了舰船
|
||||
const hasShips = Object.values(selectedFleet.value).some(count => count > 0)
|
||||
if (!hasShips) return false
|
||||
if (!hasShips) return { valid: false, errorKey: 'fleetView.noShipsSelected' }
|
||||
|
||||
// 检查是否派遣到自己的星球
|
||||
if (planet.value) {
|
||||
const isSamePlanet =
|
||||
targetPosition.value.galaxy === planet.value.position.galaxy &&
|
||||
targetPosition.value.system === planet.value.position.system &&
|
||||
targetPosition.value.position === planet.value.position.position
|
||||
if (isSamePlanet) {
|
||||
return { valid: false, errorKey: 'fleetView.cannotSendToOwnPlanet' }
|
||||
}
|
||||
}
|
||||
|
||||
// 检查载货量
|
||||
if (selectedMission.value === MissionType.Transport) {
|
||||
if (getTotalCargo() > getTotalCargoCapacity()) return false
|
||||
if (getTotalCargo() > getTotalCargoCapacity()) {
|
||||
return { valid: false, errorKey: 'fleetView.cargoExceedsCapacity' }
|
||||
}
|
||||
}
|
||||
|
||||
// 检查殖民船
|
||||
if (selectedMission.value === MissionType.Colonize) {
|
||||
if (!selectedFleet.value[ShipType.ColonyShip] || (selectedFleet.value[ShipType.ColonyShip] ?? 0) < 1) {
|
||||
return false
|
||||
return { valid: false, errorKey: 'fleetView.noColonyShip' }
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
// 检查回收任务是否有残骸
|
||||
if (selectedMission.value === MissionType.Recycle) {
|
||||
const debrisId = `debris_${targetPosition.value.galaxy}_${targetPosition.value.system}_${targetPosition.value.position}`
|
||||
const debrisField = universeStore.debrisFields[debrisId]
|
||||
if (!debrisField || (debrisField.resources.metal === 0 && debrisField.resources.crystal === 0)) {
|
||||
return { valid: false, errorKey: 'fleetView.noDebrisAtTarget' }
|
||||
}
|
||||
}
|
||||
|
||||
// 检查毁灭任务是否有死星
|
||||
if (selectedMission.value === MissionType.Destroy) {
|
||||
if (!selectedFleet.value[ShipType.Deathstar] || (selectedFleet.value[ShipType.Deathstar] ?? 0) < 1) {
|
||||
return { valid: false, errorKey: 'fleetView.noDeathstar' }
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
const sendFleet = (
|
||||
@@ -498,6 +549,16 @@
|
||||
const handleSendFleet = () => {
|
||||
if (!planet.value) return
|
||||
|
||||
// 验证是否可以派遣
|
||||
const validation = canSendFleet()
|
||||
if (!validation.valid) {
|
||||
alertDialog.value?.show({
|
||||
title: t('fleetView.sendFailed'),
|
||||
message: validation.errorKey ? t(validation.errorKey) : t('fleetView.sendFailedMessage')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 过滤出实际选择的舰船
|
||||
const fleet: Partial<Fleet> = {}
|
||||
for (const [shipType, count] of Object.entries(selectedFleet.value)) {
|
||||
@@ -547,23 +608,23 @@
|
||||
|
||||
// 获取任务剩余时间
|
||||
const getRemainingTime = (mission: any): number => {
|
||||
const now = Date.now()
|
||||
const now = currentTime.value
|
||||
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()
|
||||
const now = currentTime.value
|
||||
if (mission.status === 'outbound') {
|
||||
const total = mission.arrivalTime - mission.departureTime
|
||||
const elapsed = now - mission.departureTime
|
||||
return Math.min(100, (elapsed / total) * 100)
|
||||
return Math.max(0, 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)
|
||||
return Math.max(0, Math.min(100, (elapsed / total) * 100))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
291
src/views/GMView.vue
Normal file
291
src/views/GMView.vue
Normal file
@@ -0,0 +1,291 @@
|
||||
<template>
|
||||
<div class="container mx-auto p-4 sm:p-6 space-y-4 sm:space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('gmView.title') }}</h1>
|
||||
<Badge variant="destructive">{{ t('gmView.adminOnly') }}</Badge>
|
||||
</div>
|
||||
|
||||
<!-- 星球选择 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('gmView.selectPlanet') }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Select v-model="selectedPlanetId">
|
||||
<SelectTrigger>
|
||||
<SelectValue :placeholder="t('gmView.choosePlanet')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="planet in gameStore.player.planets" :key="planet.id" :value="planet.id">
|
||||
{{ planet.name }} ({{ planet.position.galaxy }}:{{ planet.position.system }}:{{ planet.position.position }})
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 标签切换 -->
|
||||
<div v-if="selectedPlanet" class="flex flex-wrap gap-2 border-b">
|
||||
<Button @click="activeTab = 'resources'" :variant="activeTab === 'resources' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
{{ t('gmView.resources') }}
|
||||
</Button>
|
||||
<Button @click="activeTab = 'buildings'" :variant="activeTab === 'buildings' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
{{ t('gmView.buildings') }}
|
||||
</Button>
|
||||
<Button @click="activeTab = 'research'" :variant="activeTab === 'research' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
{{ t('gmView.research') }}
|
||||
</Button>
|
||||
<Button @click="activeTab = 'ships'" :variant="activeTab === 'ships' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
{{ t('gmView.ships') }}
|
||||
</Button>
|
||||
<Button @click="activeTab = 'defense'" :variant="activeTab === 'defense' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
{{ t('gmView.defense') }}
|
||||
</Button>
|
||||
<Button @click="activeTab = 'officers'" :variant="activeTab === 'officers' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
{{ t('gmView.officers') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 资源 -->
|
||||
<div v-if="selectedPlanet && activeTab === 'resources'" class="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('gmView.modifyResources') }}</CardTitle>
|
||||
<CardDescription>{{ t('gmView.resourcesDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div v-for="resource in resourceTypes" :key="resource" class="space-y-2">
|
||||
<Label>{{ t(`resources.${resource}`) }}</Label>
|
||||
<div class="flex gap-2">
|
||||
<Input v-model.number="selectedPlanet.resources[resource]" type="number" min="0" class="flex-1" />
|
||||
<Button @click="setResourceAmount(resource, 1000000)" variant="outline" size="sm">+1M</Button>
|
||||
<Button @click="setResourceAmount(resource, 10000000)" variant="outline" size="sm">+10M</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- 建筑 -->
|
||||
<div v-if="selectedPlanet && activeTab === 'buildings'" class="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('gmView.modifyBuildings') }}</CardTitle>
|
||||
<CardDescription>{{ t('gmView.buildingsDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div v-for="building in buildingTypes" :key="building" class="space-y-2">
|
||||
<Label>{{ BUILDINGS[building].name }}</Label>
|
||||
<div class="flex gap-2">
|
||||
<Input v-model.number="selectedPlanet.buildings[building]" type="number" min="0" max="100" class="flex-1" />
|
||||
<Button @click="setBuildingLevel(building, 10)" variant="outline" size="sm">Lv 10</Button>
|
||||
<Button @click="setBuildingLevel(building, 30)" variant="outline" size="sm">Lv 30</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- 科技 -->
|
||||
<div v-if="activeTab === 'research'" class="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('gmView.modifyResearch') }}</CardTitle>
|
||||
<CardDescription>{{ t('gmView.researchDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div v-for="tech in technologyTypes" :key="tech" class="space-y-2">
|
||||
<Label>{{ TECHNOLOGIES[tech].name }}</Label>
|
||||
<div class="flex gap-2">
|
||||
<Input v-model.number="gameStore.player.technologies[tech]" type="number" min="0" max="50" class="flex-1" />
|
||||
<Button @click="setTechnologyLevel(tech, 10)" variant="outline" size="sm">Lv 10</Button>
|
||||
<Button @click="setTechnologyLevel(tech, 20)" variant="outline" size="sm">Lv 20</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- 舰船 -->
|
||||
<div v-if="selectedPlanet && activeTab === 'ships'" class="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('gmView.modifyShips') }}</CardTitle>
|
||||
<CardDescription>{{ t('gmView.shipsDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div v-for="ship in shipTypes" :key="ship" class="space-y-2">
|
||||
<Label>{{ SHIPS[ship].name }}</Label>
|
||||
<div class="flex gap-2">
|
||||
<Input v-model.number="selectedPlanet.fleet[ship]" type="number" min="0" class="flex-1" />
|
||||
<Button @click="setShipCount(ship, 100)" variant="outline" size="sm">+100</Button>
|
||||
<Button @click="setShipCount(ship, 1000)" variant="outline" size="sm">+1K</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- 防御 -->
|
||||
<div v-if="selectedPlanet && activeTab === 'defense'" class="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('gmView.modifyDefense') }}</CardTitle>
|
||||
<CardDescription>{{ t('gmView.defenseDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div v-for="defense in defenseTypes" :key="defense" class="space-y-2">
|
||||
<Label>{{ DEFENSES[defense].name }}</Label>
|
||||
<div class="flex gap-2">
|
||||
<Input v-model.number="selectedPlanet.defense[defense]" type="number" min="0" class="flex-1" />
|
||||
<Button @click="setDefenseCount(defense, 100)" variant="outline" size="sm">+100</Button>
|
||||
<Button @click="setDefenseCount(defense, 1000)" variant="outline" size="sm">+1K</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- 军官 -->
|
||||
<div v-if="activeTab === 'officers'" class="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('gmView.modifyOfficers') }}</CardTitle>
|
||||
<CardDescription>{{ t('gmView.officersDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div v-for="officer in officerTypes" :key="officer" class="space-y-2">
|
||||
<Label>{{ OFFICERS[officer].name }}</Label>
|
||||
<div class="flex gap-2">
|
||||
<Input v-model.number="officerDays[officer]" type="number" min="0" :placeholder="t('gmView.days')" class="flex-1" />
|
||||
<Button @click="setOfficerDays(officer, 7)" variant="outline" size="sm">7{{ t('gmView.days') }}</Button>
|
||||
<Button @click="setOfficerDays(officer, 30)" variant="outline" size="sm">30{{ t('gmView.days') }}</Button>
|
||||
<Button @click="setOfficerDays(officer, 365)" variant="outline" size="sm">365{{ t('gmView.days') }}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- 危险操作 -->
|
||||
<Card class="border-destructive">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-destructive">{{ t('gmView.dangerZone') }}</CardTitle>
|
||||
<CardDescription>{{ t('gmView.dangerZoneDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-2">
|
||||
<Button @click="resetGame" variant="destructive" class="w-full">{{ t('gmView.resetGame') }}</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { useGameConfig } from '@/composables/useGameConfig'
|
||||
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { BuildingType, TechnologyType, ShipType, DefenseType, OfficerType } from '@/types/game'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const { t } = useI18n()
|
||||
const { BUILDINGS, TECHNOLOGIES, SHIPS, DEFENSES, OFFICERS } = useGameConfig()
|
||||
|
||||
const selectedPlanetId = ref<string>(gameStore.player.planets[0]?.id || '')
|
||||
const activeTab = ref<'resources' | 'buildings' | 'research' | 'ships' | 'defense' | 'officers'>('resources')
|
||||
const officerDays = ref<Record<OfficerType, number>>({} as Record<OfficerType, number>)
|
||||
|
||||
// 初始化军官天数显示
|
||||
Object.values(OfficerType).forEach(officer => {
|
||||
const officerData = gameStore.player.officers[officer]
|
||||
if (officerData && officerData.expiresAt) {
|
||||
const daysLeft = Math.ceil((officerData.expiresAt - Date.now()) / (1000 * 60 * 60 * 24))
|
||||
officerDays.value[officer] = Math.max(0, daysLeft)
|
||||
} else {
|
||||
officerDays.value[officer] = 0
|
||||
}
|
||||
})
|
||||
|
||||
const selectedPlanet = computed(() => {
|
||||
return gameStore.player.planets.find(p => p.id === selectedPlanetId.value)
|
||||
})
|
||||
|
||||
const resourceTypes = ['metal', 'crystal', 'deuterium', 'darkMatter'] as const
|
||||
const buildingTypes = Object.values(BuildingType)
|
||||
const technologyTypes = Object.values(TechnologyType)
|
||||
const shipTypes = Object.values(ShipType)
|
||||
const defenseTypes = Object.values(DefenseType)
|
||||
const officerTypes = Object.values(OfficerType)
|
||||
|
||||
const setResourceAmount = (resource: string, amount: number) => {
|
||||
if (selectedPlanet.value) {
|
||||
selectedPlanet.value.resources[resource as keyof typeof selectedPlanet.value.resources] += amount
|
||||
}
|
||||
}
|
||||
|
||||
const setBuildingLevel = (building: BuildingType, level: number) => {
|
||||
if (selectedPlanet.value) {
|
||||
selectedPlanet.value.buildings[building] = level
|
||||
}
|
||||
}
|
||||
|
||||
const setTechnologyLevel = (tech: TechnologyType, level: number) => {
|
||||
gameStore.player.technologies[tech] = level
|
||||
}
|
||||
|
||||
const setShipCount = (ship: ShipType, count: number) => {
|
||||
if (selectedPlanet.value) {
|
||||
selectedPlanet.value.fleet[ship] = (selectedPlanet.value.fleet[ship] || 0) + count
|
||||
}
|
||||
}
|
||||
|
||||
const setDefenseCount = (defense: DefenseType, count: number) => {
|
||||
if (selectedPlanet.value) {
|
||||
selectedPlanet.value.defense[defense] = (selectedPlanet.value.defense[defense] || 0) + count
|
||||
}
|
||||
}
|
||||
|
||||
const setOfficerDays = (officer: OfficerType, days: number) => {
|
||||
officerDays.value[officer] = days
|
||||
const now = Date.now()
|
||||
const expiresAt = now + days * 24 * 60 * 60 * 1000
|
||||
|
||||
if (!gameStore.player.officers[officer]) {
|
||||
gameStore.player.officers[officer] = {
|
||||
type: officer,
|
||||
active: true,
|
||||
hiredAt: now,
|
||||
expiresAt: expiresAt
|
||||
}
|
||||
} else {
|
||||
gameStore.player.officers[officer].expiresAt = expiresAt
|
||||
gameStore.player.officers[officer].active = true
|
||||
if (!gameStore.player.officers[officer].hiredAt) {
|
||||
gameStore.player.officers[officer].hiredAt = now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const resetGame = () => {
|
||||
if (confirm(t('gmView.resetGameConfirm'))) {
|
||||
localStorage.clear()
|
||||
location.reload()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -94,50 +94,79 @@
|
||||
</p>
|
||||
</div>
|
||||
<div v-else class="text-sm text-muted-foreground">{{ t('galaxyView.emptySlot') }}</div>
|
||||
|
||||
<!-- 残骸场信息 -->
|
||||
<div v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)" class="mt-2 p-2 bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800 rounded text-xs">
|
||||
<div class="flex items-center gap-2 text-amber-700 dark:text-amber-400 font-medium mb-1">
|
||||
<span>{{ t('galaxyView.debrisField') }}</span>
|
||||
</div>
|
||||
<div class="flex gap-3 text-xs">
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="text-muted-foreground">{{ t('resources.metal') }}:</span>
|
||||
<span class="font-medium">{{ formatNumber(getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)!.resources.metal) }}</span>
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="text-muted-foreground">{{ t('resources.crystal') }}:</span>
|
||||
<span class="font-medium">{{ formatNumber(getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)!.resources.crystal) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</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>
|
||||
<TooltipProvider :delay-duration="300">
|
||||
<Tooltip v-if="slot.planet && !isMyPlanet(slot.planet)">
|
||||
<TooltipTrigger as-child>
|
||||
<Button @click="showPlanetActions(slot.planet, 'spy')" variant="outline" size="sm" class="h-8 w-8 p-0">
|
||||
<Eye class="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{{ t('galaxyView.scout') }}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip v-if="slot.planet && !isMyPlanet(slot.planet)">
|
||||
<TooltipTrigger as-child>
|
||||
<Button @click="showPlanetActions(slot.planet, 'attack')" variant="outline" size="sm" class="h-8 w-8 p-0">
|
||||
<Sword class="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{{ t('galaxyView.attack') }}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip v-if="!slot.planet">
|
||||
<TooltipTrigger as-child>
|
||||
<Button @click="showPlanetActions(null, 'colonize', slot.position)" variant="outline" size="sm" class="h-8 w-8 p-0">
|
||||
<Rocket class="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{{ t('galaxyView.colonize') }}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip v-if="slot.planet && isMyPlanet(slot.planet)">
|
||||
<TooltipTrigger as-child>
|
||||
<Button @click="switchToPlanet(slot.planet.id)" variant="outline" size="sm" class="h-8 w-8 p-0">
|
||||
<Home class="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{{ t('galaxyView.switch') }}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)">
|
||||
<TooltipTrigger as-child>
|
||||
<Button @click="showPlanetActions(slot.planet, 'recycle', slot.position)" variant="outline" size="sm" class="h-8 w-8 p-0">
|
||||
<Recycle class="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{{ t('galaxyView.recycle') }}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -151,20 +180,24 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useUniverseStore } from '@/stores/universeStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import type { Planet } from '@/types/game'
|
||||
import type { Planet, DebrisField } 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 { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import AlertDialog from '@/components/AlertDialog.vue'
|
||||
import { Home, Eye, Sword, Rocket } from 'lucide-vue-next'
|
||||
import { Home, Eye, Sword, Rocket, Recycle } from 'lucide-vue-next'
|
||||
import { useRouter } from 'vue-router'
|
||||
import * as gameLogic from '@/logic/gameLogic'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const universeStore = useUniverseStore()
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
const actionDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
|
||||
@@ -191,11 +224,22 @@
|
||||
const positions = gameLogic.generateSystemPositions(galaxy, system)
|
||||
return positions.map(pos => {
|
||||
const key = gameLogic.generatePositionKey(galaxy, system, pos.position)
|
||||
const planet = gameStore.universePlanets[key] || null
|
||||
// 先从玩家星球中查找,再从宇宙地图中查找
|
||||
const planet = gameStore.player.planets.find(p =>
|
||||
p.position.galaxy === galaxy &&
|
||||
p.position.system === system &&
|
||||
p.position.position === pos.position
|
||||
) || universeStore.planets[key] || null
|
||||
return { position: pos.position, planet }
|
||||
})
|
||||
}
|
||||
|
||||
// 获取指定位置的残骸场
|
||||
const getDebrisFieldAt = (galaxy: number, system: number, position: number): DebrisField | null => {
|
||||
const debrisId = `debris_${galaxy}_${system}_${position}`
|
||||
return universeStore.debrisFields[debrisId] || null
|
||||
}
|
||||
|
||||
// 加载星系
|
||||
const loadSystem = () => {
|
||||
currentGalaxy.value = selectedGalaxy.value
|
||||
@@ -223,11 +267,11 @@
|
||||
// 切换到指定星球
|
||||
const switchToPlanet = (planetId: string) => {
|
||||
gameStore.currentPlanetId = planetId
|
||||
router.push('/overview')
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
// 显示星球操作
|
||||
const showPlanetActions = (planet: Planet | null, action: 'spy' | 'attack' | 'colonize', position?: number) => {
|
||||
const showPlanetActions = (planet: Planet | null, action: 'spy' | 'attack' | 'colonize' | 'recycle', position?: number) => {
|
||||
const targetPos = planet ? planet.position : { galaxy: currentGalaxy.value, system: currentSystem.value, position: position! }
|
||||
const coordinates = `${targetPos.galaxy}:${targetPos.system}:${targetPos.position}`
|
||||
|
||||
@@ -242,6 +286,9 @@
|
||||
} else if (action === 'colonize') {
|
||||
title = t('galaxyView.colonizePlanetTitle')
|
||||
message = t('galaxyView.colonizePlanetMessage').replace('{coordinates}', coordinates)
|
||||
} else if (action === 'recycle') {
|
||||
title = t('galaxyView.recyclePlanetTitle')
|
||||
message = t('galaxyView.recyclePlanetMessage').replace('{coordinates}', coordinates)
|
||||
}
|
||||
|
||||
actionDialog.value?.show({
|
||||
|
||||
@@ -1,239 +1,119 @@
|
||||
<template>
|
||||
<div class="container mx-auto p-4 sm:p-6 space-y-4 sm:space-y-6">
|
||||
<div class="container mx-auto p-4 sm:p-6 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>
|
||||
<Sword class="h-4 w-4 mr-2" />
|
||||
{{ t('messagesView.battles') }}
|
||||
<Badge v-if="unreadBattles > 0" variant="destructive" class="ml-2">{{ unreadBattles }}</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>
|
||||
<Eye class="h-4 w-4 mr-2" />
|
||||
{{ t('messagesView.spy') }}
|
||||
<Badge v-if="unreadSpyReports > 0" variant="destructive" class="ml-2">{{ unreadSpyReports }}</Badge>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 战斗报告 -->
|
||||
<div v-if="activeTab === 'battles'" class="space-y-4">
|
||||
<!-- 战斗报告列表 -->
|
||||
<div v-if="activeTab === 'battles'" class="space-y-2">
|
||||
<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">
|
||||
<Card
|
||||
v-for="report in sortedBattleReports"
|
||||
:key="report.id"
|
||||
@click="openBattleReport(report)"
|
||||
class="cursor-pointer hover:shadow-md transition-shadow"
|
||||
>
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<Sword class="h-4 w-4 flex-shrink-0" />
|
||||
<CardTitle class="text-base sm:text-lg">{{ t('messagesView.battleReport') }}</CardTitle>
|
||||
<CardDescription class="text-xs sm:text-sm">
|
||||
{{ formatDate(report.timestamp) }}
|
||||
</CardDescription>
|
||||
<Badge v-if="!report.read" variant="default" class="text-xs">{{ t('messagesView.unread') }}</Badge>
|
||||
<Badge
|
||||
:variant="report.winner === 'attacker' ? 'default' : report.winner === 'defender' ? 'destructive' : 'secondary'"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ report.winner === 'attacker' ? t('messagesView.victory') : report.winner === 'defender' ? t('messagesView.defeat') : t('messagesView.draw') }}
|
||||
</Badge>
|
||||
</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>
|
||||
<Button @click.stop="deleteBattleReport(report.id)" variant="ghost" size="icon" class="h-8 w-8 flex-shrink-0">
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<CardDescription class="text-xs sm:text-sm">
|
||||
{{ formatDate(report.timestamp) }}
|
||||
</CardDescription>
|
||||
</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">
|
||||
<!-- 间谍报告列表 -->
|
||||
<div v-if="activeTab === 'spy'" class="space-y-2">
|
||||
<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>
|
||||
<Card
|
||||
v-for="report in sortedSpyReports"
|
||||
:key="report.id"
|
||||
@click="openSpyReport(report)"
|
||||
class="cursor-pointer hover:shadow-md transition-shadow"
|
||||
>
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<Eye class="h-4 w-4 flex-shrink-0" />
|
||||
<CardTitle class="text-base sm:text-lg">{{ t('messagesView.spyReport') }}</CardTitle>
|
||||
<CardDescription class="text-xs sm:text-sm">
|
||||
{{ formatDate(report.timestamp) }}
|
||||
</CardDescription>
|
||||
<Badge v-if="!report.read" variant="default" class="text-xs">{{ t('messagesView.unread') }}</Badge>
|
||||
<Badge variant="outline" class="text-xs">{{ report.targetPlanetId }}</Badge>
|
||||
</div>
|
||||
<Badge variant="outline">
|
||||
{{ report.targetPlanetId }}
|
||||
</Badge>
|
||||
<Button @click.stop="deleteSpyReport(report.id)" variant="ghost" size="icon" class="h-8 w-8 flex-shrink-0">
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<CardDescription class="text-xs sm:text-sm">
|
||||
{{ formatDate(report.timestamp) }}
|
||||
</CardDescription>
|
||||
</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>
|
||||
|
||||
<!-- 战斗报告对话框 -->
|
||||
<BattleReportDialog v-model:open="showBattleDialog" :report="selectedBattleReport" />
|
||||
|
||||
<!-- 间谍报告对话框 -->
|
||||
<SpyReportDialog v-model:open="showSpyDialog" :report="selectedSpyReport" />
|
||||
</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'
|
||||
import BattleReportDialog from '@/components/BattleReportDialog.vue'
|
||||
import SpyReportDialog from '@/components/SpyReportDialog.vue'
|
||||
import { formatDate } from '@/utils/format'
|
||||
import { X, Sword, Eye } from 'lucide-vue-next'
|
||||
import type { BattleResult, SpyReport } from '@/types/game'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const { t } = useI18n()
|
||||
const { SHIPS, DEFENSES, BUILDINGS } = useGameConfig()
|
||||
const activeTab = ref<'battles' | 'spy'>('battles')
|
||||
|
||||
// 对话框状态
|
||||
const showBattleDialog = ref(false)
|
||||
const showSpyDialog = ref(false)
|
||||
const selectedBattleReport = ref<BattleResult | null>(null)
|
||||
const selectedSpyReport = ref<SpyReport | null>(null)
|
||||
|
||||
// 排序后的战斗报告(最新的在前)
|
||||
const sortedBattleReports = computed(() => {
|
||||
return [...gameStore.player.battleReports].sort((a, b) => b.timestamp - a.timestamp)
|
||||
@@ -244,9 +124,49 @@
|
||||
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)
|
||||
// 未读战斗报告数量
|
||||
const unreadBattles = computed(() => {
|
||||
return gameStore.player.battleReports.filter(r => !r.read).length
|
||||
})
|
||||
|
||||
// 未读间谍报告数量
|
||||
const unreadSpyReports = computed(() => {
|
||||
return gameStore.player.spyReports.filter(r => !r.read).length
|
||||
})
|
||||
|
||||
// 打开战斗报告
|
||||
const openBattleReport = (report: BattleResult) => {
|
||||
selectedBattleReport.value = report
|
||||
showBattleDialog.value = true
|
||||
// 标记为已读
|
||||
if (!report.read) {
|
||||
report.read = true
|
||||
}
|
||||
}
|
||||
|
||||
// 打开间谍报告
|
||||
const openSpyReport = (report: SpyReport) => {
|
||||
selectedSpyReport.value = report
|
||||
showSpyDialog.value = true
|
||||
// 标记为已读
|
||||
if (!report.read) {
|
||||
report.read = true
|
||||
}
|
||||
}
|
||||
|
||||
// 删除战斗报告
|
||||
const deleteBattleReport = (reportId: string) => {
|
||||
const index = gameStore.player.battleReports.findIndex(r => r.id === reportId)
|
||||
if (index > -1) {
|
||||
gameStore.player.battleReports.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除间谍报告
|
||||
const deleteSpyReport = (reportId: string) => {
|
||||
const index = gameStore.player.spyReports.findIndex(r => r.id === reportId)
|
||||
if (index > -1) {
|
||||
gameStore.player.spyReports.splice(index, 1)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
<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>
|
||||
<TableHead class="text-right">{{ t('resources.consumption') }}{{ t('resources.perHour') }}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -44,40 +45,139 @@
|
||||
{{ 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>
|
||||
<!-- 所有资源统一显示 -->
|
||||
<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-green-600 dark:text-green-400">
|
||||
+{{ formatNumber(production?.[resourceType.key] || 0) }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right text-red-600 dark:text-red-400">
|
||||
<template v-if="resourceType.key === 'energy'">
|
||||
-{{ formatNumber(energyConsumption) }}
|
||||
</template>
|
||||
<template v-else>
|
||||
-
|
||||
</template>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 资源获取来源 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('overview.productionSources') }}</CardTitle>
|
||||
<CardDescription>{{ t('overview.productionSourcesDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="space-y-4">
|
||||
<div v-for="resourceType in resourceTypes" :key="resourceType.key" class="border-b last:border-b-0 pb-4 last:pb-0">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<ResourceIcon :type="resourceType.key" size="sm" />
|
||||
<span class="font-semibold">{{ t(`resources.${resourceType.key}`) }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="productionBreakdown" class="ml-6 space-y-1 text-sm">
|
||||
<!-- 建筑基础产量 -->
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">
|
||||
{{ t(productionBreakdown[resourceType.key].buildingName) }}
|
||||
({{ t('common.level') }} {{ productionBreakdown[resourceType.key].buildingLevel }})
|
||||
</span>
|
||||
<span class="text-green-600 dark:text-green-400">
|
||||
+{{ formatNumber(Math.floor(productionBreakdown[resourceType.key].baseProduction)) }}/{{ t('resources.hour') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 加成列表 -->
|
||||
<div v-for="(bonus, idx) in productionBreakdown[resourceType.key].bonuses" :key="idx" class="flex justify-between">
|
||||
<span class="text-muted-foreground ml-4">
|
||||
{{ t(bonus.name) }}
|
||||
</span>
|
||||
<span :class="bonus.value > 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'">
|
||||
{{ bonus.value > 0 ? '+' : '' }}{{ bonus.value }}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 最终产量 -->
|
||||
<div class="flex justify-between font-semibold pt-1 border-t mt-1">
|
||||
<span>{{ t('overview.totalProduction') }}</span>
|
||||
<span class="text-green-600 dark:text-green-400">
|
||||
+{{ formatNumber(Math.floor(productionBreakdown[resourceType.key].finalProduction)) }}/{{ t('resources.hour') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 资源消耗来源 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('overview.consumptionSources') }}</CardTitle>
|
||||
<CardDescription>{{ t('overview.consumptionSourcesDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="space-y-2">
|
||||
<!-- 金属矿消耗 -->
|
||||
<div v-if="consumptionBreakdown && consumptionBreakdown.metalMine.buildingLevel > 0" class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">
|
||||
{{ t(consumptionBreakdown.metalMine.buildingName) }}
|
||||
({{ t('common.level') }} {{ consumptionBreakdown.metalMine.buildingLevel }})
|
||||
</span>
|
||||
<span class="text-red-600 dark:text-red-400">
|
||||
-{{ formatNumber(Math.floor(consumptionBreakdown.metalMine.consumption)) }}/{{ t('resources.hour') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 晶体矿消耗 -->
|
||||
<div v-if="consumptionBreakdown && consumptionBreakdown.crystalMine.buildingLevel > 0" class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">
|
||||
{{ t(consumptionBreakdown.crystalMine.buildingName) }}
|
||||
({{ t('common.level') }} {{ consumptionBreakdown.crystalMine.buildingLevel }})
|
||||
</span>
|
||||
<span class="text-red-600 dark:text-red-400">
|
||||
-{{ formatNumber(Math.floor(consumptionBreakdown.crystalMine.consumption)) }}/{{ t('resources.hour') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 重氢合成器消耗 -->
|
||||
<div v-if="consumptionBreakdown && consumptionBreakdown.deuteriumSynthesizer.buildingLevel > 0" class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">
|
||||
{{ t(consumptionBreakdown.deuteriumSynthesizer.buildingName) }}
|
||||
({{ t('common.level') }} {{ consumptionBreakdown.deuteriumSynthesizer.buildingLevel }})
|
||||
</span>
|
||||
<span class="text-red-600 dark:text-red-400">
|
||||
-{{ formatNumber(Math.floor(consumptionBreakdown.deuteriumSynthesizer.consumption)) }}/{{ t('resources.hour') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 总消耗 -->
|
||||
<div v-if="consumptionBreakdown" class="flex justify-between font-semibold pt-2 border-t">
|
||||
<span>{{ t('overview.totalConsumption') }}</span>
|
||||
<span class="text-red-600 dark:text-red-400">
|
||||
-{{ formatNumber(Math.floor(consumptionBreakdown.total)) }}/{{ t('resources.hour') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 无消耗提示 -->
|
||||
<div v-if="consumptionBreakdown && consumptionBreakdown.total === 0" class="text-sm text-muted-foreground text-center py-2">
|
||||
{{ t('overview.noConsumption') }}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 舰队信息 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -109,8 +209,8 @@
|
||||
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'
|
||||
import * as officerLogic from '@/logic/officerLogic'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const { t } = useI18n()
|
||||
@@ -119,18 +219,25 @@
|
||||
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)
|
||||
})
|
||||
|
||||
// 资源产量详细breakdown
|
||||
const productionBreakdown = computed(() => {
|
||||
if (!planet.value) return null
|
||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
|
||||
return resourceLogic.calculateProductionBreakdown(planet.value, bonuses)
|
||||
})
|
||||
|
||||
// 资源消耗详细breakdown
|
||||
const consumptionBreakdown = computed(() => {
|
||||
if (!planet.value) return null
|
||||
return resourceLogic.calculateConsumptionBreakdown(planet.value)
|
||||
})
|
||||
|
||||
// 资源类型配置
|
||||
const resourceTypes = [
|
||||
{ key: 'metal' as const },
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
<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" />
|
||||
<CardUnlockOverlay :requirements="TECHNOLOGIES[techType].requirements" :currentLevel="getTechLevel(techType)" />
|
||||
<CardHeader>
|
||||
<div class="flex justify-between items-start gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
@@ -63,7 +63,7 @@
|
||||
</div>
|
||||
|
||||
<Button @click="handleResearch(techType)" :disabled="!canResearch(techType)" class="w-full">
|
||||
{{ t('researchView.research') }}
|
||||
{{ getResearchButtonText(techType) }}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -98,7 +98,7 @@
|
||||
const gameStore = useGameStore()
|
||||
const detailDialog = useDetailDialogStore()
|
||||
const { t } = useI18n()
|
||||
const { TECHNOLOGIES } = useGameConfig()
|
||||
const { TECHNOLOGIES, BUILDINGS } = useGameConfig()
|
||||
const planet = computed(() => gameStore.currentPlanet)
|
||||
const player = computed(() => gameStore.player)
|
||||
const alertDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
|
||||
@@ -123,8 +123,86 @@
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查升级前置条件是否满足
|
||||
const checkUpgradeRequirements = (techType: TechnologyType): boolean => {
|
||||
if (!planet.value) return false
|
||||
const config = TECHNOLOGIES.value[techType]
|
||||
const currentLevel = getTechLevel(techType)
|
||||
const targetLevel = currentLevel + 1
|
||||
|
||||
// 获取目标等级的所有前置条件(包括等级门槛)
|
||||
const requirements = publicLogic.getLevelRequirements(config, targetLevel)
|
||||
|
||||
if (!requirements || Object.keys(requirements).length === 0) return true
|
||||
return publicLogic.checkRequirements(planet.value, gameStore.player.technologies, requirements)
|
||||
}
|
||||
|
||||
// 获取研究按钮文本
|
||||
const getResearchButtonText = (techType: TechnologyType): string => {
|
||||
if (!planet.value) return t('researchView.research')
|
||||
|
||||
const config = TECHNOLOGIES.value[techType]
|
||||
const currentLevel = getTechLevel(techType)
|
||||
|
||||
// 检查是否达到等级上限
|
||||
if (config.maxLevel !== undefined && currentLevel >= config.maxLevel) {
|
||||
return t('researchView.maxLevelReached') // "等级已满"
|
||||
}
|
||||
|
||||
if (player.value.researchQueue.length > 0) return t('researchView.research')
|
||||
|
||||
// 检查前置条件
|
||||
if (!checkUpgradeRequirements(techType)) {
|
||||
return t('buildingsView.requirementsNotMet') // "条件不足"
|
||||
}
|
||||
|
||||
return t('researchView.research') // "研究"
|
||||
}
|
||||
|
||||
// 获取前置条件列表文本
|
||||
const getRequirementsList = (techType: TechnologyType): string => {
|
||||
const config = TECHNOLOGIES.value[techType]
|
||||
const currentLevel = getTechLevel(techType)
|
||||
const targetLevel = currentLevel + 1
|
||||
|
||||
// 获取目标等级的所有前置条件(包括等级门槛)
|
||||
const requirements = publicLogic.getLevelRequirements(config, targetLevel)
|
||||
|
||||
if (!requirements || !planet.value) return ''
|
||||
|
||||
const lines: string[] = []
|
||||
for (const [key, requiredLevel] of Object.entries(requirements)) {
|
||||
// 检查是否为建筑类型
|
||||
if (Object.values(BuildingType).includes(key as BuildingType)) {
|
||||
const bt = key as BuildingType
|
||||
const currentLevel = planet.value.buildings[bt] || 0
|
||||
const name = BUILDINGS.value[bt]?.name || bt
|
||||
const status = currentLevel >= requiredLevel ? '✓' : '✗'
|
||||
lines.push(`${status} ${name}: Lv ${requiredLevel} (${t('common.current')}: Lv ${currentLevel})`)
|
||||
}
|
||||
// 检查是否为科技类型
|
||||
else if (Object.values(TechnologyType).includes(key as TechnologyType)) {
|
||||
const tt = key as TechnologyType
|
||||
const currentLevel = gameStore.player.technologies[tt] || 0
|
||||
const name = TECHNOLOGIES.value[tt]?.name || tt
|
||||
const status = currentLevel >= requiredLevel ? '✓' : '✗'
|
||||
lines.push(`${status} ${name}: Lv ${requiredLevel} (${t('common.current')}: Lv ${currentLevel})`)
|
||||
}
|
||||
}
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
// 研究科技
|
||||
const handleResearch = (techType: TechnologyType) => {
|
||||
// 检查前置条件
|
||||
if (!checkUpgradeRequirements(techType)) {
|
||||
alertDialog.value?.show({
|
||||
title: t('common.requirementsNotMet'),
|
||||
message: getRequirementsList(techType)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const success = researchTechnology(techType)
|
||||
if (!success) {
|
||||
alertDialog.value?.show({
|
||||
@@ -141,10 +219,18 @@
|
||||
|
||||
// 检查是否可以研究
|
||||
const canResearch = (techType: TechnologyType): boolean => {
|
||||
if (!planet.value || player.value.researchQueue.length > 0) return false
|
||||
if (!planet.value) return false
|
||||
|
||||
const config = TECHNOLOGIES.value[techType]
|
||||
const currentLevel = getTechLevel(techType)
|
||||
|
||||
// 检查是否达到等级上限
|
||||
if (config.maxLevel !== undefined && currentLevel >= config.maxLevel) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (player.value.researchQueue.length > 0) return false
|
||||
|
||||
const cost = getTechnologyCost(techType, currentLevel + 1)
|
||||
|
||||
return (
|
||||
|
||||
@@ -59,19 +59,16 @@
|
||||
<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="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div class="space-y-1">
|
||||
<Label>{{ t('settings.gameSpeed') }}</Label>
|
||||
<p class="text-sm text-muted-foreground">{{ t('settings.gameSpeedDesc') }}</p>
|
||||
<h3 class="font-medium">{{ t('settings.gamePause') }}</h3>
|
||||
<p class="text-sm text-muted-foreground">{{ t('settings.gamePauseDesc') }}</p>
|
||||
</div>
|
||||
<div class="text-2xl font-bold">1x</div>
|
||||
<Button @click="togglePause" :variant="gameStore.isPaused ? 'default' : 'outline'">
|
||||
<component :is="gameStore.isPaused ? Play : Pause" class="mr-2 h-4 w-4" />
|
||||
{{ gameStore.isPaused ? t('settings.resume') : t('settings.pause') }}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -89,7 +86,7 @@
|
||||
</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>
|
||||
<span class="font-medium">{{ pkg.buildDate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -139,8 +136,6 @@
|
||||
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,
|
||||
@@ -151,17 +146,17 @@
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Download, Upload, Trash2, ExternalLink, MessagesSquare } from 'lucide-vue-next'
|
||||
import { Download, Upload, Trash2, ExternalLink, MessagesSquare, Play, Pause } from 'lucide-vue-next'
|
||||
import { saveAs } from 'file-saver'
|
||||
import { toast } from 'vue-sonner'
|
||||
import pkg from '../../package.json'
|
||||
import 'vue-sonner/style.css'
|
||||
|
||||
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('')
|
||||
@@ -176,17 +171,30 @@
|
||||
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) {
|
||||
|
||||
// 获取游戏数据
|
||||
const gameData = localStorage.getItem(pkg.name)
|
||||
// 获取地图数据
|
||||
const universeData = localStorage.getItem(`${pkg.name}-universe`)
|
||||
|
||||
if (!gameData) {
|
||||
toast.error(t('settings.exportFailed'))
|
||||
return
|
||||
}
|
||||
|
||||
// 合并数据
|
||||
const exportData = {
|
||||
game: gameData,
|
||||
universe: universeData || null
|
||||
}
|
||||
|
||||
const fileName = `${pkg.name}-${new Date().toISOString().slice(0, 10)}-${Date.now()}.json`
|
||||
saveAs(new Blob([data], { type: 'application/json' }), fileName)
|
||||
const jsonString = JSON.stringify(exportData, null, 2)
|
||||
saveAs(new Blob([jsonString], { type: 'application/json' }), fileName)
|
||||
toast.success(t('settings.exportSuccess'))
|
||||
} catch (error) {
|
||||
console.error('Export failed:', error)
|
||||
@@ -205,14 +213,14 @@
|
||||
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
|
||||
gameStore.isPaused = true
|
||||
confirmCallback = () => importData(file)
|
||||
}
|
||||
|
||||
// 导入数据
|
||||
// 导入数据(包含游戏数据和地图数据)
|
||||
const importData = async (file: File) => {
|
||||
try {
|
||||
const reader = new FileReader()
|
||||
@@ -220,9 +228,28 @@
|
||||
try {
|
||||
const result = e.target?.result
|
||||
if (typeof result === 'string') {
|
||||
localStorage.setItem(pkg.name, result)
|
||||
const importData = JSON.parse(result)
|
||||
|
||||
// 兼容旧版本:如果是旧格式(直接是字符串),只导入游戏数据
|
||||
if (typeof importData === 'string' || !importData.game) {
|
||||
localStorage.setItem(pkg.name, result)
|
||||
toast.success(t('settings.importSuccess'))
|
||||
setTimeout(() => window.location.reload(), 1000)
|
||||
return
|
||||
}
|
||||
|
||||
// 新格式:分别导入游戏数据和地图数据
|
||||
if (importData.game) {
|
||||
localStorage.setItem(pkg.name, importData.game)
|
||||
}
|
||||
|
||||
if (importData.universe) {
|
||||
localStorage.setItem(`${pkg.name}-universe`, importData.universe)
|
||||
}
|
||||
|
||||
toast.success(t('settings.importSuccess'))
|
||||
setTimeout(() => location.reload(), 500)
|
||||
// 延迟刷新页面以让toast显示
|
||||
setTimeout(() => window.location.reload(), 1000)
|
||||
} else {
|
||||
toast.error(t('settings.importFailed'))
|
||||
}
|
||||
@@ -253,10 +280,13 @@
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
// 更新玩家名称
|
||||
const updatePlayerName = () => {
|
||||
if (playerName.value.trim()) {
|
||||
gameStore.player.name = playerName.value.trim()
|
||||
// 切换游戏暂停状态
|
||||
const togglePause = () => {
|
||||
gameStore.isPaused = !gameStore.isPaused
|
||||
if (gameStore.isPaused) {
|
||||
toast.info(t('settings.gamePaused'))
|
||||
} else {
|
||||
toast.success(t('settings.gameResumed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,6 +301,7 @@
|
||||
|
||||
// 取消操作
|
||||
const cancelAction = () => {
|
||||
gameStore.isPaused = false
|
||||
confirmCallback = null
|
||||
showConfirmDialog.value = false
|
||||
// 重置文件输入
|
||||
|
||||
@@ -5,6 +5,29 @@
|
||||
|
||||
<h1 class="text-2xl sm:text-3xl font-bold mb-4 sm:mb-6">{{ t('shipyardView.title') }}</h1>
|
||||
|
||||
<!-- 舰队仓储显示 -->
|
||||
<div class="mb-4 sm:mb-6 p-3 sm:p-4 bg-muted/50 rounded-lg border">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-sm sm:text-base font-medium">{{ t('shipyardView.fleetStorage') }}:</div>
|
||||
<div class="text-sm sm:text-base font-bold">
|
||||
<span :class="fleetStorageUsage > maxFleetStorage ? 'text-destructive' : 'text-primary'">
|
||||
{{ formatNumber(fleetStorageUsage) }}
|
||||
</span>
|
||||
<span class="text-muted-foreground mx-1">/</span>
|
||||
<span>{{ formatNumber(maxFleetStorage) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<div class="w-full bg-background rounded-full h-2.5 sm:h-3 overflow-hidden">
|
||||
<div
|
||||
class="h-full transition-all duration-300"
|
||||
:class="fleetStorageUsage > maxFleetStorage ? 'bg-destructive' : 'bg-primary'"
|
||||
:style="{ width: `${Math.min((fleetStorageUsage / maxFleetStorage) * 100, 100)}%` }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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" />
|
||||
@@ -151,6 +174,7 @@
|
||||
import { formatNumber, getResourceCostColor } from '@/utils/format'
|
||||
import * as shipValidation from '@/logic/shipValidation'
|
||||
import * as publicLogic from '@/logic/publicLogic'
|
||||
import * as fleetStorageLogic from '@/logic/fleetStorageLogic'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const detailDialog = useDetailDialogStore()
|
||||
@@ -159,6 +183,18 @@
|
||||
const planet = computed(() => gameStore.currentPlanet)
|
||||
const alertDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
|
||||
|
||||
// 舰队仓储使用量
|
||||
const fleetStorageUsage = computed(() => {
|
||||
if (!planet.value) return 0
|
||||
return fleetStorageLogic.calculateFleetStorageUsage(planet.value.fleet)
|
||||
})
|
||||
|
||||
// 舰队仓储上限
|
||||
const maxFleetStorage = computed(() => {
|
||||
if (!planet.value) return 0
|
||||
return fleetStorageLogic.calculateMaxFleetStorage(planet.value, gameStore.player.technologies)
|
||||
})
|
||||
|
||||
// 每种舰船的建造数量
|
||||
const quantities = ref<Record<ShipType, number>>({
|
||||
[ShipType.LightFighter]: 0,
|
||||
@@ -170,7 +206,8 @@
|
||||
[ShipType.ColonyShip]: 0,
|
||||
[ShipType.Recycler]: 0,
|
||||
[ShipType.EspionageProbe]: 0,
|
||||
[ShipType.DarkMatterHarvester]: 0
|
||||
[ShipType.DarkMatterHarvester]: 0,
|
||||
[ShipType.Deathstar]: 0
|
||||
})
|
||||
|
||||
const buildShip = (shipType: ShipType, quantity: number): boolean => {
|
||||
|
||||
Reference in New Issue
Block a user