feat: 重构战报弹窗与模拟器视图,优化UI与逻辑

重构BattleReportDialog和BattleSimulatorView相关静态资源,替换旧版JS/CSS文件,提升界面一致性和交互体验。新增和优化空状态、滚动区域等通用UI组件,移除部分冗余composable,完善多语言内容。引入导弹逻辑,补充版本检测工具,提升整体代码结构和可维护性。
This commit is contained in:
谦君
2025-12-15 20:04:40 +08:00
parent 9b9fda0400
commit 59dd7bfd05
126 changed files with 3944 additions and 1487 deletions

View File

@@ -415,7 +415,9 @@
${t('buildingsView.demolishRefund')}:
${t('resources.metal')}: ${formatNumber(refund.metal)}
${t('resources.crystal')}: ${formatNumber(refund.crystal)}
${t('resources.deuterium')}: ${formatNumber(refund.deuterium)}${refund.darkMatter > 0 ? `\n${t('resources.darkMatter')}: ${formatNumber(refund.darkMatter)}` : ''}`
${t('resources.deuterium')}: ${formatNumber(refund.deuterium)}${
refund.darkMatter > 0 ? `\n${t('resources.darkMatter')}: ${formatNumber(refund.darkMatter)}` : ''
}`
pendingDemolishBuilding.value = buildingType
demolishConfirmOpen.value = true

View File

@@ -5,6 +5,29 @@
<h1 class="text-2xl sm:text-3xl font-bold mb-4 sm:mb-6">{{ t('defenseView.title') }}</h1>
<!-- 导弹容量显示 -->
<div v-if="missileSiloCapacity > 0" 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('defenseView.missileCapacity') }}:</div>
<div class="text-sm sm:text-base font-bold">
<span :class="currentMissileCount > missileSiloCapacity ? 'text-destructive' : 'text-primary'">
{{ formatNumber(currentMissileCount) }}
</span>
<span class="text-muted-foreground mx-1">/</span>
<span>{{ formatNumber(missileSiloCapacity) }}</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="currentMissileCount > missileSiloCapacity ? 'bg-destructive' : 'bg-primary'"
:style="{ width: `${Math.min((currentMissileCount / missileSiloCapacity) * 100, 100)}%` }"
/>
</div>
</div>
</div>
<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" />
@@ -156,6 +179,7 @@
import { formatNumber, getResourceCostColor } from '@/utils/format'
import * as publicLogic from '@/logic/publicLogic'
import * as shipValidation from '@/logic/shipValidation'
import * as shipLogic from '@/logic/shipLogic'
const gameStore = useGameStore()
const detailDialog = useDetailDialogStore()
@@ -163,6 +187,17 @@
const { DEFENSES } = useGameConfig()
const planet = computed(() => gameStore.currentPlanet)
// 导弹容量相关计算
const missileSiloCapacity = computed(() => {
if (!planet.value) return 0
return shipLogic.calculateMissileSiloCapacity(planet.value.buildings)
})
const currentMissileCount = computed(() => {
if (!planet.value) return 0
return shipLogic.calculateCurrentMissileCount(planet.value.defense)
})
// AlertDialog 状态
const alertDialogOpen = ref(false)
const alertDialogTitle = ref('')
@@ -197,11 +232,12 @@
}
const buildDefense = (defenseType: DefenseType, quantity: number): boolean => {
if (!gameStore.currentPlanet) return false
const validation = shipValidation.validateDefenseBuild(gameStore.currentPlanet, defenseType, quantity, gameStore.player.technologies)
const currentPlanet = gameStore.currentPlanet
if (!currentPlanet) return false
const validation = shipValidation.validateDefenseBuild(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)
const queueItem = shipValidation.executeDefenseBuild(currentPlanet, defenseType, quantity, gameStore.player.officers)
currentPlanet.buildQueue.push(queueItem)
return true
}

View File

@@ -12,19 +12,34 @@
<TabsList class="grid w-full grid-cols-4">
<TabsTrigger value="all">
{{ t('diplomacy.tabs.all') }}
<Badge variant="secondary" class="ml-2">{{ allNpcs.length }}</Badge>
<Badge variant="outline" class="ml-2 bg-blue-100 dark:bg-blue-950 text-blue-700 dark:text-blue-300 border-blue-300 dark:border-blue-700">{{ allNpcs.length }}</Badge>
</TabsTrigger>
<TabsTrigger value="friendly">
{{ t('diplomacy.tabs.friendly') }}
<Badge variant="secondary" class="ml-2">{{ friendlyNpcs.length }}</Badge>
<Badge
variant="outline"
class="ml-2 bg-green-100 dark:bg-green-950 text-green-700 dark:text-green-300 border-green-300 dark:border-green-700"
>
{{ friendlyNpcs.length }}
</Badge>
</TabsTrigger>
<TabsTrigger value="neutral">
{{ t('diplomacy.tabs.neutral') }}
<Badge variant="secondary" class="ml-2">{{ neutralNpcs.length }}</Badge>
<Badge
variant="outline"
class="ml-2 bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600"
>
{{ neutralNpcs.length }}
</Badge>
</TabsTrigger>
<TabsTrigger value="hostile">
{{ t('diplomacy.tabs.hostile') }}
<Badge variant="secondary" class="ml-2">{{ hostileNpcs.length }}</Badge>
<Badge
variant="outline"
class="ml-2 bg-red-100 dark:bg-red-950 text-red-700 dark:text-red-300 border-red-300 dark:border-red-700"
>
{{ hostileNpcs.length }}
</Badge>
</TabsTrigger>
</TabsList>
@@ -202,7 +217,7 @@
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { computed, ref, onMounted } from 'vue'
import { useGameStore } from '@/stores/gameStore'
import { useNPCStore } from '@/stores/npcStore'
import { useI18n } from '@/composables/useI18n'
@@ -222,6 +237,52 @@
const activeTab = ref('all')
// 检测并生成NPC盟友
const initializeNPCAllies = () => {
const npcs = npcStore.npcs
if (npcs.length < 2) return // 至少需要2个NPC才能生成盟友关系
npcs.forEach(npc => {
// 如果NPC没有盟友列表,初始化为空数组
if (!npc.allies) {
npc.allies = []
}
// 如果NPC没有盟友,随机生成1-2个盟友
if (npc.allies.length === 0) {
const otherNpcs = npcs.filter(n => n.id !== npc.id)
if (otherNpcs.length === 0) return
// 随机选择1-2个盟友
const allyCount = Math.min(Math.floor(Math.random() * 2) + 1, otherNpcs.length)
const shuffled = [...otherNpcs].sort(() => Math.random() - 0.5)
const selectedAllies = shuffled.slice(0, allyCount)
selectedAllies.forEach(ally => {
// 添加双向盟友关系
if (!npc.allies!.includes(ally.id)) {
npc.allies!.push(ally.id)
}
// 确保盟友也有盟友列表
if (!ally.allies) {
ally.allies = []
}
// 确保双向关系
if (!ally.allies.includes(npc.id)) {
ally.allies.push(npc.id)
}
})
}
})
}
// 组件挂载时初始化NPC盟友
onMounted(() => {
initializeNPCAllies()
})
// 分页状态
const ITEMS_PER_PAGE = 20
const currentPage = ref<Record<string, number>>({

View File

@@ -332,7 +332,7 @@
import { useNPCStore } from '@/stores/npcStore'
import { useI18n } from '@/composables/useI18n'
import { useGameConfig } from '@/composables/useGameConfig'
import { computed, ref, onMounted, onUnmounted } from 'vue'
import { computed, ref, onMounted, onUnmounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ShipType, MissionType, BuildingType } from '@/types/game'
import type { Fleet, Resources } from '@/types/game'
@@ -473,6 +473,13 @@
// 是否为赠送模式
const isGiftMode = ref(false)
// 监听目标NPC变化当目标不再是NPC时自动禁用赠送模式
watch(targetNpc, newValue => {
if (!newValue && isGiftMode.value) {
isGiftMode.value = false
}
})
// 计算赠送的预估好感度增加值
const calculateGiftReputation = (): number => {
return diplomaticLogic.calculateGiftReputationGain(cargo.value)

View File

@@ -1,5 +1,25 @@
<template>
<div class="container mx-auto p-4 sm:p-6 space-y-4 sm:space-y-6">
<!-- 无权限时显示404 -->
<div v-if="!gameStore.player.isGMEnabled" class="container mx-auto p-4 sm:p-6 flex items-center justify-center min-h-[60vh]">
<Empty class="border-0">
<EmptyMedia>
<div class="text-8xl sm:text-9xl font-bold text-muted-foreground/20">404</div>
</EmptyMedia>
<EmptyHeader>
<EmptyTitle>{{ t('notFound.title') }}</EmptyTitle>
<EmptyDescription>{{ t('notFound.description') }}</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button @click="goHome" size="lg">
<Home class="mr-2 h-4 w-4" />
{{ t('notFound.goHome') }}
</Button>
</EmptyContent>
</Empty>
</div>
<!-- 有权限时显示GM页面 -->
<div v-else 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>
@@ -12,7 +32,7 @@
</CardHeader>
<CardContent>
<Select v-model="selectedPlanetId">
<SelectTrigger>
<SelectTrigger class="w-full">
<SelectValue :placeholder="t('gmView.choosePlanet')" />
</SelectTrigger>
<SelectContent>
@@ -25,148 +45,144 @@
</Card>
<!-- 标签切换 -->
<div v-if="selectedPlanet" class="flex flex-wrap gap-2 border-b">
<Button
v-for="tab in tabs"
:key="tab.value"
@click="activeTab = tab.value"
:variant="activeTab === tab.value ? 'default' : 'ghost'"
class="rounded-b-none"
>
{{ t(tab.label) }}
</Button>
</div>
<Tabs v-if="selectedPlanet" default-value="resources" class="w-full">
<TabsList class="grid w-full" :style="{ gridTemplateColumns: `repeat(${tabs.length}, minmax(0, 1fr))` }">
<TabsTrigger v-for="tab in tabs" :key="tab.value" :value="tab.value">
{{ t(tab.label) }}
</TabsTrigger>
</TabsList>
<!-- 资源 -->
<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>
<!-- 资源 -->
<TabsContent value="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.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>
<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>
</div>
</CardContent>
</Card>
</div>
</CardContent>
</Card>
</TabsContent>
<!-- 科技 -->
<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>
<!-- 建筑 -->
<TabsContent value="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>
</div>
</CardContent>
</Card>
</div>
</CardContent>
</Card>
</TabsContent>
<!-- 舰船 -->
<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>
<!-- 科技 -->
<TabsContent value="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>
</div>
</CardContent>
</Card>
</div>
</CardContent>
</Card>
</TabsContent>
<!-- 防御 -->
<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>
<!-- 舰船 -->
<TabsContent value="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>
</div>
</CardContent>
</Card>
</div>
</CardContent>
</Card>
</TabsContent>
<!-- 军官 -->
<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>
<!-- 防御 -->
<TabsContent value="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>
</div>
</CardContent>
</Card>
</div>
</CardContent>
</Card>
</TabsContent>
<!-- 军官 -->
<TabsContent value="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>
</TabsContent>
</Tabs>
<!-- NPC测试 -->
<Card class="border-primary">
@@ -175,30 +191,32 @@
<CardDescription>{{ t('gmView.npcTestingDesc') || 'Test NPC spy and attack behavior' }}</CardDescription>
</CardHeader>
<CardContent class="space-y-3">
<div class="space-y-2">
<Label>{{ t('gmView.selectNPC') || 'Select NPC' }}</Label>
<Select v-model="selectedNPCId">
<SelectTrigger>
<SelectValue :placeholder="t('gmView.chooseNPC') || 'Choose NPC'" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="npc in npcStore.npcs" :key="npc.id" :value="npc.id">{{ npc.name }} ({{ npc.difficulty }})</SelectItem>
</SelectContent>
</Select>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div class="space-y-2">
<Label>{{ t('gmView.selectNPC') || 'Select NPC' }}</Label>
<Select v-model="selectedNPCId">
<SelectTrigger class="w-full">
<SelectValue :placeholder="t('gmView.chooseNPC') || 'Choose NPC'" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="npc in npcStore.npcs" :key="npc.id" :value="npc.id">{{ npc.name }} ({{ npc.difficulty }})</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label>{{ t('gmView.targetPlanet') || 'Target Planet' }}</Label>
<Select v-model="targetPlanetIndex">
<SelectTrigger>
<SelectValue :placeholder="t('gmView.chooseTarget') || 'Choose Target Planet'" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="(planet, index) in gameStore.player.planets" :key="planet.id" :value="index.toString()">
{{ planet.name }} ({{ planet.position.galaxy }}:{{ planet.position.system }}:{{ planet.position.position }})
</SelectItem>
</SelectContent>
</Select>
<div class="space-y-2">
<Label>{{ t('gmView.targetPlanet') || 'Target Planet' }}</Label>
<Select v-model="targetPlanetIndex">
<SelectTrigger class="w-full">
<SelectValue :placeholder="t('gmView.chooseTarget') || 'Choose Target Planet'" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="(planet, index) in gameStore.player.planets" :key="planet.id" :value="index.toString()">
{{ planet.name }} ({{ planet.position.galaxy }}:{{ planet.position.system }}:{{ planet.position.position }})
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div class="grid grid-cols-2 gap-2">
@@ -231,14 +249,46 @@
<CardDescription>{{ t('gmView.dangerZoneDesc') }}</CardDescription>
</CardHeader>
<CardContent class="space-y-2">
<Button @click="resetGame" variant="destructive" class="w-full">{{ t('gmView.resetGame') }}</Button>
<Button @click="showResetConfirmDialog" variant="destructive" class="w-full">{{ t('gmView.resetGame') }}</Button>
</CardContent>
</Card>
<!-- Reset Game 确认对话框 -->
<AlertDialog :open="resetDialogOpen" @update:open="handleResetDialogClose">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{{ t('gmView.resetGame') }}</AlertDialogTitle>
<AlertDialogDescription>
{{ t('gmView.resetGameConfirm') }}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel @click="handleResetCancel">{{ t('common.cancel') }}</AlertDialogCancel>
<AlertDialogAction @click="confirmResetGame">{{ t('common.confirm') }}</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<!-- AlertDialog 提示对话框 -->
<AlertDialog :open="alertDialogOpen" @update:open="alertDialogOpen = $event">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{{ alertDialogTitle }}</AlertDialogTitle>
<AlertDialogDescription v-if="alertDialogMessage" class="whitespace-pre-line">
{{ alertDialogMessage }}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogAction @click="handleAlertConfirm">{{ t('common.confirm') }}</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useGameStore } from '@/stores/gameStore'
import { useNPCStore } from '@/stores/npcStore'
import { useUniverseStore } from '@/stores/universeStore'
@@ -250,21 +300,47 @@
import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle
} from '@/components/ui/alert-dialog'
import { BuildingType, TechnologyType, ShipType, DefenseType, OfficerType } from '@/types/game'
import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic'
import { Home } from 'lucide-vue-next'
const router = useRouter()
const gameStore = useGameStore()
const npcStore = useNPCStore()
const universeStore = useUniverseStore()
const { t } = useI18n()
const { BUILDINGS, TECHNOLOGIES, SHIPS, DEFENSES, OFFICERS } = useGameConfig()
const goHome = () => {
router.push('/')
}
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>)
const selectedNPCId = ref<string>(npcStore.npcs[0]?.id || '')
const targetPlanetIndex = ref<string>('0')
// AlertDialog 状态
const alertDialogOpen = ref(false)
const alertDialogTitle = ref('')
const alertDialogMessage = ref('')
const alertDialogCallback = ref<(() => void) | null>(null)
// Reset Dialog 状态
const resetDialogOpen = ref(false)
// 初始化军官天数显示
Object.values(OfficerType).forEach(officer => {
const officerData = gameStore.player.officers[officer]
@@ -354,17 +430,63 @@
}
}
const resetGame = () => {
if (confirm(t('gmView.resetGameConfirm'))) {
// 显示重置游戏确认对话框
const showResetConfirmDialog = () => {
// 暂停游戏
gameStore.isPaused = true
resetDialogOpen.value = true
}
// 处理重置对话框关闭
const handleResetDialogClose = (open: boolean) => {
if (!open) {
// 如果对话框关闭,恢复游戏
gameStore.isPaused = false
}
resetDialogOpen.value = open
}
// 取消重置
const handleResetCancel = () => {
resetDialogOpen.value = false
gameStore.isPaused = false
}
// 确认重置游戏
const confirmResetGame = () => {
gameStore.isPaused = true
resetDialogOpen.value = false
try {
gameStore.player.isGMEnabled = false
localStorage.clear()
location.reload()
} catch (error) {
console.error('Failed to reset game:', error)
window.location.reload()
}
}
// 显示AlertDialog的辅助函数
const showAlert = (title: string, message: string, callback?: () => void) => {
alertDialogTitle.value = title
alertDialogMessage.value = message
alertDialogCallback.value = callback || null
alertDialogOpen.value = true
}
// AlertDialog确认处理
const handleAlertConfirm = () => {
alertDialogOpen.value = false
if (alertDialogCallback.value) {
alertDialogCallback.value()
alertDialogCallback.value = null
}
}
// NPC测试函数
const testNPCSpy = () => {
if (!selectedNPC.value) {
alert(t('gmView.selectNPCFirst') || 'Please select an NPC first')
showAlert(t('gmView.selectNPCFirst') || 'Please select an NPC first', '')
return
}
@@ -376,17 +498,18 @@
)
if (mission) {
// 加速任务到5秒后到达
npcBehaviorLogic.accelerateNPCMission(selectedNPC.value, mission.id, 5)
alert(`${selectedNPC.value.name} will spy in 5 seconds`)
showAlert(t('gmView.npcWillSpyIn5s', { npcName: selectedNPC.value.name }), t('gmView.testSpyMessage'), () => {
// 加速任务到5秒后到达在确认后执行这样时间更准确
npcBehaviorLogic.accelerateNPCMission(selectedNPC.value!, mission.id, 5, gameStore.player)
})
} else {
alert(t('gmView.npcNoProbes') || 'NPC does not have spy probes')
showAlert(t('gmView.npcNoProbes') || 'NPC does not have spy probes', '')
}
}
const testNPCAttack = () => {
if (!selectedNPC.value) {
alert(t('gmView.selectNPCFirst') || 'Please select an NPC first')
showAlert(t('gmView.selectNPCFirst') || 'Please select an NPC first', '')
return
}
@@ -398,17 +521,18 @@
)
if (mission) {
// 加速任务到5秒后到达
npcBehaviorLogic.accelerateNPCMission(selectedNPC.value, mission.id, 5)
alert(`${selectedNPC.value.name} will attack in 5 seconds`)
showAlert(t('gmView.npcWillAttackIn5s', { npcName: selectedNPC.value.name }), t('gmView.testAttackMessage'), () => {
// 加速任务到5秒后到达在确认后执行这样时间更准确
npcBehaviorLogic.accelerateNPCMission(selectedNPC.value!, mission.id, 5, gameStore.player)
})
} else {
alert(t('gmView.npcNoSpyReport') || 'NPC needs to spy first')
showAlert(t('gmView.npcNoSpyReport') || 'NPC needs to spy first', '')
}
}
const testNPCSpyAndAttack = () => {
if (!selectedNPC.value) {
alert(t('gmView.selectNPCFirst') || 'Please select an NPC first')
showAlert(t('gmView.selectNPCFirst') || 'Please select an NPC first', '')
return
}
@@ -420,36 +544,37 @@
)
if (spyMission && attackMission) {
// 加速任务侦查5秒后到达攻击10秒后到达
npcBehaviorLogic.accelerateNPCMission(selectedNPC.value, spyMission.id, 5)
npcBehaviorLogic.accelerateNPCMission(selectedNPC.value, attackMission.id, 10)
alert(`${selectedNPC.value.name} will spy in 5s and attack in 10s`)
showAlert(t('gmView.npcWillSpyAndAttack', { npcName: selectedNPC.value.name }), t('gmView.testSpyAndAttackMessage'), () => {
// 加速任务侦查5秒后到达攻击10秒后到达在确认后执行这样时间更准确
npcBehaviorLogic.accelerateNPCMission(selectedNPC.value!, spyMission.id, 5, gameStore.player)
npcBehaviorLogic.accelerateNPCMission(selectedNPC.value!, attackMission.id, 10, gameStore.player)
})
} else {
alert(t('gmView.npcMissionFailed') || 'Failed to create missions')
showAlert(t('gmView.npcMissionFailed') || 'Failed to create missions', '')
}
}
const accelerateAllMissions = () => {
if (!selectedNPC.value) {
alert(t('gmView.selectNPCFirst') || 'Please select an NPC first')
showAlert(t('gmView.selectNPCFirst') || 'Please select an NPC first', '')
return
}
const count = npcBehaviorLogic.accelerateAllNPCMissions(selectedNPC.value, 5)
alert(`Accelerated ${count} missions to 5 seconds`)
const count = npcBehaviorLogic.accelerateAllNPCMissions(selectedNPC.value, 5, gameStore.player)
showAlert(t('gmView.acceleratedMissions', { count }), '')
}
// 初始化NPC舰队
const initializeNPCFleet = () => {
if (!selectedNPC.value) {
alert(t('gmView.selectNPCFirst') || 'Please select an NPC first')
showAlert(t('gmView.selectNPCFirst') || 'Please select an NPC first', '')
return
}
// 给NPC的第一个星球添加基础舰队
const npcPlanet = selectedNPC.value.planets[0]
if (!npcPlanet) {
alert('NPC has no planets')
showAlert(t('gmView.npcNoPlanets'), '')
return
}
@@ -465,8 +590,6 @@
npcPlanet.fleet[ShipType.Destroyer] = (npcPlanet.fleet[ShipType.Destroyer] || 0) + 30
npcPlanet.fleet[ShipType.Battlecruiser] = (npcPlanet.fleet[ShipType.Battlecruiser] || 0) + 20
alert(
`${selectedNPC.value.name} fleet initialized:\n- 100 Spy Probes\n- 500 Light Fighters\n- 300 Heavy Fighters\n- 200 Cruisers\n- 100 Battleships\n- 50 Bombers\n- 30 Destroyers\n- 20 Battlecruisers`
)
showAlert(t('gmView.npcFleetInitialized', { npcName: selectedNPC.value.name }), t('gmView.npcFleetDetails'))
}
</script>

View File

@@ -162,7 +162,7 @@
<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="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4 p-2 sm:p-3 border rounded-lg hover:bg-muted/50 transition-colors"
:class="{
// 空位置
'bg-muted/30': !slot.planet,
@@ -192,38 +192,81 @@
(!getRelation(slot.planet) || getRelation(slot.planet)?.status === RelationStatus.Neutral)
}"
>
<!-- 位置编号 -->
<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">
<!-- 移动端:垂直布局 / PC端水平布局 -->
<div class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2">
<!-- 星球名称和坐标 -->
<div class="flex items-baseline gap-1.5 min-w-0">
<h3 class="font-semibold text-sm sm:text-base truncate">{{ slot.planet.name }}</h3>
<span class="text-xs text-muted-foreground whitespace-nowrap flex-shrink-0 sm:hidden">
[{{ slot.planet.position.galaxy }}:{{ slot.planet.position.system }}:{{ slot.planet.position.position }}]
</span>
<!-- 移动端布局 -->
<div class="sm:hidden w-full space-y-2">
<!-- 第一行:位置编号 + 星球信息(名称、坐标、状态、残骸) -->
<div class="flex items-start gap-2 w-full">
<!-- 位置编号 -->
<div class="w-8 text-center flex-shrink-0">
<Badge variant="outline" class="text-xs">{{ 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-1.5 min-w-0 flex-wrap">
<h3 class="font-semibold text-sm truncate">{{ slot.planet.name }}</h3>
<span class="text-xs text-muted-foreground whitespace-nowrap flex-shrink-0">
[{{ slot.planet.position.galaxy }}:{{ slot.planet.position.system }}:{{ slot.planet.position.position }}]
</span>
<Badge v-if="isMyPlanet(slot.planet)" variant="default" class="text-xs flex-shrink-0">
{{ t('galaxyView.mine') }}
</Badge>
<Badge v-else :variant="getRelationBadgeVariant(slot.planet)" class="text-xs flex-shrink-0">
{{ getRelationStatusText(slot.planet) }}
</Badge>
<Popover v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)">
<PopoverTrigger as-child>
<Badge
variant="outline"
class="text-xs cursor-pointer hover:bg-amber-50 dark:hover:bg-amber-950/30 border-amber-300 dark:border-amber-700 text-amber-700 dark:text-amber-400 gap-1"
>
<Recycle class="h-3 w-3" />
</Badge>
</PopoverTrigger>
<PopoverContent class="w-auto p-3" side="top" align="start">
<div class="space-y-2">
<p class="text-xs font-semibold text-amber-700 dark:text-amber-400">{{ t('galaxyView.debrisField') }}</p>
<div class="space-y-1 text-xs">
<div class="flex items-center gap-2">
<ResourceIcon type="metal" size="sm" />
<span class="text-muted-foreground">{{ t('resources.metal') }}:</span>
<span class="font-medium">
{{ formatNumber(getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)!.resources.metal) }}
</span>
</div>
<div class="flex items-center gap-2">
<ResourceIcon type="crystal" size="sm" />
<span class="text-muted-foreground">{{ t('resources.crystal') }}:</span>
<span class="font-medium">
{{ formatNumber(getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)!.resources.crystal) }}
</span>
</div>
</div>
</div>
</PopoverContent>
</Popover>
</div>
<!-- 第二行:好感度 -->
<div v-if="!isMyPlanet(slot.planet) && getReputationValue(slot.planet) !== null" class="text-xs">
<span class="text-muted-foreground">{{ t('diplomacy.reputation') }}:</span>
<span class="ml-1 font-semibold" :class="getReputationColor(getReputationValue(slot.planet))">
{{ getReputationValue(slot.planet)! > 0 ? '+' : '' }}{{ getReputationValue(slot.planet) }}
</span>
</div>
</div>
<!-- 徽章组 -->
<div class="flex items-center gap-2 flex-wrap">
<Badge v-if="isMyPlanet(slot.planet)" variant="default" class="text-xs">{{ t('galaxyView.mine') }}</Badge>
<Badge v-else :variant="getRelationBadgeVariant(slot.planet)" class="text-xs">
{{ getRelationStatusText(slot.planet) }}
</Badge>
<!-- 残骸场徽章 - 紧凑显示 -->
<!-- 空位置 -->
<div v-else class="space-y-1">
<div class="text-sm text-muted-foreground">{{ t('galaxyView.emptySlot') }}</div>
<!-- 残骸场徽章 - 空位置时也显示 -->
<Popover v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)">
<PopoverTrigger as-child>
<Badge
variant="outline"
class="text-xs cursor-pointer hover:bg-amber-50 dark:hover:bg-amber-950/30 border-amber-300 dark:border-amber-700 text-amber-700 dark:text-amber-400 gap-1"
class="text-xs cursor-pointer hover:bg-amber-50 dark:hover:bg-amber-950/30 border-amber-300 dark:border-amber-700 text-amber-700 dark:text-amber-400 gap-1 inline-flex"
>
<Recycle class="h-3 w-3" />
<span class="hidden sm:inline">{{ t('galaxyView.debris') }}</span>
<span>{{ t('galaxyView.debris') }}</span>
</Badge>
</PopoverTrigger>
<PopoverContent class="w-auto p-3" side="top" align="start">
@@ -250,59 +293,195 @@
</Popover>
</div>
</div>
<!-- PC端显示坐标 -->
<p class="text-xs text-muted-foreground hidden sm:block">
[{{ slot.planet.position.galaxy }}:{{ slot.planet.position.system }}:{{ slot.planet.position.position }}]
</p>
<!-- 好感度显示仅NPC星球 -->
<div v-if="!isMyPlanet(slot.planet) && getReputationValue(slot.planet) !== null" class="text-xs">
<span class="text-muted-foreground">{{ t('diplomacy.reputation') }}:</span>
<span class="ml-1 font-semibold" :class="getReputationColor(getReputationValue(slot.planet))">
{{ getReputationValue(slot.planet)! > 0 ? '+' : '' }}{{ getReputationValue(slot.planet) }}
</span>
</div>
</div>
<!-- 空位置 -->
<div v-else class="space-y-1">
<div class="text-sm text-muted-foreground">{{ t('galaxyView.emptySlot') }}</div>
<!-- 残骸场徽章 - 空位置时也显示 -->
<Popover v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)">
<PopoverTrigger as-child>
<Badge
variant="outline"
class="text-xs cursor-pointer hover:bg-amber-50 dark:hover:bg-amber-950/30 border-amber-300 dark:border-amber-700 text-amber-700 dark:text-amber-400 gap-1 inline-flex"
>
<Recycle class="h-3 w-3" />
<span>{{ t('galaxyView.debris') }}</span>
</Badge>
</PopoverTrigger>
<PopoverContent class="w-auto p-3" side="top" align="start">
<div class="space-y-2">
<p class="text-xs font-semibold text-amber-700 dark:text-amber-400">{{ t('galaxyView.debrisField') }}</p>
<div class="space-y-1 text-xs">
<div class="flex items-center gap-2">
<ResourceIcon type="metal" size="sm" />
<span class="text-muted-foreground">{{ t('resources.metal') }}:</span>
<span class="font-medium">
{{ formatNumber(getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)!.resources.metal) }}
</span>
</div>
<div class="flex items-center gap-2">
<ResourceIcon type="crystal" size="sm" />
<span class="text-muted-foreground">{{ t('resources.crystal') }}:</span>
<span class="font-medium">
{{ formatNumber(getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)!.resources.crystal) }}
</span>
</div>
</div>
</div>
</PopoverContent>
</Popover>
<!-- 第三行:操作按钮 -->
<div class="flex gap-1 pl-10">
<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" />
</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" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ t('galaxyView.attack') }}</p>
</TooltipContent>
</Tooltip>
<Tooltip v-if="slot.planet && !isMyPlanet(slot.planet) && hasInterplanetaryMissiles">
<TooltipTrigger as-child>
<Button @click="showMissileAttackDialog(slot.planet)" variant="outline" size="sm" class="h-8 w-8 p-0">
<Bomb class="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ t('galaxyView.missileAttack') }}</p>
</TooltipContent>
</Tooltip>
<Tooltip v-if="slot.planet && !isMyPlanet(slot.planet) && getPlanetNPC(slot.planet)">
<TooltipTrigger as-child>
<Button @click="showPlanetActions(slot.planet, 'gift')" variant="outline" size="sm" class="h-8 w-8 p-0">
<Gift class="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ t('galaxyView.sendGift') }}</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" />
</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" />
</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" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ t('galaxyView.recycle') }}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
<!-- 操作按钮 -->
<div class="flex gap-1 sm:gap-2 flex-shrink-0">
<!-- PC端布局位置编号 + 星球信息(水平) -->
<div class="hidden sm:flex items-center gap-4 flex-1 min-w-0">
<!-- 位置编号 -->
<div class="w-12 text-center flex-shrink-0">
<Badge variant="outline" class="text-sm">{{ slot.position }}</Badge>
</div>
<!-- 星球信息 -->
<div class="flex-1 min-w-0">
<div v-if="slot.planet" class="space-y-1">
<!-- PC端标题和徽章 -->
<div class="flex items-center gap-2 flex-wrap">
<h3 class="font-semibold text-base">{{ slot.planet.name }}</h3>
<Badge v-if="isMyPlanet(slot.planet)" variant="default" class="text-xs">{{ t('galaxyView.mine') }}</Badge>
<Badge v-else :variant="getRelationBadgeVariant(slot.planet)" class="text-xs">
{{ getRelationStatusText(slot.planet) }}
</Badge>
<!-- 残骸场徽章 -->
<Popover v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)">
<PopoverTrigger as-child>
<Badge
variant="outline"
class="text-xs cursor-pointer hover:bg-amber-50 dark:hover:bg-amber-950/30 border-amber-300 dark:border-amber-700 text-amber-700 dark:text-amber-400 gap-1"
>
<Recycle class="h-3 w-3" />
<span>{{ t('galaxyView.debris') }}</span>
</Badge>
</PopoverTrigger>
<PopoverContent class="w-auto p-3" side="top" align="start">
<div class="space-y-2">
<p class="text-xs font-semibold text-amber-700 dark:text-amber-400">{{ t('galaxyView.debrisField') }}</p>
<div class="space-y-1 text-xs">
<div class="flex items-center gap-2">
<ResourceIcon type="metal" size="sm" />
<span class="text-muted-foreground">{{ t('resources.metal') }}:</span>
<span class="font-medium">
{{ formatNumber(getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)!.resources.metal) }}
</span>
</div>
<div class="flex items-center gap-2">
<ResourceIcon type="crystal" size="sm" />
<span class="text-muted-foreground">{{ t('resources.crystal') }}:</span>
<span class="font-medium">
{{ formatNumber(getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)!.resources.crystal) }}
</span>
</div>
</div>
</div>
</PopoverContent>
</Popover>
</div>
<!-- PC端坐标 -->
<p class="text-xs text-muted-foreground">
[{{ slot.planet.position.galaxy }}:{{ slot.planet.position.system }}:{{ slot.planet.position.position }}]
</p>
<!-- PC端好感度显示仅NPC星球 -->
<div v-if="!isMyPlanet(slot.planet) && getReputationValue(slot.planet) !== null" class="text-xs">
<span class="text-muted-foreground">{{ t('diplomacy.reputation') }}:</span>
<span class="ml-1 font-semibold" :class="getReputationColor(getReputationValue(slot.planet))">
{{ getReputationValue(slot.planet)! > 0 ? '+' : '' }}{{ getReputationValue(slot.planet) }}
</span>
</div>
</div>
<!-- 空位置 -->
<div v-else class="space-y-1">
<div class="text-sm text-muted-foreground">{{ t('galaxyView.emptySlot') }}</div>
<!-- 残骸场徽章 - 空位置时也显示 -->
<Popover v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)">
<PopoverTrigger as-child>
<Badge
variant="outline"
class="text-xs cursor-pointer hover:bg-amber-50 dark:hover:bg-amber-950/30 border-amber-300 dark:border-amber-700 text-amber-700 dark:text-amber-400 gap-1 inline-flex"
>
<Recycle class="h-3 w-3" />
<span>{{ t('galaxyView.debris') }}</span>
</Badge>
</PopoverTrigger>
<PopoverContent class="w-auto p-3" side="top" align="start">
<div class="space-y-2">
<p class="text-xs font-semibold text-amber-700 dark:text-amber-400">{{ t('galaxyView.debrisField') }}</p>
<div class="space-y-1 text-xs">
<div class="flex items-center gap-2">
<ResourceIcon type="metal" size="sm" />
<span class="text-muted-foreground">{{ t('resources.metal') }}:</span>
<span class="font-medium">
{{ formatNumber(getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)!.resources.metal) }}
</span>
</div>
<div class="flex items-center gap-2">
<ResourceIcon type="crystal" size="sm" />
<span class="text-muted-foreground">{{ t('resources.crystal') }}:</span>
<span class="font-medium">
{{ formatNumber(getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)!.resources.crystal) }}
</span>
</div>
</div>
</div>
</PopoverContent>
</Popover>
</div>
</div>
</div>
<!-- 操作按钮 (PC端) -->
<div class="hidden sm:flex gap-1 sm:gap-2 flex-shrink-0">
<TooltipProvider :delay-duration="300">
<Tooltip v-if="slot.planet && !isMyPlanet(slot.planet)">
<TooltipTrigger as-child>
@@ -324,6 +503,16 @@
<p>{{ t('galaxyView.attack') }}</p>
</TooltipContent>
</Tooltip>
<Tooltip v-if="slot.planet && !isMyPlanet(slot.planet) && hasInterplanetaryMissiles">
<TooltipTrigger as-child>
<Button @click="showMissileAttackDialog(slot.planet)" variant="outline" size="sm" class="h-8 w-8 p-0">
<Bomb class="h-3 w-3 sm:h-4 sm:w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ t('galaxyView.missileAttack') }}</p>
</TooltipContent>
</Tooltip>
<Tooltip v-if="slot.planet && !isMyPlanet(slot.planet) && getPlanetNPC(slot.planet)">
<TooltipTrigger as-child>
<Button @click="showPlanetActions(slot.planet, 'gift')" variant="outline" size="sm" class="h-8 w-8 p-0">
@@ -376,6 +565,60 @@
</CardContent>
</Card>
<!-- 导弹攻击对话框 -->
<Dialog :open="missileDialogOpen" @update:open="missileDialogOpen = $event">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ t('galaxyView.missileAttackTitle') }}</DialogTitle>
<DialogDescription v-if="missileTargetPlanet">
{{
t('galaxyView.missileAttackMessage').replace(
'{coordinates}',
`${missileTargetPlanet.position.galaxy}:${missileTargetPlanet.position.system}:${missileTargetPlanet.position.position}`
)
}}
</DialogDescription>
</DialogHeader>
<div v-if="gameStore.currentPlanet && missileTargetPlanet" class="space-y-4">
<!-- 导弹数量输入 -->
<div class="space-y-2">
<Label>{{ t('galaxyView.missileCount') }}</Label>
<Input
v-model.number="missileCount"
type="number"
min="1"
:max="gameStore.currentPlanet.defense['interplanetaryMissile'] || 0"
/>
<p class="text-sm text-muted-foreground">
{{ t('galaxyView.availableMissiles') }}: {{ gameStore.currentPlanet.defense['interplanetaryMissile'] || 0 }}
</p>
</div>
<!-- 射程和距离信息 -->
<div class="space-y-2 text-sm">
<div class="flex justify-between">
<span class="text-muted-foreground">{{ t('galaxyView.missileRange') }}:</span>
<span>{{ calculateMissileRange() }} {{ t('galaxyView.systems') }}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">{{ t('galaxyView.distance') }}:</span>
<span>{{ calculateDistance(missileTargetPlanet) }} {{ t('galaxyView.systems') }}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">{{ t('galaxyView.flightTime') }}:</span>
<span>{{ formatFlightTime(calculateDistance(missileTargetPlanet)) }}</span>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" @click="missileDialogOpen = false">{{ t('galaxyView.cancel') }}</Button>
<Button @click="launchMissileAttack">{{ t('galaxyView.launchMissile') }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<!-- 快速派遣对话框 -->
<AlertDialog :open="alertDialogOpen" @update:open="alertDialogOpen = $event">
<AlertDialogContent>
@@ -405,10 +648,12 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Label } from '@/components/ui/label'
import { Input } from '@/components/ui/input'
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 { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import {
AlertDialog,
AlertDialogAction,
@@ -420,7 +665,7 @@
AlertDialogTitle
} from '@/components/ui/alert-dialog'
import ResourceIcon from '@/components/ResourceIcon.vue'
import { Home, Eye, Sword, Rocket, Recycle, Gift, Globe } from 'lucide-vue-next'
import { Home, Eye, Sword, Rocket, Recycle, Gift, Globe, Bomb } from 'lucide-vue-next'
import { useRouter, useRoute } from 'vue-router'
import * as gameLogic from '@/logic/gameLogic'
import { formatNumber } from '@/utils/format'
@@ -438,6 +683,11 @@
const alertDialogMessage = ref('')
const alertDialogConfirmAction = ref<(() => void) | null>(null)
// 导弹攻击对话框状态
const missileDialogOpen = ref(false)
const missileTargetPlanet = ref<Planet | null>(null)
const missileCount = ref(1)
const selectedGalaxy = ref(1)
const selectedSystem = ref(1)
const currentGalaxy = ref(1)
@@ -465,6 +715,12 @@
return gameStore.player.planets.filter(p => !p.isMoon)
})
// 检查当前星球是否有星际导弹
const hasInterplanetaryMissiles = computed(() => {
if (!gameStore.currentPlanet) return false
return (gameStore.currentPlanet.defense['interplanetaryMissile'] || 0) > 0
})
// 判断当前是否在母星所在星系
const isInHomePlanetSystem = computed(() => {
if (!homePlanet.value) return false
@@ -680,4 +936,80 @@
}
alertDialogOpen.value = true
}
// 显示导弹攻击对话框
const showMissileAttackDialog = (planet: Planet) => {
missileTargetPlanet.value = planet
missileCount.value = 1
missileDialogOpen.value = true
}
// 执行导弹攻击
const launchMissileAttack = () => {
if (!missileTargetPlanet.value || !gameStore.currentPlanet) return
// 导入missileLogic进行验证和执行
import('@/logic/missileLogic').then(missileLogic => {
const validation = missileLogic.validateMissileLaunch(
gameStore.currentPlanet!,
missileTargetPlanet.value!.position,
missileCount.value,
gameStore.player.technologies
)
if (!validation.valid) {
alertDialogTitle.value = t('errors.launchFailed')
alertDialogMessage.value = t(validation.reason || 'errors.unknown')
alertDialogOpen.value = true
return
}
// 创建导弹攻击任务
const missileAttack = missileLogic.createMissileAttack(
gameStore.player.id,
gameStore.currentPlanet!,
missileTargetPlanet.value!.position,
missileTargetPlanet.value!.id,
missileCount.value
)
// 扣除导弹
missileLogic.executeMissileLaunch(gameStore.currentPlanet!, missileCount.value)
// 添加到玩家的导弹攻击列表
gameStore.player.missileAttacks.push(missileAttack)
// 关闭对话框
missileDialogOpen.value = false
// 显示成功提示
alertDialogTitle.value = t('common.success')
alertDialogMessage.value = t('galaxyView.missileLaunched')
alertDialogOpen.value = true
})
}
// 计算导弹射程
const calculateMissileRange = () => {
const impulseDriveLevel = gameStore.player.technologies['impulseDrive'] || 0
if (impulseDriveLevel === 0) return 0
return 5 * impulseDriveLevel - 1
}
// 计算到目标的距离
const calculateDistance = (target: Planet) => {
if (!gameStore.currentPlanet) return 0
const from = gameStore.currentPlanet.position
const to = target.position
if (from.galaxy !== to.galaxy) return Infinity
return Math.abs(from.system - to.system)
}
// 格式化飞行时间
const formatFlightTime = (distance: number) => {
const seconds = 30 + distance * 60
const minutes = Math.floor(seconds / 60)
const secs = seconds % 60
return `${minutes}:${secs.toString().padStart(2, '0')}`
}
</script>

View File

@@ -580,7 +580,8 @@
[MissionType.Colonize]: t('fleetView.colonize'),
[MissionType.Deploy]: t('fleetView.deploy'),
[MissionType.Recycle]: t('fleetView.recycle'),
[MissionType.Destroy]: t('fleetView.destroy')
[MissionType.Destroy]: t('fleetView.destroy'),
[MissionType.MissileAttack]: t('galaxyView.missileAttack')
}
return typeMap[missionType] || missionType
}
@@ -615,7 +616,7 @@
const acceptGift = (gift: GiftNotification) => {
const npc = npcStore.npcs.find(n => n.id === gift.fromNpcId)
if (npc) {
diplomaticLogic.acceptNPCGift(gameStore.player, npc, gift)
diplomaticLogic.acceptNPCGift(gameStore.player, npc, gift, gameStore.locale)
}
}
@@ -623,7 +624,7 @@
const rejectGift = (gift: GiftNotification) => {
const npc = npcStore.npcs.find(n => n.id === gift.fromNpcId)
if (npc) {
diplomaticLogic.rejectNPCGift(gameStore.player, npc, gift)
diplomaticLogic.rejectNPCGift(gameStore.player, npc, gift, gameStore.locale)
}
}

View File

@@ -0,0 +1,34 @@
<template>
<div class="container mx-auto p-4 sm:p-6 flex items-center justify-center min-h-[60vh]">
<Empty class="border-0">
<EmptyMedia>
<div class="text-8xl sm:text-9xl font-bold text-muted-foreground/20">404</div>
</EmptyMedia>
<EmptyHeader>
<EmptyTitle>{{ t('notFound.title') }}</EmptyTitle>
<EmptyDescription>{{ t('notFound.description') }}</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button @click="goHome" size="lg">
<Home class="mr-2 h-4 w-4" />
{{ t('notFound.goHome') }}
</Button>
</EmptyContent>
</Empty>
</div>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { useI18n } from '@/composables/useI18n'
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty'
import { Button } from '@/components/ui/button'
import { Home } from 'lucide-vue-next'
const router = useRouter()
const { t } = useI18n()
const goHome = () => {
router.push('/')
}
</script>

View File

@@ -59,6 +59,22 @@
<CardDescription>{{ t('settings.gameSettingsDesc') }}</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
<!-- 游戏倍率 -->
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between p-4 border rounded-lg gap-3">
<div class="space-y-1 flex-1">
<h3 class="font-medium">{{ t('settings.gameSpeed') }}</h3>
<p class="text-sm text-muted-foreground">{{ t('settings.gameSpeedDesc') }}</p>
</div>
<div class="flex items-center gap-2 sm:gap-4 w-full sm:w-auto">
<div class="flex items-center gap-2 flex-1 sm:flex-initial">
<Button @click="decreaseSpeed" variant="outline" size="sm" :disabled="gameStore.gameSpeed <= 0.5">-</Button>
<span class="min-w-[60px] text-center font-medium">{{ gameStore.gameSpeed || 1 }}x</span>
<Button @click="increaseSpeed" variant="outline" size="sm" :disabled="gameStore.gameSpeed >= 10">+</Button>
</div>
<Button @click="resetSpeed" variant="ghost" size="sm">{{ t('settings.reset') }}</Button>
</div>
</div>
<!-- 游戏暂停 -->
<div class="flex items-center justify-between p-4 border rounded-lg">
<div class="space-y-1">
@@ -88,6 +104,15 @@
<span class="text-muted-foreground">{{ t('settings.buildDate') }}:</span>
<span class="font-medium">{{ pkg.buildDate }}</span>
</div>
<!-- 检查更新按钮 -->
<div class="pt-2">
<Button @click="handleCheckVersion" variant="outline" size="sm" :disabled="isCheckingVersion || !canCheck" class="w-full">
<RefreshCw class="mr-2 h-4 w-4" :class="{ 'animate-spin': isCheckingVersion }" />
<template v-if="isCheckingVersion">{{ t('settings.checking') }}</template>
<template v-else-if="!canCheck && cooldownTime">{{ cooldownTime }}</template>
<template v-else>{{ t('settings.checkUpdate') }}</template>
</Button>
</div>
</div>
<!-- 社区链接 -->
@@ -127,11 +152,14 @@
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<!-- 更新对话框 -->
<UpdateDialog v-model:open="showUpdateDialog" :version-info="updateInfo" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useGameStore } from '@/stores/gameStore'
import { useI18n } from '@/composables/useI18n'
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
@@ -146,23 +174,64 @@
AlertDialogHeader,
AlertDialogTitle
} from '@/components/ui/alert-dialog'
import { Download, Upload, Trash2, ExternalLink, MessagesSquare, Play, Pause } from 'lucide-vue-next'
import { Download, Upload, Trash2, ExternalLink, MessagesSquare, Play, Pause, RefreshCw } from 'lucide-vue-next'
import { saveAs } from 'file-saver'
import { toast } from 'vue-sonner'
import pkg from '../../package.json'
import 'vue-sonner/style.css'
import { checkLatestVersion, canCheckVersion } from '@/utils/versionCheck'
import type { VersionInfo } from '@/utils/versionCheck'
import UpdateDialog from '@/components/UpdateDialog.vue'
const { t } = useI18n()
const gameStore = useGameStore()
const fileInputRef = ref<HTMLInputElement>()
const isExporting = ref(false)
const isCheckingVersion = ref(false)
const cooldownTime = ref('')
const showConfirmDialog = ref(false)
const confirmTitle = ref('')
const confirmMessage = ref('')
let confirmCallback: (() => void) | null = null
// 计算是否可以检查版本主动检测5分钟内不能重复检查
const canCheck = computed(() => canCheckVersion(gameStore.player.lastManualUpdateCheck || 0))
// 计算剩余冷却时间
const updateCooldownTime = () => {
if (canCheck.value) {
cooldownTime.value = ''
return
}
const lastCheck = gameStore.player.lastManualUpdateCheck || 0
const now = Date.now()
const fiveMinutes = 5 * 60 * 1000
const timePassed = now - lastCheck
const timeRemaining = fiveMinutes - timePassed
if (timeRemaining <= 0) {
cooldownTime.value = ''
return
}
const minutes = Math.floor(timeRemaining / 60000)
const seconds = Math.floor((timeRemaining % 60000) / 1000)
cooldownTime.value = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
}
// 每秒更新倒计时
let cooldownInterval: ReturnType<typeof setInterval> | null = null
onMounted(() => {
updateCooldownTime()
cooldownInterval = setInterval(updateCooldownTime, 1000)
})
onUnmounted(() => {
if (cooldownInterval) clearInterval(cooldownInterval)
})
const openGithub = () => {
window.open(`https://github.com/${pkg.author.name}/${pkg.name}`, '_blank')
}
@@ -171,6 +240,32 @@
window.open(`https://qm.qq.com/q/${pkg.id}`, '_blank')
}
// 手动检查版本
const showUpdateDialog = ref(false)
const updateInfo = ref<VersionInfo | null>(null)
const handleCheckVersion = async () => {
if (isCheckingVersion.value || !canCheck.value) return
isCheckingVersion.value = true
try {
const versionInfo = await checkLatestVersion(gameStore.player.lastManualUpdateCheck || 0, (time: number) => {
gameStore.player.lastManualUpdateCheck = time
})
if (versionInfo) {
updateInfo.value = versionInfo
showUpdateDialog.value = true
} else {
toast.success(t('settings.upToDate'))
}
} catch (error) {
console.error('Failed to check for updates:', error)
toast.error(t('settings.checkUpdateFailed'))
} finally {
isCheckingVersion.value = false
}
}
// 导出数据(包含游戏数据和地图数据)
const handleExport = async () => {
try {
@@ -281,10 +376,37 @@
}
const clearData = () => {
// 清除localStorage
localStorage.clear()
// 重新加载页面
window.location.reload()
gameStore.isPaused = true
try {
localStorage.clear()
window.location.reload()
} catch (error) {
console.error('Failed to clear data:', error)
// 即使出错也尝试重新加载
window.location.reload()
}
}
// 增加游戏倍率
const increaseSpeed = () => {
if (gameStore.gameSpeed < 10) {
gameStore.gameSpeed = Math.min(10, gameStore.gameSpeed + 0.5)
toast.success(t('settings.speedChanged', { speed: gameStore.gameSpeed }))
}
}
// 减少游戏倍率
const decreaseSpeed = () => {
if (gameStore.gameSpeed > 0.5) {
gameStore.gameSpeed = Math.max(0.5, gameStore.gameSpeed - 0.5)
toast.success(t('settings.speedChanged', { speed: gameStore.gameSpeed }))
}
}
// 重置游戏倍率
const resetSpeed = () => {
gameStore.gameSpeed = 1
toast.success(t('settings.speedReset'))
}
// 切换游戏暂停状态