mirror of
https://github.com/setube/ogame-vue-ts.git
synced 2026-05-12 07:55:11 +08:00
feat: 重构战报弹窗与模拟器视图,优化UI与逻辑
重构BattleReportDialog和BattleSimulatorView相关静态资源,替换旧版JS/CSS文件,提升界面一致性和交互体验。新增和优化空状态、滚动区域等通用UI组件,移除部分冗余composable,完善多语言内容。引入导弹逻辑,补充版本检测工具,提升整体代码结构和可维护性。
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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>>({
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
34
src/views/NotFoundView.vue
Normal file
34
src/views/NotFoundView.vue
Normal 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>
|
||||
@@ -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'))
|
||||
}
|
||||
|
||||
// 切换游戏暂停状态
|
||||
|
||||
Reference in New Issue
Block a user