mirror of
https://github.com/setube/ogame-vue-ts.git
synced 2026-05-12 07:55:11 +08:00
feat: 新增NPC与外交逻辑,优化UI组件结构
重构并精简了部分UI组件,移除冗余弹窗与详情组件,新增NPC相关逻辑(npcBehaviorLogic、npcGrowthLogic、npcStore等)及外交逻辑(diplomaticLogic、DiplomacyView)。完善分页、标签、复选框等通用UI组件。优化战报弹窗,调整README下载链接为相对路径,修复部分国际化内容。
This commit is contained in:
@@ -1,131 +1,137 @@
|
||||
<template>
|
||||
<div class="container mx-auto p-4 sm:p-6 space-y-6">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('simulatorView.title') }}</h1>
|
||||
|
||||
<!-- 标签切换 -->
|
||||
<div class="flex gap-2 border-b">
|
||||
<Button @click="activeTab = 'attacker'" :variant="activeTab === 'attacker' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
<Sword />
|
||||
{{ t('simulatorView.attacker') }}
|
||||
</Button>
|
||||
<Button @click="activeTab = 'defender'" :variant="activeTab === 'defender' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
<Shield />
|
||||
{{ t('simulatorView.defender') }}
|
||||
</Button>
|
||||
</div>
|
||||
<!-- 攻击方配置 -->
|
||||
<Card v-if="activeTab === 'attacker'">
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('simulatorView.attackerConfig') }}</CardTitle>
|
||||
<CardDescription>{{ t('simulatorView.attackerConfigDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<!-- 舰队配置 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-3">{{ t('simulatorView.fleet') }}</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div v-for="shipType in Object.values(ShipType)" :key="shipType" class="space-y-1">
|
||||
<Label :for="`attacker-${shipType}`" class="text-xs">{{ SHIPS[shipType].name }}</Label>
|
||||
<Input :id="`attacker-${shipType}`" v-model.number="attackerFleet[shipType]" type="number" min="0" class="h-8" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 科技等级 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-3">{{ t('simulatorView.techLevels') }}</h3>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div class="space-y-1">
|
||||
<Label for="attacker-weapon" class="text-xs">{{ t('simulatorView.weapon') }}</Label>
|
||||
<Input id="attacker-weapon" v-model.number="attackerTech.weapon" type="number" min="0" class="h-8" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="attacker-shield" class="text-xs">{{ t('simulatorView.shield') }}</Label>
|
||||
<Input id="attacker-shield" v-model.number="attackerTech.shield" type="number" min="0" class="h-8" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="attacker-armor" class="text-xs">{{ t('simulatorView.armor') }}</Label>
|
||||
<Input id="attacker-armor" v-model.number="attackerTech.armor" type="number" min="0" class="h-8" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<!-- 防守方配置 -->
|
||||
<Card v-else>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('simulatorView.defenderConfig') }}</CardTitle>
|
||||
<CardDescription>{{ t('simulatorView.defenderConfigDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<!-- 舰队配置 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-3">{{ t('simulatorView.fleet') }}</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div v-for="shipType in Object.values(ShipType)" :key="shipType" class="space-y-1">
|
||||
<Label :for="`defender-${shipType}`" class="text-xs">{{ SHIPS[shipType].name }}</Label>
|
||||
<Input :id="`defender-${shipType}`" v-model.number="defenderFleet[shipType]" type="number" min="0" class="h-8" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Tabs v-model="activeTab" class="w-full">
|
||||
<TabsList class="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="attacker">
|
||||
<Sword class="h-4 w-4 mr-2" />
|
||||
{{ t('simulatorView.attacker') }}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="defender">
|
||||
<Shield class="h-4 w-4 mr-2" />
|
||||
{{ t('simulatorView.defender') }}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<!-- 防御设施 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-3">{{ t('simulatorView.defenseStructures') }}</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div v-for="defenseType in Object.values(DefenseType)" :key="defenseType" class="space-y-1">
|
||||
<Label :for="`defense-${defenseType}`" class="text-xs">{{ DEFENSES[defenseType].name }}</Label>
|
||||
<Input :id="`defense-${defenseType}`" v-model.number="defenderDefense[defenseType]" type="number" min="0" class="h-8" />
|
||||
<!-- 攻击方配置 -->
|
||||
<TabsContent value="attacker" class="mt-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('simulatorView.attackerConfig') }}</CardTitle>
|
||||
<CardDescription>{{ t('simulatorView.attackerConfigDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<!-- 舰队配置 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-3">{{ t('simulatorView.fleet') }}</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div v-for="shipType in Object.values(ShipType)" :key="shipType" class="space-y-1">
|
||||
<Label :for="`attacker-${shipType}`" class="text-xs">{{ SHIPS[shipType].name }}</Label>
|
||||
<Input
|
||||
:id="`attacker-${shipType}`"
|
||||
:model-value="attackerFleet[shipType] ?? 0"
|
||||
@update:model-value="val => (attackerFleet[shipType] = typeof val === 'number' ? val : 0)"
|
||||
type="number"
|
||||
min="0"
|
||||
class="h-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 科技等级 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-3">{{ t('simulatorView.techLevels') }}</h3>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div v-for="techType in techTypes" :key="techType" class="space-y-1">
|
||||
<Label :for="`attacker-${techType}`" class="text-xs">{{ t(`simulatorView.${techType}`) }}</Label>
|
||||
<Input :id="`attacker-${techType}`" v-model.number="attackerTech[techType]" type="number" min="0" class="h-8" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 科技等级 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-3">{{ t('simulatorView.techLevels') }}</h3>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div class="space-y-1">
|
||||
<Label for="defender-weapon" class="text-xs">{{ t('simulatorView.weapon') }}</Label>
|
||||
<Input id="defender-weapon" v-model.number="defenderTech.weapon" type="number" min="0" class="h-8" />
|
||||
<!-- 防守方配置 -->
|
||||
<TabsContent value="defender" class="mt-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('simulatorView.defenderConfig') }}</CardTitle>
|
||||
<CardDescription>{{ t('simulatorView.defenderConfigDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<!-- 舰队配置 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-3">{{ t('simulatorView.fleet') }}</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div v-for="shipType in Object.values(ShipType)" :key="shipType" class="space-y-1">
|
||||
<Label :for="`defender-${shipType}`" class="text-xs">{{ SHIPS[shipType].name }}</Label>
|
||||
<Input
|
||||
:id="`defender-${shipType}`"
|
||||
:model-value="defenderFleet[shipType] ?? 0"
|
||||
@update:model-value="val => (defenderFleet[shipType] = typeof val === 'number' ? val : 0)"
|
||||
type="number"
|
||||
min="0"
|
||||
class="h-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="defender-shield" class="text-xs">{{ t('simulatorView.shield') }}</Label>
|
||||
<Input id="defender-shield" v-model.number="defenderTech.shield" type="number" min="0" class="h-8" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="defender-armor" class="text-xs">{{ t('simulatorView.armor') }}</Label>
|
||||
<Input id="defender-armor" v-model.number="defenderTech.armor" type="number" min="0" class="h-8" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 防守方资源 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-3">{{ t('simulatorView.defenderResources') }}</h3>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div class="space-y-1">
|
||||
<Label for="defender-metal" class="text-xs flex items-center gap-1">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
{{ t('resources.metal') }}
|
||||
</Label>
|
||||
<Input id="defender-metal" v-model.number="defenderResources.metal" type="number" min="0" class="h-8" />
|
||||
<!-- 防御设施 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-3">{{ t('simulatorView.defenseStructures') }}</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div v-for="defenseType in Object.values(DefenseType)" :key="defenseType" class="space-y-1">
|
||||
<Label :for="`defense-${defenseType}`" class="text-xs">{{ DEFENSES[defenseType].name }}</Label>
|
||||
<Input
|
||||
:id="`defense-${defenseType}`"
|
||||
:model-value="defenderDefense[defenseType] ?? 0"
|
||||
@update:model-value="val => (defenderDefense[defenseType] = typeof val === 'number' ? val : 0)"
|
||||
type="number"
|
||||
min="0"
|
||||
class="h-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="defender-crystal" class="text-xs flex items-center gap-1">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
{{ t('resources.crystal') }}
|
||||
</Label>
|
||||
<Input id="defender-crystal" v-model.number="defenderResources.crystal" type="number" min="0" class="h-8" />
|
||||
|
||||
<!-- 科技等级 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-3">{{ t('simulatorView.techLevels') }}</h3>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div v-for="techType in techTypes" :key="techType" class="space-y-1">
|
||||
<Label :for="`defender-${techType}`" class="text-xs">{{ t(`simulatorView.${techType}`) }}</Label>
|
||||
<Input :id="`defender-${techType}`" v-model.number="defenderTech[techType]" type="number" min="0" class="h-8" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="defender-deuterium" class="text-xs flex items-center gap-1">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
{{ t('resources.deuterium') }}
|
||||
</Label>
|
||||
<Input id="defender-deuterium" v-model.number="defenderResources.deuterium" type="number" min="0" class="h-8" />
|
||||
|
||||
<!-- 防守方资源 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-3">{{ t('simulatorView.defenderResources') }}</h3>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div v-for="resourceType in resourceTypes" :key="resourceType.key" class="space-y-1">
|
||||
<Label :for="`defender-${resourceType.key}`" class="text-xs flex items-center gap-1">
|
||||
<ResourceIcon :type="resourceType.key" size="sm" />
|
||||
{{ t(`resources.${resourceType.key}`) }}
|
||||
</Label>
|
||||
<Input
|
||||
:id="`defender-${resourceType.key}`"
|
||||
v-model.number="defenderResources[resourceType.key]"
|
||||
type="number"
|
||||
min="0"
|
||||
class="h-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex gap-2">
|
||||
@@ -149,6 +155,7 @@
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { useGameConfig } from '@/composables/useGameConfig'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
@@ -163,19 +170,23 @@
|
||||
const { t } = useI18n()
|
||||
const { SHIPS, DEFENSES } = useGameConfig()
|
||||
|
||||
// 科技类型配置
|
||||
const techTypes = ['weapon', 'shield', 'armor'] as const
|
||||
|
||||
// 资源类型配置(用于防守方资源输入)
|
||||
const resourceTypes = [{ key: 'metal' as const }, { key: 'crystal' as const }, { key: 'deuterium' as const }]
|
||||
|
||||
// 动态初始化所有舰船类型为0
|
||||
const initializeFleet = (): Partial<Fleet> => {
|
||||
const fleet: Partial<Fleet> = {}
|
||||
Object.values(ShipType).forEach(shipType => {
|
||||
fleet[shipType] = 0
|
||||
})
|
||||
return fleet
|
||||
}
|
||||
|
||||
// 攻击方配置
|
||||
const attackerFleet = ref<Partial<Fleet>>({
|
||||
[ShipType.LightFighter]: 0,
|
||||
[ShipType.HeavyFighter]: 0,
|
||||
[ShipType.Cruiser]: 0,
|
||||
[ShipType.Battleship]: 0,
|
||||
[ShipType.SmallCargo]: 0,
|
||||
[ShipType.LargeCargo]: 0,
|
||||
[ShipType.ColonyShip]: 0,
|
||||
[ShipType.Recycler]: 0,
|
||||
[ShipType.EspionageProbe]: 0,
|
||||
[ShipType.DarkMatterHarvester]: 0
|
||||
})
|
||||
const attackerFleet = ref<Partial<Fleet>>(initializeFleet())
|
||||
|
||||
const activeTab = ref('attacker')
|
||||
|
||||
@@ -186,29 +197,18 @@
|
||||
})
|
||||
|
||||
// 防守方配置
|
||||
const defenderFleet = ref<Partial<Fleet>>({
|
||||
[ShipType.LightFighter]: 0,
|
||||
[ShipType.HeavyFighter]: 0,
|
||||
[ShipType.Cruiser]: 0,
|
||||
[ShipType.Battleship]: 0,
|
||||
[ShipType.SmallCargo]: 0,
|
||||
[ShipType.LargeCargo]: 0,
|
||||
[ShipType.ColonyShip]: 0,
|
||||
[ShipType.Recycler]: 0,
|
||||
[ShipType.EspionageProbe]: 0,
|
||||
[ShipType.DarkMatterHarvester]: 0
|
||||
})
|
||||
const defenderFleet = ref<Partial<Fleet>>(initializeFleet())
|
||||
|
||||
const defenderDefense = ref<Partial<Record<DefenseType, number>>>({
|
||||
[DefenseType.RocketLauncher]: 0,
|
||||
[DefenseType.LightLaser]: 0,
|
||||
[DefenseType.HeavyLaser]: 0,
|
||||
[DefenseType.GaussCannon]: 0,
|
||||
[DefenseType.IonCannon]: 0,
|
||||
[DefenseType.PlasmaTurret]: 0,
|
||||
[DefenseType.SmallShieldDome]: 0,
|
||||
[DefenseType.LargeShieldDome]: 0
|
||||
})
|
||||
// 动态初始化所有防御类型为0
|
||||
const initializeDefense = (): Partial<Record<DefenseType, number>> => {
|
||||
const defense: Partial<Record<DefenseType, number>> = {}
|
||||
Object.values(DefenseType).forEach(defenseType => {
|
||||
defense[defenseType] = 0
|
||||
})
|
||||
return defense
|
||||
}
|
||||
|
||||
const defenderDefense = ref<Partial<Record<DefenseType, number>>>(initializeDefense())
|
||||
|
||||
const defenderTech = ref({
|
||||
weapon: 0,
|
||||
@@ -294,15 +294,9 @@
|
||||
|
||||
// 重置模拟
|
||||
const resetSimulation = () => {
|
||||
Object.keys(attackerFleet.value).forEach(key => {
|
||||
attackerFleet.value[key as ShipType] = 0
|
||||
})
|
||||
Object.keys(defenderFleet.value).forEach(key => {
|
||||
defenderFleet.value[key as ShipType] = 0
|
||||
})
|
||||
Object.keys(defenderDefense.value).forEach(key => {
|
||||
defenderDefense.value[key as DefenseType] = 0
|
||||
})
|
||||
attackerFleet.value = initializeFleet()
|
||||
defenderFleet.value = initializeFleet()
|
||||
defenderDefense.value = initializeDefense()
|
||||
attackerTech.value = { weapon: 0, shield: 0, armor: 0 }
|
||||
defenderTech.value = { weapon: 0, shield: 0, armor: 0 }
|
||||
simulationResult.value = null
|
||||
|
||||
@@ -1,12 +1,30 @@
|
||||
<template>
|
||||
<div v-if="planet" class="container mx-auto p-4 sm:p-6">
|
||||
<div class="flex justify-between items-center mb-4 sm:mb-6 gap-2">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('buildingsView.title') }}</h1>
|
||||
<div class="text-xs sm:text-sm">
|
||||
<span class="flex items-center gap-1.5 text-muted-foreground">
|
||||
<Grid3x3 :size="14" />
|
||||
{{ getUsedSpace(planet) }} / {{ planet.maxSpace }}
|
||||
</span>
|
||||
<h1 class="text-2xl sm:text-3xl font-bold mb-4 sm:mb-6">{{ t('buildingsView.title') }}</h1>
|
||||
|
||||
<!-- 占地显示 -->
|
||||
<div class="mb-4 sm:mb-6 p-3 sm:p-4 bg-muted/50 rounded-lg border">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-sm sm:text-base font-medium flex items-center gap-2">
|
||||
<Grid3x3 :size="16" />
|
||||
{{ t('buildingsView.spaceUsage') }}:
|
||||
</div>
|
||||
<div class="text-sm sm:text-base font-bold">
|
||||
<span :class="getUsedSpace(planet) > planet.maxSpace ? 'text-destructive' : 'text-primary'">
|
||||
{{ formatNumber(getUsedSpace(planet)) }}
|
||||
</span>
|
||||
<span class="text-muted-foreground mx-1">/</span>
|
||||
<span>{{ formatNumber(planet.maxSpace) }}</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="getUsedSpace(planet) > planet.maxSpace ? 'bg-destructive' : 'bg-primary'"
|
||||
:style="{ width: `${Math.min((getUsedSpace(planet) / planet.maxSpace) * 100, 100)}%` }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,64 +34,46 @@
|
||||
<CardUnlockOverlay :requirements="BUILDINGS[buildingType].requirements" :currentLevel="getBuildingLevel(buildingType)" />
|
||||
|
||||
<CardHeader>
|
||||
<div class="flex justify-between items-start gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="mb-2">
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-2">
|
||||
<CardTitle
|
||||
class="text-base sm:text-lg cursor-pointer hover:text-primary transition-colors"
|
||||
class="text-sm sm:text-base lg:text-lg cursor-pointer hover:text-primary transition-colors underline decoration-dotted underline-offset-4 order-2 sm:order-1"
|
||||
@click="detailDialog.openBuilding(buildingType, getBuildingLevel(buildingType))"
|
||||
>
|
||||
{{ BUILDINGS[buildingType].name }}
|
||||
</CardTitle>
|
||||
<CardDescription class="text-xs sm:text-sm">{{ BUILDINGS[buildingType].description }}</CardDescription>
|
||||
<Badge variant="secondary" class="text-xs whitespace-nowrap self-start order-1 sm:order-2">
|
||||
Lv {{ getBuildingLevel(buildingType) }}
|
||||
</Badge>
|
||||
</div>
|
||||
<Badge variant="secondary" class="text-xs whitespace-nowrap flex-shrink-0">Lv {{ getBuildingLevel(buildingType) }}</Badge>
|
||||
</div>
|
||||
<CardDescription class="text-xs sm:text-sm">{{ BUILDINGS[buildingType].description }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="space-y-3">
|
||||
<div class="text-xs sm:text-sm space-y-1.5 sm:space-y-2">
|
||||
<p class="text-muted-foreground mb-1 sm:mb-2">{{ t('buildingsView.upgradeCost') }}:</p>
|
||||
<div class="space-y-1 sm:space-y-1.5">
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.metal') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="
|
||||
getResourceCostColor(planet.resources.metal, getBuildingCost(buildingType, getBuildingLevel(buildingType) + 1).metal)
|
||||
"
|
||||
>
|
||||
{{ formatNumber(getBuildingCost(buildingType, getBuildingLevel(buildingType) + 1).metal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.crystal') }}:</span>
|
||||
<div
|
||||
v-for="resourceType in costResourceTypes"
|
||||
:key="resourceType.key"
|
||||
v-show="
|
||||
resourceType.key !== 'darkMatter' || getBuildingCost(buildingType, getBuildingLevel(buildingType) + 1).darkMatter > 0
|
||||
"
|
||||
class="flex items-center gap-1.5 sm:gap-2"
|
||||
>
|
||||
<ResourceIcon :type="resourceType.key" size="sm" />
|
||||
<span class="text-xs">{{ t(`resources.${resourceType.key}`) }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="
|
||||
getResourceCostColor(
|
||||
planet.resources.crystal,
|
||||
getBuildingCost(buildingType, getBuildingLevel(buildingType) + 1).crystal
|
||||
planet.resources[resourceType.key],
|
||||
getBuildingCost(buildingType, getBuildingLevel(buildingType) + 1)[resourceType.key]
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ formatNumber(getBuildingCost(buildingType, getBuildingLevel(buildingType) + 1).crystal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.deuterium') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="
|
||||
getResourceCostColor(
|
||||
planet.resources.deuterium,
|
||||
getBuildingCost(buildingType, getBuildingLevel(buildingType) + 1).deuterium
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ formatNumber(getBuildingCost(buildingType, getBuildingLevel(buildingType) + 1).deuterium) }}
|
||||
{{ formatNumber(getBuildingCost(buildingType, getBuildingLevel(buildingType) + 1)[resourceType.key]) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,6 +113,9 @@
|
||||
<span>{{ formatNumber(getDemolishRefund(buildingType).metal) }} {{ t('resources.metal') }}</span>
|
||||
<span>{{ formatNumber(getDemolishRefund(buildingType).crystal) }} {{ t('resources.crystal') }}</span>
|
||||
<span>{{ formatNumber(getDemolishRefund(buildingType).deuterium) }} {{ t('resources.deuterium') }}</span>
|
||||
<span v-if="getDemolishRefund(buildingType).darkMatter > 0">
|
||||
{{ formatNumber(getDemolishRefund(buildingType).darkMatter) }} {{ t('resources.darkMatter') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -121,7 +124,35 @@
|
||||
</div>
|
||||
|
||||
<!-- 提示对话框 -->
|
||||
<AlertDialog ref="alertDialog" />
|
||||
<AlertDialog :open="alertDialogOpen" @update:open="alertDialogOpen = $event">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ alertDialogTitle }}</AlertDialogTitle>
|
||||
<AlertDialogDescription class="whitespace-pre-line">
|
||||
{{ alertDialogMessage }}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction>{{ t('common.confirm') }}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<!-- 拆除确认对话框 -->
|
||||
<AlertDialog :open="demolishConfirmOpen" @update:open="demolishConfirmOpen = $event">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ t('buildingsView.confirmDemolish') }}</AlertDialogTitle>
|
||||
<AlertDialogDescription class="whitespace-pre-line">
|
||||
{{ demolishConfirmMessage }}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{{ t('common.cancel') }}</AlertDialogCancel>
|
||||
<AlertDialogAction @click="confirmDemolish">{{ t('common.confirm') }}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -138,24 +169,51 @@
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import CardUnlockOverlay from '@/components/CardUnlockOverlay.vue'
|
||||
import AlertDialog from '@/components/AlertDialog.vue'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Clock, Grid3x3 } from 'lucide-vue-next'
|
||||
import { formatNumber, formatTime, getResourceCostColor } from '@/utils/format'
|
||||
import * as buildingLogic from '@/logic/buildingLogic'
|
||||
import * as buildingValidation from '@/logic/buildingValidation'
|
||||
import * as publicLogic from '@/logic/publicLogic'
|
||||
import * as officerLogic from '@/logic/officerLogic'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const detailDialog = useDetailDialogStore()
|
||||
const { t } = useI18n()
|
||||
const { BUILDINGS, TECHNOLOGIES } = useGameConfig()
|
||||
const planet = computed(() => gameStore.currentPlanet)
|
||||
const alertDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
|
||||
|
||||
// AlertDialog 状态
|
||||
const alertDialogOpen = ref(false)
|
||||
const alertDialogTitle = ref('')
|
||||
const alertDialogMessage = ref('')
|
||||
|
||||
// 拆除确认对话框状态
|
||||
const demolishConfirmOpen = ref(false)
|
||||
const demolishConfirmMessage = ref('')
|
||||
const pendingDemolishBuilding = ref<BuildingType | null>(null)
|
||||
|
||||
// 资源类型配置(用于成本显示)
|
||||
const costResourceTypes = [
|
||||
{ key: 'metal' as const },
|
||||
{ key: 'crystal' as const },
|
||||
{ key: 'deuterium' as const },
|
||||
{ key: 'darkMatter' as const }
|
||||
]
|
||||
|
||||
// 根据星球类型过滤可用建筑
|
||||
const availableBuildings = computed(() => {
|
||||
const availableBuildings = computed<BuildingType[]>(() => {
|
||||
if (!planet.value) return []
|
||||
return Object.values(BuildingType).filter(buildingType => {
|
||||
return (Object.values(BuildingType) as BuildingType[]).filter(buildingType => {
|
||||
const config = BUILDINGS.value[buildingType]
|
||||
if (planet.value!.isMoon) {
|
||||
// 月球只能建造月球专属建筑
|
||||
@@ -189,19 +247,17 @@
|
||||
const handleUpgrade = (buildingType: BuildingType) => {
|
||||
// 检查前置条件
|
||||
if (!checkUpgradeRequirements(buildingType)) {
|
||||
alertDialog.value?.show({
|
||||
title: t('common.requirementsNotMet'),
|
||||
message: getRequirementsList(buildingType)
|
||||
})
|
||||
alertDialogTitle.value = t('common.requirementsNotMet')
|
||||
alertDialogMessage.value = getRequirementsList(buildingType)
|
||||
alertDialogOpen.value = true
|
||||
return
|
||||
}
|
||||
|
||||
const success = upgradeBuilding(buildingType)
|
||||
if (!success) {
|
||||
alertDialog.value?.show({
|
||||
title: t('buildingsView.upgradeFailed'),
|
||||
message: t('buildingsView.upgradeFailedMessage')
|
||||
})
|
||||
alertDialogTitle.value = t('buildingsView.upgradeFailed')
|
||||
alertDialogMessage.value = t('buildingsView.upgradeFailedMessage')
|
||||
alertDialogOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,7 +347,13 @@
|
||||
return false
|
||||
}
|
||||
|
||||
if (planet.value.buildQueue.length > 0) return false
|
||||
// 检查建造队列是否已满(只计算建筑类型的队列项)
|
||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
|
||||
const maxQueue = publicLogic.getMaxBuildQueue(planet.value, bonuses.additionalBuildQueue)
|
||||
const buildingQueueCount = planet.value.buildQueue.filter(item => item.type === 'building' || item.type === 'demolish').length
|
||||
if (buildingQueueCount >= maxQueue) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查前置条件
|
||||
const validation = buildingValidation.validateBuildingUpgrade(
|
||||
@@ -307,7 +369,8 @@
|
||||
return (
|
||||
planet.value.resources.metal >= cost.metal &&
|
||||
planet.value.resources.crystal >= cost.crystal &&
|
||||
planet.value.resources.deuterium >= cost.deuterium
|
||||
planet.value.resources.deuterium >= cost.deuterium &&
|
||||
planet.value.resources.darkMatter >= cost.darkMatter
|
||||
)
|
||||
}
|
||||
|
||||
@@ -316,7 +379,21 @@
|
||||
}
|
||||
|
||||
const getBuildingTime = (buildingType: BuildingType, targetLevel: number): number => {
|
||||
return buildingLogic.calculateBuildingTime(buildingType, targetLevel)
|
||||
if (!planet.value) return 0
|
||||
|
||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
|
||||
|
||||
// 获取机器人工厂和纳米工厂等级
|
||||
const roboticsFactoryLevel = planet.value.buildings[BuildingType.RoboticsFactory] || 0
|
||||
const naniteFactoryLevel = planet.value.buildings[BuildingType.NaniteFactory] || 0
|
||||
|
||||
return buildingLogic.calculateBuildingTime(
|
||||
buildingType,
|
||||
targetLevel,
|
||||
bonuses.buildingSpeedBonus,
|
||||
roboticsFactoryLevel,
|
||||
naniteFactoryLevel
|
||||
)
|
||||
}
|
||||
|
||||
// 拆除建筑
|
||||
@@ -330,20 +407,47 @@
|
||||
}
|
||||
|
||||
const handleDemolish = (buildingType: BuildingType) => {
|
||||
const success = demolishBuilding(buildingType)
|
||||
if (!success) {
|
||||
alertDialog.value?.show({
|
||||
title: t('buildingsView.demolishFailed'),
|
||||
message: t('buildingsView.demolishFailedMessage')
|
||||
})
|
||||
const buildingName = BUILDINGS.value[buildingType].name
|
||||
const refund = getDemolishRefund(buildingType)
|
||||
|
||||
demolishConfirmMessage.value = `${t('buildingsView.confirmDemolishMessage')}: ${buildingName}
|
||||
|
||||
${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)}` : ''}`
|
||||
|
||||
pendingDemolishBuilding.value = buildingType
|
||||
demolishConfirmOpen.value = true
|
||||
}
|
||||
|
||||
const confirmDemolish = () => {
|
||||
if (pendingDemolishBuilding.value) {
|
||||
const success = demolishBuilding(pendingDemolishBuilding.value)
|
||||
if (!success) {
|
||||
alertDialogTitle.value = t('buildingsView.demolishFailed')
|
||||
alertDialogMessage.value = t('buildingsView.demolishFailedMessage')
|
||||
alertDialogOpen.value = true
|
||||
}
|
||||
}
|
||||
demolishConfirmOpen.value = false
|
||||
pendingDemolishBuilding.value = null
|
||||
}
|
||||
|
||||
// 检查是否可以拆除
|
||||
const canDemolish = (buildingType: BuildingType): boolean => {
|
||||
if (!planet.value) return false
|
||||
if (planet.value.buildQueue.length > 0) return false
|
||||
return getBuildingLevel(buildingType) > 0
|
||||
if (getBuildingLevel(buildingType) <= 0) return false
|
||||
|
||||
// 检查建造队列是否已满(只计算建筑类型的队列项)
|
||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
|
||||
const maxQueue = publicLogic.getMaxBuildQueue(planet.value, bonuses.additionalBuildQueue)
|
||||
const buildingQueueCount = planet.value.buildQueue.filter(item => item.type === 'building' || item.type === 'demolish').length
|
||||
if (buildingQueueCount >= maxQueue) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 获取拆除返还资源
|
||||
|
||||
@@ -9,20 +9,20 @@
|
||||
<Card v-for="defenseType in Object.values(DefenseType)" :key="defenseType" class="relative">
|
||||
<CardUnlockOverlay :requirements="DEFENSES[defenseType].requirements" />
|
||||
<CardHeader>
|
||||
<div class="flex justify-between items-start gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="mb-2">
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-2">
|
||||
<CardTitle
|
||||
class="text-base sm:text-lg cursor-pointer hover:text-primary transition-colors"
|
||||
class="text-sm sm:text-base lg:text-lg cursor-pointer hover:text-primary transition-colors underline decoration-dotted underline-offset-4 order-2 sm:order-1"
|
||||
@click="detailDialog.openDefense(defenseType)"
|
||||
>
|
||||
{{ DEFENSES[defenseType].name }}
|
||||
</CardTitle>
|
||||
<CardDescription class="text-xs sm:text-sm">{{ DEFENSES[defenseType].description }}</CardDescription>
|
||||
<Badge variant="secondary" class="text-xs whitespace-nowrap self-start order-1 sm:order-2">
|
||||
{{ planet.defense[defenseType] }}
|
||||
</Badge>
|
||||
</div>
|
||||
<Badge variant="secondary" class="text-xs whitespace-nowrap flex-shrink-0">
|
||||
{{ planet.defense[defenseType] }}
|
||||
</Badge>
|
||||
</div>
|
||||
<CardDescription class="text-xs sm:text-sm">{{ DEFENSES[defenseType].description }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="space-y-3 sm:space-y-4">
|
||||
@@ -48,34 +48,19 @@
|
||||
<div class="text-xs sm:text-sm space-y-1.5 sm:space-y-2">
|
||||
<p class="text-muted-foreground mb-1 sm:mb-2">{{ t('defenseView.unitCost') }}:</p>
|
||||
<div class="space-y-1 sm:space-y-1.5">
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.metal') }}:</span>
|
||||
<div
|
||||
v-for="resourceType in costResourceTypes"
|
||||
:key="resourceType.key"
|
||||
v-show="resourceType.key !== 'darkMatter' || DEFENSES[defenseType].cost.darkMatter > 0"
|
||||
class="flex items-center gap-1.5 sm:gap-2"
|
||||
>
|
||||
<ResourceIcon :type="resourceType.key" size="sm" />
|
||||
<span class="text-xs">{{ t(`resources.${resourceType.key}`) }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.metal, DEFENSES[defenseType].cost.metal)"
|
||||
:class="getResourceCostColor(planet.resources[resourceType.key], DEFENSES[defenseType].cost[resourceType.key])"
|
||||
>
|
||||
{{ formatNumber(DEFENSES[defenseType].cost.metal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.crystal') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.crystal, DEFENSES[defenseType].cost.crystal)"
|
||||
>
|
||||
{{ formatNumber(DEFENSES[defenseType].cost.crystal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.deuterium') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.deuterium, DEFENSES[defenseType].cost.deuterium)"
|
||||
>
|
||||
{{ formatNumber(DEFENSES[defenseType].cost.deuterium) }}
|
||||
{{ formatNumber(DEFENSES[defenseType].cost[resourceType.key]) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -101,34 +86,19 @@
|
||||
<div v-if="quantities[defenseType] > 0" class="text-xs sm:text-sm space-y-1.5 sm:space-y-2 p-2.5 sm:p-3 bg-muted rounded-lg">
|
||||
<p class="font-medium text-muted-foreground">{{ t('defenseView.totalCost') }}:</p>
|
||||
<div class="space-y-1 sm:space-y-1.5">
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.metal') }}:</span>
|
||||
<div
|
||||
v-for="resourceType in costResourceTypes"
|
||||
:key="resourceType.key"
|
||||
v-show="resourceType.key !== 'darkMatter' || getTotalCost(defenseType).darkMatter > 0"
|
||||
class="flex items-center gap-1.5 sm:gap-2"
|
||||
>
|
||||
<ResourceIcon :type="resourceType.key" size="sm" />
|
||||
<span class="text-xs">{{ t(`resources.${resourceType.key}`) }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.metal, getTotalCost(defenseType).metal)"
|
||||
:class="getResourceCostColor(planet.resources[resourceType.key], getTotalCost(defenseType)[resourceType.key])"
|
||||
>
|
||||
{{ formatNumber(getTotalCost(defenseType).metal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.crystal') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.crystal, getTotalCost(defenseType).crystal)"
|
||||
>
|
||||
{{ formatNumber(getTotalCost(defenseType).crystal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.deuterium') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.deuterium, getTotalCost(defenseType).deuterium)"
|
||||
>
|
||||
{{ formatNumber(getTotalCost(defenseType).deuterium) }}
|
||||
{{ formatNumber(getTotalCost(defenseType)[resourceType.key]) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -143,7 +113,19 @@
|
||||
</div>
|
||||
|
||||
<!-- 提示对话框 -->
|
||||
<AlertDialog ref="alertDialog" />
|
||||
<AlertDialog :open="alertDialogOpen" @update:open="alertDialogOpen = $event">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ alertDialogTitle }}</AlertDialogTitle>
|
||||
<AlertDialogDescription class="whitespace-pre-line">
|
||||
{{ alertDialogMessage }}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction>{{ t('common.confirm') }}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -160,7 +142,15 @@
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import AlertDialog from '@/components/AlertDialog.vue'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import UnlockRequirement from '@/components/UnlockRequirement.vue'
|
||||
import CardUnlockOverlay from '@/components/CardUnlockOverlay.vue'
|
||||
import { formatNumber, getResourceCostColor } from '@/utils/format'
|
||||
@@ -172,7 +162,19 @@
|
||||
const { t } = useI18n()
|
||||
const { DEFENSES } = useGameConfig()
|
||||
const planet = computed(() => gameStore.currentPlanet)
|
||||
const alertDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
|
||||
|
||||
// AlertDialog 状态
|
||||
const alertDialogOpen = ref(false)
|
||||
const alertDialogTitle = ref('')
|
||||
const alertDialogMessage = ref('')
|
||||
|
||||
// 资源类型配置(用于成本显示)
|
||||
const costResourceTypes = [
|
||||
{ key: 'metal' as const },
|
||||
{ key: 'crystal' as const },
|
||||
{ key: 'deuterium' as const },
|
||||
{ key: 'darkMatter' as const }
|
||||
]
|
||||
|
||||
// 每种防御设施的建造数量
|
||||
const quantities = ref<Record<DefenseType, number>>({
|
||||
@@ -184,6 +186,8 @@
|
||||
[DefenseType.PlasmaTurret]: 0,
|
||||
[DefenseType.SmallShieldDome]: 0,
|
||||
[DefenseType.LargeShieldDome]: 0,
|
||||
[DefenseType.AntiBallisticMissile]: 0,
|
||||
[DefenseType.InterplanetaryMissile]: 0,
|
||||
[DefenseType.PlanetaryShield]: 0
|
||||
})
|
||||
|
||||
@@ -205,19 +209,17 @@
|
||||
const handleBuild = (defenseType: DefenseType) => {
|
||||
const quantity = quantities.value[defenseType]
|
||||
if (quantity <= 0) {
|
||||
alertDialog.value?.show({
|
||||
title: t('defenseView.inputError'),
|
||||
message: t('defenseView.inputErrorMessage')
|
||||
})
|
||||
alertDialogTitle.value = t('defenseView.inputError')
|
||||
alertDialogMessage.value = t('defenseView.inputErrorMessage')
|
||||
alertDialogOpen.value = true
|
||||
return
|
||||
}
|
||||
|
||||
const success = buildDefense(defenseType, quantity)
|
||||
if (!success) {
|
||||
alertDialog.value?.show({
|
||||
title: t('defenseView.buildFailed'),
|
||||
message: t('defenseView.buildFailedMessage')
|
||||
})
|
||||
alertDialogTitle.value = t('defenseView.buildFailed')
|
||||
alertDialogMessage.value = t('defenseView.buildFailedMessage')
|
||||
alertDialogOpen.value = true
|
||||
} else {
|
||||
quantities.value[defenseType] = 0
|
||||
}
|
||||
@@ -240,14 +242,16 @@
|
||||
const totalCost = {
|
||||
metal: config.cost.metal * quantity,
|
||||
crystal: config.cost.crystal * quantity,
|
||||
deuterium: config.cost.deuterium * quantity
|
||||
deuterium: config.cost.deuterium * quantity,
|
||||
darkMatter: config.cost.darkMatter * quantity
|
||||
}
|
||||
|
||||
return (
|
||||
publicLogic.checkRequirements(planet.value, gameStore.player.technologies, config.requirements) &&
|
||||
planet.value.resources.metal >= totalCost.metal &&
|
||||
planet.value.resources.crystal >= totalCost.crystal &&
|
||||
planet.value.resources.deuterium >= totalCost.deuterium
|
||||
planet.value.resources.deuterium >= totalCost.deuterium &&
|
||||
planet.value.resources.darkMatter >= totalCost.darkMatter
|
||||
)
|
||||
}
|
||||
|
||||
@@ -258,7 +262,8 @@
|
||||
return {
|
||||
metal: config.cost.metal * quantity,
|
||||
crystal: config.cost.crystal * quantity,
|
||||
deuterium: config.cost.deuterium * quantity
|
||||
deuterium: config.cost.deuterium * quantity,
|
||||
darkMatter: config.cost.darkMatter * quantity
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
390
src/views/DiplomacyView.vue
Normal file
390
src/views/DiplomacyView.vue
Normal file
@@ -0,0 +1,390 @@
|
||||
<template>
|
||||
<div class="container mx-auto p-4 sm:p-6 space-y-6">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('diplomacy.title') }}</h1>
|
||||
<p class="text-sm text-muted-foreground mt-1">{{ t('diplomacy.description') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 关系状态过滤标签 -->
|
||||
<Tabs v-model="activeTab" class="w-full">
|
||||
<TabsList class="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="all">
|
||||
{{ t('diplomacy.tabs.all') }}
|
||||
<Badge variant="secondary" class="ml-2">{{ allNpcs.length }}</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="friendly">
|
||||
{{ t('diplomacy.tabs.friendly') }}
|
||||
<Badge variant="secondary" class="ml-2">{{ friendlyNpcs.length }}</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="neutral">
|
||||
{{ t('diplomacy.tabs.neutral') }}
|
||||
<Badge variant="secondary" class="ml-2">{{ neutralNpcs.length }}</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="hostile">
|
||||
{{ t('diplomacy.tabs.hostile') }}
|
||||
<Badge variant="secondary" class="ml-2">{{ hostileNpcs.length }}</Badge>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<!-- 全部NPC -->
|
||||
<TabsContent value="all" class="space-y-4 mt-6">
|
||||
<div v-if="allNpcs.length === 0" class="text-center py-12 text-muted-foreground">
|
||||
{{ t('diplomacy.noNpcs') }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<NpcRelationCard v-for="npc in paginatedAllNpcs" :key="npc.id" :npc="npc" :relation="getRelation(npc.id)" />
|
||||
</div>
|
||||
<Pagination
|
||||
v-if="totalPagesAll > 1"
|
||||
v-model:page="currentPage.all"
|
||||
:total="allNpcs.length"
|
||||
:items-per-page="ITEMS_PER_PAGE"
|
||||
:sibling-count="1"
|
||||
show-edges
|
||||
class="mt-6"
|
||||
>
|
||||
<PaginationContent>
|
||||
<PaginationPrevious>{{ t('pagination.previous') }}</PaginationPrevious>
|
||||
|
||||
<template v-for="(pageNum, index) in pageNumbersAll" :key="index">
|
||||
<PaginationItem v-if="typeof pageNum === 'number'" :value="pageNum" :is-active="pageNum === currentPage.all">
|
||||
{{ pageNum }}
|
||||
</PaginationItem>
|
||||
<span v-else class="px-2 text-muted-foreground">{{ pageNum }}</span>
|
||||
</template>
|
||||
|
||||
<PaginationNext>{{ t('pagination.next') }}</PaginationNext>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</template>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 友好NPC -->
|
||||
<TabsContent value="friendly" class="space-y-4 mt-6">
|
||||
<div v-if="friendlyNpcs.length === 0" class="text-center py-12 text-muted-foreground">
|
||||
{{ t('diplomacy.noFriendlyNpcs') }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<NpcRelationCard v-for="npc in paginatedFriendlyNpcs" :key="npc.id" :npc="npc" :relation="getRelation(npc.id)" />
|
||||
</div>
|
||||
<Pagination
|
||||
v-if="totalPagesFriendly > 1"
|
||||
v-model:page="currentPage.friendly"
|
||||
:total="friendlyNpcs.length"
|
||||
:items-per-page="ITEMS_PER_PAGE"
|
||||
:sibling-count="1"
|
||||
show-edges
|
||||
class="mt-6"
|
||||
>
|
||||
<PaginationContent>
|
||||
<PaginationPrevious>{{ t('pagination.previous') }}</PaginationPrevious>
|
||||
|
||||
<template v-for="(pageNum, index) in pageNumbersFriendly" :key="index">
|
||||
<PaginationItem v-if="typeof pageNum === 'number'" :value="pageNum" :is-active="pageNum === currentPage.friendly">
|
||||
{{ pageNum }}
|
||||
</PaginationItem>
|
||||
<span v-else class="px-2 text-muted-foreground">{{ pageNum }}</span>
|
||||
</template>
|
||||
|
||||
<PaginationNext>{{ t('pagination.next') }}</PaginationNext>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</template>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 中立NPC -->
|
||||
<TabsContent value="neutral" class="space-y-4 mt-6">
|
||||
<div v-if="neutralNpcs.length === 0" class="text-center py-12 text-muted-foreground">
|
||||
{{ t('diplomacy.noNeutralNpcs') }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<NpcRelationCard v-for="npc in paginatedNeutralNpcs" :key="npc.id" :npc="npc" :relation="getRelation(npc.id)" />
|
||||
</div>
|
||||
<Pagination
|
||||
v-if="totalPagesNeutral > 1"
|
||||
v-model:page="currentPage.neutral"
|
||||
:total="neutralNpcs.length"
|
||||
:items-per-page="ITEMS_PER_PAGE"
|
||||
:sibling-count="1"
|
||||
show-edges
|
||||
class="mt-6"
|
||||
>
|
||||
<PaginationContent>
|
||||
<PaginationPrevious>{{ t('pagination.previous') }}</PaginationPrevious>
|
||||
|
||||
<template v-for="(pageNum, index) in pageNumbersNeutral" :key="index">
|
||||
<PaginationItem v-if="typeof pageNum === 'number'" :value="pageNum" :is-active="pageNum === currentPage.neutral">
|
||||
{{ pageNum }}
|
||||
</PaginationItem>
|
||||
<span v-else class="px-2 text-muted-foreground">{{ pageNum }}</span>
|
||||
</template>
|
||||
|
||||
<PaginationNext>{{ t('pagination.next') }}</PaginationNext>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</template>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 敌对NPC -->
|
||||
<TabsContent value="hostile" class="space-y-4 mt-6">
|
||||
<div v-if="hostileNpcs.length === 0" class="text-center py-12 text-muted-foreground">
|
||||
{{ t('diplomacy.noHostileNpcs') }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<NpcRelationCard v-for="npc in paginatedHostileNpcs" :key="npc.id" :npc="npc" :relation="getRelation(npc.id)" />
|
||||
</div>
|
||||
<Pagination
|
||||
v-if="totalPagesHostile > 1"
|
||||
v-model:page="currentPage.hostile"
|
||||
:total="hostileNpcs.length"
|
||||
:items-per-page="ITEMS_PER_PAGE"
|
||||
:sibling-count="1"
|
||||
show-edges
|
||||
class="mt-6"
|
||||
>
|
||||
<PaginationContent>
|
||||
<PaginationPrevious>{{ t('pagination.previous') }}</PaginationPrevious>
|
||||
|
||||
<template v-for="(pageNum, index) in pageNumbersHostile" :key="index">
|
||||
<PaginationItem v-if="typeof pageNum === 'number'" :value="pageNum" :is-active="pageNum === currentPage.hostile">
|
||||
{{ pageNum }}
|
||||
</PaginationItem>
|
||||
<span v-else class="px-2 text-muted-foreground">{{ pageNum }}</span>
|
||||
</template>
|
||||
|
||||
<PaginationNext>{{ t('pagination.next') }}</PaginationNext>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</template>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<!-- 外交报告历史 -->
|
||||
<Card v-if="diplomaticReports.length > 0">
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('diplomacy.recentEvents') }}</CardTitle>
|
||||
<CardDescription>{{ t('diplomacy.recentEventsDescription') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="space-y-2 max-h-96 overflow-y-auto">
|
||||
<div
|
||||
v-for="report in diplomaticReports"
|
||||
:key="report.id"
|
||||
class="flex items-start gap-3 p-3 rounded-lg border bg-card hover:bg-accent/50 transition-colors"
|
||||
>
|
||||
<div class="flex-shrink-0 mt-0.5">
|
||||
<component :is="getEventIcon(report.eventType)" class="h-5 w-5" :class="getEventIconColor(report.reputationChange)" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="font-medium">{{ report.npcName }}</span>
|
||||
<Badge :variant="getReputationBadgeVariant(report.reputationChange)" class="text-xs">
|
||||
{{ report.reputationChange > 0 ? '+' : '' }}{{ report.reputationChange }}
|
||||
</Badge>
|
||||
<Badge :variant="getStatusBadgeVariant(report.newStatus)" class="text-xs">
|
||||
{{ getStatusText(report.newStatus) }}
|
||||
</Badge>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">{{ report.message }}</p>
|
||||
<p class="text-xs text-muted-foreground mt-1">{{ formatTime(Date.now() - report.timestamp) }} {{ t('diplomacy.ago') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useNPCStore } from '@/stores/npcStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Pagination, PaginationContent, PaginationItem, PaginationNext, PaginationPrevious } from '@/components/ui/pagination'
|
||||
import NpcRelationCard from '@/components/NpcRelationCard.vue'
|
||||
import { Gift, Sword, Eye, Trash2 } from 'lucide-vue-next'
|
||||
import { RelationStatus, DiplomaticEventType } from '@/types/game'
|
||||
import type { DiplomaticRelation, DiplomaticReport } from '@/types/game'
|
||||
import { formatTime } from '@/utils/format'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const npcStore = useNPCStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const activeTab = ref('all')
|
||||
|
||||
// 分页状态
|
||||
const ITEMS_PER_PAGE = 20
|
||||
const currentPage = ref<Record<string, number>>({
|
||||
all: 1,
|
||||
friendly: 1,
|
||||
neutral: 1,
|
||||
hostile: 1
|
||||
})
|
||||
|
||||
// 获取玩家对NPC的关系
|
||||
const getRelation = (npcId: string): DiplomaticRelation | undefined => {
|
||||
return gameStore.player.diplomaticRelations?.[npcId]
|
||||
}
|
||||
|
||||
// 按关系状态分类NPC
|
||||
const allNpcs = computed(() => npcStore.npcs)
|
||||
|
||||
const friendlyNpcs = computed(() => {
|
||||
return npcStore.npcs.filter(npc => {
|
||||
const relation = getRelation(npc.id)
|
||||
return relation?.status === RelationStatus.Friendly
|
||||
})
|
||||
})
|
||||
|
||||
const neutralNpcs = computed(() => {
|
||||
return npcStore.npcs.filter(npc => {
|
||||
const relation = getRelation(npc.id)
|
||||
return !relation || relation.status === RelationStatus.Neutral
|
||||
})
|
||||
})
|
||||
|
||||
const hostileNpcs = computed(() => {
|
||||
return npcStore.npcs.filter(npc => {
|
||||
const relation = getRelation(npc.id)
|
||||
return relation?.status === RelationStatus.Hostile
|
||||
})
|
||||
})
|
||||
|
||||
// 分页辅助函数
|
||||
const getPaginatedNpcs = (npcs: typeof allNpcs.value, tabKey: string) => {
|
||||
const page = currentPage.value[tabKey] || 1
|
||||
const start = (page - 1) * ITEMS_PER_PAGE
|
||||
const end = start + ITEMS_PER_PAGE
|
||||
return npcs.slice(start, end)
|
||||
}
|
||||
|
||||
const getTotalPages = (npcs: typeof allNpcs.value) => {
|
||||
return Math.ceil(npcs.length / ITEMS_PER_PAGE)
|
||||
}
|
||||
|
||||
// 分页后的NPC列表
|
||||
const paginatedAllNpcs = computed(() => getPaginatedNpcs(allNpcs.value, 'all'))
|
||||
const paginatedFriendlyNpcs = computed(() => getPaginatedNpcs(friendlyNpcs.value, 'friendly'))
|
||||
const paginatedNeutralNpcs = computed(() => getPaginatedNpcs(neutralNpcs.value, 'neutral'))
|
||||
const paginatedHostileNpcs = computed(() => getPaginatedNpcs(hostileNpcs.value, 'hostile'))
|
||||
|
||||
// 总页数
|
||||
const totalPagesAll = computed(() => getTotalPages(allNpcs.value))
|
||||
const totalPagesFriendly = computed(() => getTotalPages(friendlyNpcs.value))
|
||||
const totalPagesNeutral = computed(() => getTotalPages(neutralNpcs.value))
|
||||
const totalPagesHostile = computed(() => getTotalPages(hostileNpcs.value))
|
||||
|
||||
// 生成页码列表(用于分页UI)
|
||||
const getPageNumbers = (currentPageNum: number, totalPages: number) => {
|
||||
const pages: (number | string)[] = []
|
||||
const maxVisible = 5 // 最多显示5个页码
|
||||
|
||||
if (totalPages <= maxVisible) {
|
||||
// 如果总页数少于等于5,显示全部
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
} else {
|
||||
// 总是显示第1页
|
||||
pages.push(1)
|
||||
|
||||
if (currentPageNum > 3) {
|
||||
pages.push('...')
|
||||
}
|
||||
|
||||
// 计算中间显示的页码范围
|
||||
const start = Math.max(2, currentPageNum - 1)
|
||||
const end = Math.min(totalPages - 1, currentPageNum + 1)
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
|
||||
if (currentPageNum < totalPages - 2) {
|
||||
pages.push('...')
|
||||
}
|
||||
|
||||
// 总是显示最后一页
|
||||
pages.push(totalPages)
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
// 各标签页的页码列表
|
||||
const pageNumbersAll = computed(() => getPageNumbers(currentPage.value.all || 1, totalPagesAll.value))
|
||||
const pageNumbersFriendly = computed(() => getPageNumbers(currentPage.value.friendly || 1, totalPagesFriendly.value))
|
||||
const pageNumbersNeutral = computed(() => getPageNumbers(currentPage.value.neutral || 1, totalPagesNeutral.value))
|
||||
const pageNumbersHostile = computed(() => getPageNumbers(currentPage.value.hostile || 1, totalPagesHostile.value))
|
||||
|
||||
// 外交报告(最近20条,按时间倒序)
|
||||
const diplomaticReports = computed(() => {
|
||||
const reports = gameStore.player.diplomaticReports || []
|
||||
return [...reports].sort((a, b) => b.timestamp - a.timestamp).slice(0, 20)
|
||||
})
|
||||
|
||||
// 获取事件图标
|
||||
const getEventIcon = (eventType: DiplomaticReport['eventType']) => {
|
||||
switch (eventType) {
|
||||
case DiplomaticEventType.GiftResources:
|
||||
return Gift
|
||||
case DiplomaticEventType.Attack:
|
||||
case DiplomaticEventType.AllyAttacked:
|
||||
return Sword
|
||||
case DiplomaticEventType.Spy:
|
||||
return Eye
|
||||
case DiplomaticEventType.StealDebris:
|
||||
return Trash2
|
||||
default:
|
||||
return Gift
|
||||
}
|
||||
}
|
||||
|
||||
// 获取事件图标颜色
|
||||
const getEventIconColor = (reputationChange: number) => {
|
||||
if (reputationChange > 0) return 'text-green-600 dark:text-green-400'
|
||||
if (reputationChange < 0) return 'text-red-600 dark:text-red-400'
|
||||
return 'text-muted-foreground'
|
||||
}
|
||||
|
||||
// 获取好感度Badge样式
|
||||
const getReputationBadgeVariant = (change: number) => {
|
||||
if (change > 0) return 'default'
|
||||
if (change < 0) return 'destructive'
|
||||
return 'secondary'
|
||||
}
|
||||
|
||||
// 获取关系状态Badge样式
|
||||
const getStatusBadgeVariant = (status: RelationStatus) => {
|
||||
switch (status) {
|
||||
case RelationStatus.Friendly:
|
||||
return 'default'
|
||||
case RelationStatus.Hostile:
|
||||
return 'destructive'
|
||||
default:
|
||||
return 'secondary'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取关系状态文本
|
||||
const getStatusText = (status: RelationStatus) => {
|
||||
switch (status) {
|
||||
case RelationStatus.Friendly:
|
||||
return t('diplomacy.status.friendly')
|
||||
case RelationStatus.Hostile:
|
||||
return t('diplomacy.status.hostile')
|
||||
default:
|
||||
return t('diplomacy.status.neutral')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -6,297 +6,330 @@
|
||||
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('fleetView.title') }}</h1>
|
||||
|
||||
<!-- 标签切换 -->
|
||||
<div class="flex gap-2 border-b">
|
||||
<Button @click="activeTab = 'fleet'" :variant="activeTab === 'fleet' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
{{ t('fleetView.fleetOverview') }}
|
||||
</Button>
|
||||
<Button @click="activeTab = 'send'" :variant="activeTab === 'send' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
{{ t('fleetView.sendFleet') }}
|
||||
</Button>
|
||||
<Button @click="activeTab = 'missions'" :variant="activeTab === 'missions' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
{{ t('fleetView.flightMissions') }}
|
||||
<Badge v-if="gameStore.player.fleetMissions.length > 0" variant="secondary" class="ml-1">
|
||||
{{ gameStore.player.fleetMissions.length }}
|
||||
</Badge>
|
||||
</Button>
|
||||
</div>
|
||||
<Tabs v-model="activeTab" class="w-full">
|
||||
<TabsList class="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="fleet">{{ t('fleetView.fleetOverview') }}</TabsTrigger>
|
||||
<TabsTrigger value="send">{{ t('fleetView.sendFleet') }}</TabsTrigger>
|
||||
<TabsTrigger value="missions">
|
||||
{{ t('fleetView.flightMissions') }}
|
||||
<Badge v-if="gameStore.player.fleetMissions.length > 0" variant="destructive" class="ml-1">
|
||||
{{ gameStore.player.fleetMissions.length }}
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<!-- 舰队总览 -->
|
||||
<div v-if="activeTab === 'fleet'">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.currentPlanetFleet') }}</CardTitle>
|
||||
<CardDescription>
|
||||
{{ planet.name }} [{{ planet.position.galaxy }}:{{ planet.position.system }}:{{ planet.position.position }}]
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3 sm:gap-4">
|
||||
<div v-for="(count, shipType) in planet.fleet" :key="shipType" class="p-3 sm:p-4 border rounded-lg space-y-2">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 class="font-semibold text-sm sm:text-base">{{ SHIPS[shipType].name }}</h3>
|
||||
<p class="text-xl sm:text-2xl font-bold">{{ formatNumber(count) }}</p>
|
||||
<!-- 舰队总览 -->
|
||||
<TabsContent value="fleet" class="mt-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.currentPlanetFleet') }}</CardTitle>
|
||||
<CardDescription>
|
||||
{{ planet.name }} [{{ planet.position.galaxy }}:{{ planet.position.system }}:{{ planet.position.position }}]
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3 sm:gap-4">
|
||||
<div v-for="(count, shipType) in planet.fleet" :key="shipType" class="p-3 sm:p-4 border rounded-lg space-y-2">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 class="font-semibold text-sm sm:text-base">{{ SHIPS[shipType].name }}</h3>
|
||||
<p class="text-xl sm:text-2xl font-bold">{{ formatNumber(count) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs sm:text-sm text-muted-foreground space-y-1">
|
||||
<p>{{ t('fleetView.attack') }}: {{ SHIPS[shipType].attack }}</p>
|
||||
<p>{{ t('fleetView.shield') }}: {{ SHIPS[shipType].shield }}</p>
|
||||
<p>{{ t('fleetView.armor') }}: {{ SHIPS[shipType].armor }}</p>
|
||||
<p>{{ t('fleetView.speed') }}: {{ formatNumber(SHIPS[shipType].speed) }}</p>
|
||||
<p>{{ t('fleetView.cargo') }}: {{ formatNumber(SHIPS[shipType].cargoCapacity) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs sm:text-sm text-muted-foreground space-y-1">
|
||||
<p>{{ t('fleetView.attack') }}: {{ SHIPS[shipType].attack }}</p>
|
||||
<p>{{ t('fleetView.shield') }}: {{ SHIPS[shipType].shield }}</p>
|
||||
<p>{{ t('fleetView.armor') }}: {{ SHIPS[shipType].armor }}</p>
|
||||
<p>{{ t('fleetView.speed') }}: {{ formatNumber(SHIPS[shipType].speed) }}</p>
|
||||
<p>{{ t('fleetView.cargo') }}: {{ formatNumber(SHIPS[shipType].cargoCapacity) }}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 派遣舰队 -->
|
||||
<TabsContent value="send" class="mt-4 space-y-4">
|
||||
<!-- 舰队任务槽位信息 -->
|
||||
<Card>
|
||||
<CardContent class="py-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm font-medium">{{ t('fleetView.fleetMissionSlots') }}:</span>
|
||||
<span class="text-sm font-bold">{{ gameStore.player.fleetMissions.length }} / {{ maxFleetMissions }}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 选择舰队 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.selectFleet') }}</CardTitle>
|
||||
<CardDescription>{{ t('fleetView.selectFleetDescription') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4">
|
||||
<div v-for="(count, shipType) in planet.fleet" :key="shipType" class="space-y-2">
|
||||
<Label :for="`ship-${shipType}`" class="text-xs sm:text-sm">
|
||||
{{ SHIPS[shipType].name }} ({{ t('fleetView.available') }}: {{ count }})
|
||||
</Label>
|
||||
<div class="flex gap-2">
|
||||
<Input
|
||||
:id="`ship-${shipType}`"
|
||||
v-model.number="selectedFleet[shipType]"
|
||||
type="number"
|
||||
min="0"
|
||||
:max="count"
|
||||
placeholder="0"
|
||||
class="text-sm"
|
||||
/>
|
||||
<Button @click="selectedFleet[shipType] = count" variant="outline" size="sm">{{ t('fleetView.all') }}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 派遣舰队 -->
|
||||
<div v-if="activeTab === 'send'" class="space-y-4">
|
||||
<!-- 舰队任务槽位信息 -->
|
||||
<Card>
|
||||
<CardContent class="py-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm font-medium">{{ t('fleetView.fleetMissionSlots') }}:</span>
|
||||
<span class="text-sm font-bold">{{ gameStore.player.fleetMissions.length }} / {{ maxFleetMissions }}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<!-- 目标坐标 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.targetCoordinates') }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-3 gap-2 sm:gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="galaxy" class="text-xs sm:text-sm">{{ t('fleetView.galaxy') }}</Label>
|
||||
<Input id="galaxy" v-model.number="targetPosition.galaxy" type="number" min="1" max="9" placeholder="1" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="system" class="text-xs sm:text-sm">{{ t('fleetView.system') }}</Label>
|
||||
<Input id="system" v-model.number="targetPosition.system" type="number" min="1" max="10" placeholder="1" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="position" class="text-xs sm:text-sm">{{ t('fleetView.position') }}</Label>
|
||||
<Input id="position" v-model.number="targetPosition.position" type="number" min="1" max="10" placeholder="1" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 选择舰队 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.selectFleet') }}</CardTitle>
|
||||
<CardDescription>{{ t('fleetView.selectFleetDescription') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4">
|
||||
<div v-for="(count, shipType) in planet.fleet" :key="shipType" class="space-y-2">
|
||||
<Label :for="`ship-${shipType}`" class="text-xs sm:text-sm">
|
||||
{{ SHIPS[shipType].name }} ({{ t('fleetView.available') }}: {{ count }})
|
||||
</Label>
|
||||
<div class="flex gap-2">
|
||||
<!-- 任务类型 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.missionType') }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
<Button
|
||||
v-for="mission in availableMissions"
|
||||
:key="mission.type"
|
||||
@click="selectedMission = mission.type"
|
||||
:variant="selectedMission === mission.type ? 'default' : 'outline'"
|
||||
class="justify-start"
|
||||
>
|
||||
<component :is="mission.icon" class="h-4 w-4 mr-2" />
|
||||
{{ mission.name }}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 运输资源(仅运输任务) -->
|
||||
<Card v-if="selectedMission === MissionType.Transport">
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.transportResources') }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<!-- 赠送模式切换(仅当目标是NPC星球时显示) -->
|
||||
<div v-if="targetNpc" class="mb-4 p-3 border rounded-lg bg-muted/50">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<Checkbox id="gift-mode" :default-value="isGiftMode" />
|
||||
<Label for="gift-mode" class="flex items-center gap-2 cursor-pointer">
|
||||
<Gift class="h-4 w-4" />
|
||||
{{ t('fleetView.giftMode') }}
|
||||
</Label>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">{{ t('fleetView.giftModeDescription') }} {{ targetNpc.name }}</p>
|
||||
<div v-if="isGiftMode && (cargo.metal > 0 || cargo.crystal > 0 || cargo.deuterium > 0)" class="mt-2 text-xs">
|
||||
<span class="text-muted-foreground">{{ t('fleetView.estimatedReputationGain') }}:</span>
|
||||
<span class="ml-1 font-semibold text-green-600 dark:text-green-400">+{{ calculateGiftReputation() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 sm:gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="cargo-metal" class="text-xs sm:text-sm flex items-center gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
{{ t('resources.metal') }} ({{ t('fleetView.available') }}: {{ formatNumber(planet.resources.metal) }})
|
||||
</Label>
|
||||
<Input id="cargo-metal" v-model.number="cargo.metal" type="number" min="0" :max="planet.resources.metal" placeholder="0" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="cargo-crystal" class="text-xs sm:text-sm flex items-center gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
{{ t('resources.crystal') }} ({{ t('fleetView.available') }}: {{ formatNumber(planet.resources.crystal) }})
|
||||
</Label>
|
||||
<Input
|
||||
:id="`ship-${shipType}`"
|
||||
v-model.number="selectedFleet[shipType]"
|
||||
id="cargo-crystal"
|
||||
v-model.number="cargo.crystal"
|
||||
type="number"
|
||||
min="0"
|
||||
:max="count"
|
||||
:max="planet.resources.crystal"
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="cargo-deuterium" class="text-xs sm:text-sm flex items-center gap-2">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
{{ t('resources.deuterium') }} ({{ t('fleetView.available') }}: {{ formatNumber(planet.resources.deuterium) }})
|
||||
</Label>
|
||||
<Input
|
||||
id="cargo-deuterium"
|
||||
v-model.number="cargo.deuterium"
|
||||
type="number"
|
||||
min="0"
|
||||
:max="planet.resources.deuterium"
|
||||
placeholder="0"
|
||||
class="text-sm"
|
||||
/>
|
||||
<Button @click="selectedFleet[shipType] = count" variant="outline" size="sm">{{ t('fleetView.all') }}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<p class="text-xs sm:text-sm text-muted-foreground mt-2">
|
||||
{{ t('fleetView.totalCargoCapacity') }}: {{ formatNumber(getTotalCargoCapacity()) }} | {{ t('fleetView.used') }}:
|
||||
{{ formatNumber(getTotalCargo()) }}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 目标坐标 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.targetCoordinates') }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-3 gap-2 sm:gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="galaxy" class="text-xs sm:text-sm">{{ t('fleetView.galaxy') }}</Label>
|
||||
<Input id="galaxy" v-model.number="targetPosition.galaxy" type="number" min="1" max="9" placeholder="1" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="system" class="text-xs sm:text-sm">{{ t('fleetView.system') }}</Label>
|
||||
<Input id="system" v-model.number="targetPosition.system" type="number" min="1" max="10" placeholder="1" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="position" class="text-xs sm:text-sm">{{ t('fleetView.position') }}</Label>
|
||||
<Input id="position" v-model.number="targetPosition.position" type="number" min="1" max="10" placeholder="1" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 任务类型 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.missionType') }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
<Button
|
||||
v-for="mission in availableMissions"
|
||||
:key="mission.type"
|
||||
@click="selectedMission = mission.type"
|
||||
:variant="selectedMission === mission.type ? 'default' : 'outline'"
|
||||
class="justify-start"
|
||||
>
|
||||
<component :is="mission.icon" class="h-4 w-4 mr-2" />
|
||||
{{ mission.name }}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 运输资源(仅运输任务) -->
|
||||
<Card v-if="selectedMission === MissionType.Transport">
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.transportResources') }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 sm:gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="cargo-metal" class="text-xs sm:text-sm flex items-center gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
{{ t('resources.metal') }} ({{ t('fleetView.available') }}: {{ formatNumber(planet.resources.metal) }})
|
||||
</Label>
|
||||
<Input id="cargo-metal" v-model.number="cargo.metal" type="number" min="0" :max="planet.resources.metal" placeholder="0" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="cargo-crystal" class="text-xs sm:text-sm flex items-center gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
{{ t('resources.crystal') }} ({{ t('fleetView.available') }}: {{ formatNumber(planet.resources.crystal) }})
|
||||
</Label>
|
||||
<Input
|
||||
id="cargo-crystal"
|
||||
v-model.number="cargo.crystal"
|
||||
type="number"
|
||||
min="0"
|
||||
:max="planet.resources.crystal"
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="cargo-deuterium" class="text-xs sm:text-sm flex items-center gap-2">
|
||||
<!-- 任务信息 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.missionInfo') }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-2">
|
||||
<div class="flex justify-between text-xs sm:text-sm">
|
||||
<span class="text-muted-foreground">{{ t('fleetView.fuelConsumption') }}:</span>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
{{ t('resources.deuterium') }} ({{ t('fleetView.available') }}: {{ formatNumber(planet.resources.deuterium) }})
|
||||
</Label>
|
||||
<Input
|
||||
id="cargo-deuterium"
|
||||
v-model.number="cargo.deuterium"
|
||||
type="number"
|
||||
min="0"
|
||||
:max="planet.resources.deuterium"
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs sm:text-sm text-muted-foreground mt-2">
|
||||
{{ t('fleetView.totalCargoCapacity') }}: {{ formatNumber(getTotalCargoCapacity()) }} | {{ t('fleetView.used') }}:
|
||||
{{ formatNumber(getTotalCargo()) }}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 任务信息 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.missionInfo') }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-2">
|
||||
<div class="flex justify-between text-xs sm:text-sm">
|
||||
<span class="text-muted-foreground">{{ t('fleetView.fuelConsumption') }}:</span>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
<span :class="getFuelConsumption() > planet.resources.deuterium ? 'text-red-600 dark:text-red-400 font-medium' : ''">
|
||||
{{ formatNumber(getFuelConsumption()) }}
|
||||
<span :class="getFuelConsumption() > planet.resources.deuterium ? 'text-red-600 dark:text-red-400 font-medium' : ''">
|
||||
{{ formatNumber(getFuelConsumption()) }}
|
||||
</span>
|
||||
<span class="text-muted-foreground">/ {{ formatNumber(planet.resources.deuterium) }}</span>
|
||||
</span>
|
||||
<span class="text-muted-foreground">/ {{ formatNumber(planet.resources.deuterium) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="Object.values(selectedFleet).some(c => c > 0)" class="flex justify-between text-xs sm:text-sm">
|
||||
<span class="text-muted-foreground">{{ t('fleetView.flightTime') }}:</span>
|
||||
<span>{{ formatTime(getFlightTime()) }}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 派遣按钮 -->
|
||||
<Button @click="handleSendFleet" :disabled="!canSendFleet()" class="w-full" size="lg">{{ t('fleetView.sendFleet') }}</Button>
|
||||
</div>
|
||||
|
||||
<!-- 飞行任务 -->
|
||||
<div v-if="activeTab === 'missions'" class="space-y-4">
|
||||
<Card v-if="gameStore.player.fleetMissions.length === 0">
|
||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('fleetView.noFlightMissions') }}</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card v-for="mission in gameStore.player.fleetMissions" :key="mission.id">
|
||||
<CardHeader>
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<CardTitle class="text-base sm:text-lg">{{ getMissionName(mission.missionType) }}</CardTitle>
|
||||
<CardDescription class="text-xs sm:text-sm">
|
||||
{{ getPlanetName(mission.originPlanetId) }} → [{{ mission.targetPosition.galaxy }}:{{ mission.targetPosition.system }}:{{
|
||||
mission.targetPosition.position
|
||||
}}]
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge :variant="mission.status === 'outbound' ? 'default' : 'secondary'">
|
||||
{{ mission.status === 'outbound' ? t('fleetView.outbound') : t('fleetView.returning') }}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-3">
|
||||
<!-- 舰队组成 -->
|
||||
<div>
|
||||
<p class="text-xs sm:text-sm font-medium mb-2">{{ t('fleetView.fleetComposition') }}:</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Badge v-for="(count, shipType) in mission.fleet" :key="shipType" variant="outline">
|
||||
{{ SHIPS[shipType].name }}: {{ count }}
|
||||
<div v-if="Object.values(selectedFleet).some(c => c > 0)" class="flex justify-between text-xs sm:text-sm">
|
||||
<span class="text-muted-foreground">{{ t('fleetView.flightTime') }}:</span>
|
||||
<span>{{ formatTime(getFlightTime()) }}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 派遣按钮 -->
|
||||
<Button @click="handleSendFleet" :disabled="!canSendFleet()" class="w-full" size="lg">{{ t('fleetView.sendFleet') }}</Button>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 飞行任务 -->
|
||||
<TabsContent value="missions" class="mt-4 space-y-4">
|
||||
<Card v-if="gameStore.player.fleetMissions.length === 0">
|
||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('fleetView.noFlightMissions') }}</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card v-for="mission in gameStore.player.fleetMissions" :key="mission.id">
|
||||
<CardHeader>
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<CardTitle class="text-base sm:text-lg">{{ getMissionName(mission.missionType) }}</CardTitle>
|
||||
<CardDescription class="text-xs sm:text-sm">
|
||||
{{ getPlanetName(mission.originPlanetId) }} → [{{ mission.targetPosition.galaxy }}:{{ mission.targetPosition.system }}:{{
|
||||
mission.targetPosition.position
|
||||
}}]
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge :variant="mission.status === 'outbound' ? 'default' : 'secondary'">
|
||||
{{ mission.status === 'outbound' ? t('fleetView.outbound') : t('fleetView.returning') }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 携带资源 -->
|
||||
<div v-if="mission.cargo.metal > 0 || mission.cargo.crystal > 0 || mission.cargo.deuterium > 0 || mission.cargo.darkMatter > 0">
|
||||
<p class="text-xs sm:text-sm font-medium mb-2">{{ t('fleetView.carryingResources') }}:</p>
|
||||
<div class="flex flex-wrap gap-2 text-xs">
|
||||
<span v-if="mission.cargo.metal > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
{{ formatNumber(mission.cargo.metal) }}
|
||||
</span>
|
||||
<span v-if="mission.cargo.crystal > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
{{ formatNumber(mission.cargo.crystal) }}
|
||||
</span>
|
||||
<span v-if="mission.cargo.deuterium > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
{{ formatNumber(mission.cargo.deuterium) }}
|
||||
</span>
|
||||
<span v-if="mission.cargo.darkMatter > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="darkMatter" size="sm" />
|
||||
{{ formatNumber(mission.cargo.darkMatter) }}
|
||||
</span>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-3">
|
||||
<!-- 舰队组成 -->
|
||||
<div>
|
||||
<p class="text-xs sm:text-sm font-medium mb-2">{{ t('fleetView.fleetComposition') }}:</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Badge v-for="(count, shipType) in mission.fleet" :key="shipType" variant="outline">
|
||||
{{ SHIPS[shipType].name }}: {{ count }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between text-xs sm:text-sm">
|
||||
<span>{{ mission.status === 'outbound' ? t('fleetView.arrivalTime') : t('fleetView.returnTime') }}:</span>
|
||||
<span>{{ formatTime(getRemainingTime(mission)) }}</span>
|
||||
<!-- 携带资源 -->
|
||||
<div v-if="mission.cargo.metal > 0 || mission.cargo.crystal > 0 || mission.cargo.deuterium > 0 || mission.cargo.darkMatter > 0">
|
||||
<p class="text-xs sm:text-sm font-medium mb-2">{{ t('fleetView.carryingResources') }}:</p>
|
||||
<div class="flex flex-wrap gap-2 text-xs">
|
||||
<span v-if="mission.cargo.metal > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
{{ formatNumber(mission.cargo.metal) }}
|
||||
</span>
|
||||
<span v-if="mission.cargo.crystal > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
{{ formatNumber(mission.cargo.crystal) }}
|
||||
</span>
|
||||
<span v-if="mission.cargo.deuterium > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
{{ formatNumber(mission.cargo.deuterium) }}
|
||||
</span>
|
||||
<span v-if="mission.cargo.darkMatter > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="darkMatter" size="sm" />
|
||||
{{ formatNumber(mission.cargo.darkMatter) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Progress :model-value="getMissionProgress(mission)" />
|
||||
</div>
|
||||
|
||||
<!-- 操作 -->
|
||||
<div class="flex gap-2">
|
||||
<Button v-if="mission.status === 'outbound'" @click="handleRecallFleet(mission.id)" variant="outline" size="sm" class="w-full">
|
||||
{{ t('fleetView.recallFleet') }}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<!-- 进度条 -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between text-xs sm:text-sm">
|
||||
<span>{{ mission.status === 'outbound' ? t('fleetView.arrivalTime') : t('fleetView.returnTime') }}:</span>
|
||||
<span>{{ formatTime(getRemainingTime(mission)) }}</span>
|
||||
</div>
|
||||
<Progress :model-value="getMissionProgress(mission)" />
|
||||
</div>
|
||||
|
||||
<!-- 操作 -->
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
v-if="mission.status === 'outbound'"
|
||||
@click="handleRecallFleet(mission.id)"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="w-full"
|
||||
>
|
||||
{{ t('fleetView.recallFleet') }}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<!-- 提示对话框 -->
|
||||
<AlertDialog ref="alertDialog" />
|
||||
<AlertDialog :open="alertDialogOpen" @update:open="alertDialogOpen = $event">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ alertDialogTitle }}</AlertDialogTitle>
|
||||
<AlertDialogDescription class="whitespace-pre-line">
|
||||
{{ alertDialogMessage }}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction>{{ t('common.confirm') }}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useUniverseStore } from '@/stores/universeStore'
|
||||
import { useNPCStore } from '@/stores/npcStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { useGameConfig } from '@/composables/useGameConfig'
|
||||
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
||||
@@ -304,30 +337,46 @@
|
||||
import { ShipType, MissionType, BuildingType } from '@/types/game'
|
||||
import type { Fleet, Resources } from '@/types/game'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import AlertDialog from '@/components/AlertDialog.vue'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import UnlockRequirement from '@/components/UnlockRequirement.vue'
|
||||
import { Sword, Package, Rocket as RocketIcon, Eye, Users, Recycle, Skull } from 'lucide-vue-next'
|
||||
import { Sword, Package, Rocket as RocketIcon, Eye, Users, Recycle, Skull, Gift } from 'lucide-vue-next'
|
||||
import { formatNumber, formatTime } from '@/utils/format'
|
||||
import * as shipValidation from '@/logic/shipValidation'
|
||||
import * as fleetLogic from '@/logic/fleetLogic'
|
||||
import * as shipLogic from '@/logic/shipLogic'
|
||||
import * as officerLogic from '@/logic/officerLogic'
|
||||
import * as publicLogic from '@/logic/publicLogic'
|
||||
import * as diplomaticLogic from '@/logic/diplomaticLogic'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const gameStore = useGameStore()
|
||||
const universeStore = useUniverseStore()
|
||||
const npcStore = useNPCStore()
|
||||
const { t } = useI18n()
|
||||
const { SHIPS } = useGameConfig()
|
||||
const planet = computed(() => gameStore.currentPlanet)
|
||||
const alertDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
|
||||
|
||||
// AlertDialog 状态
|
||||
const alertDialogOpen = ref(false)
|
||||
const alertDialogTitle = ref('')
|
||||
const alertDialogMessage = ref('')
|
||||
|
||||
// 当前时间(响应式)
|
||||
const currentTime = ref(Date.now())
|
||||
@@ -372,7 +421,7 @@
|
||||
currentTime.value = Date.now()
|
||||
}, 1000) // 每秒更新一次
|
||||
|
||||
const { galaxy, system, position, mission } = route.query
|
||||
const { galaxy, system, position, mission, gift } = route.query
|
||||
|
||||
// 如果有参数,填充数据
|
||||
if (galaxy || system || position) {
|
||||
@@ -388,6 +437,10 @@
|
||||
selectedMission.value = MissionType.Attack
|
||||
} else if (mission === 'colonize') {
|
||||
selectedMission.value = MissionType.Colonize
|
||||
} else if (gift === '1') {
|
||||
// 如果有gift参数,设置为运输任务并启用赠送模式
|
||||
selectedMission.value = MissionType.Transport
|
||||
isGiftMode.value = true
|
||||
}
|
||||
|
||||
// 自动切换到派遣舰队标签
|
||||
@@ -405,6 +458,26 @@
|
||||
}
|
||||
})
|
||||
|
||||
// 检查目标是否为NPC星球
|
||||
const targetNpc = computed(() => {
|
||||
return npcStore.npcs.find(npc =>
|
||||
npc.planets.some(
|
||||
p =>
|
||||
p.position.galaxy === targetPosition.value.galaxy &&
|
||||
p.position.system === targetPosition.value.system &&
|
||||
p.position.position === targetPosition.value.position
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
// 是否为赠送模式
|
||||
const isGiftMode = ref(false)
|
||||
|
||||
// 计算赠送的预估好感度增加值
|
||||
const calculateGiftReputation = (): number => {
|
||||
return diplomaticLogic.calculateGiftReputationGain(cargo.value)
|
||||
}
|
||||
|
||||
// 可用任务类型
|
||||
const availableMissions = computed(() => [
|
||||
{ type: MissionType.Attack, name: t('fleetView.attackMission'), icon: Sword },
|
||||
@@ -467,7 +540,8 @@
|
||||
if (!hasShips) return { valid: false, errorKey: 'fleetView.noShipsSelected' }
|
||||
|
||||
// 检查是否派遣到自己的星球
|
||||
if (planet.value) {
|
||||
// 回收任务和部署任务除外(回收残骸可能在同位置,部署可能到自己的月球)
|
||||
if (planet.value && selectedMission.value !== MissionType.Recycle && selectedMission.value !== MissionType.Deploy) {
|
||||
const isSamePlanet =
|
||||
targetPosition.value.galaxy === planet.value.position.galaxy &&
|
||||
targetPosition.value.system === planet.value.position.system &&
|
||||
@@ -541,6 +615,13 @@
|
||||
cargo,
|
||||
flightTime
|
||||
)
|
||||
|
||||
// 如果是赠送模式,标记任务
|
||||
if (missionType === MissionType.Transport && isGiftMode.value && targetNpc.value) {
|
||||
mission.isGift = true
|
||||
mission.giftTargetNpcId = targetNpc.value.id
|
||||
}
|
||||
|
||||
gameStore.player.fleetMissions.push(mission)
|
||||
return true
|
||||
}
|
||||
@@ -552,10 +633,9 @@
|
||||
// 验证是否可以派遣
|
||||
const validation = canSendFleet()
|
||||
if (!validation.valid) {
|
||||
alertDialog.value?.show({
|
||||
title: t('fleetView.sendFailed'),
|
||||
message: validation.errorKey ? t(validation.errorKey) : t('fleetView.sendFailedMessage')
|
||||
})
|
||||
alertDialogTitle.value = t('fleetView.sendFailed')
|
||||
alertDialogMessage.value = validation.errorKey ? t(validation.errorKey) : t('fleetView.sendFailedMessage')
|
||||
alertDialogOpen.value = true
|
||||
return
|
||||
}
|
||||
|
||||
@@ -580,12 +660,12 @@
|
||||
selectedFleet.value[key as ShipType] = 0
|
||||
})
|
||||
cargo.value = { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 }
|
||||
isGiftMode.value = false
|
||||
activeTab.value = 'missions'
|
||||
} else {
|
||||
alertDialog.value?.show({
|
||||
title: t('fleetView.sendFailed'),
|
||||
message: t('fleetView.sendFailedMessage')
|
||||
})
|
||||
alertDialogTitle.value = t('fleetView.sendFailed')
|
||||
alertDialogMessage.value = t('fleetView.sendFailedMessage')
|
||||
alertDialogOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -599,10 +679,9 @@
|
||||
const handleRecallFleet = (missionId: string) => {
|
||||
const success = recallFleet(missionId)
|
||||
if (!success) {
|
||||
alertDialog.value?.show({
|
||||
title: t('fleetView.recallFailed'),
|
||||
message: t('fleetView.recallFailedMessage')
|
||||
})
|
||||
alertDialogTitle.value = t('fleetView.recallFailed')
|
||||
alertDialogMessage.value = t('fleetView.recallFailedMessage')
|
||||
alertDialogOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,23 +26,14 @@
|
||||
|
||||
<!-- 标签切换 -->
|
||||
<div v-if="selectedPlanet" class="flex flex-wrap gap-2 border-b">
|
||||
<Button @click="activeTab = 'resources'" :variant="activeTab === 'resources' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
{{ t('gmView.resources') }}
|
||||
</Button>
|
||||
<Button @click="activeTab = 'buildings'" :variant="activeTab === 'buildings' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
{{ t('gmView.buildings') }}
|
||||
</Button>
|
||||
<Button @click="activeTab = 'research'" :variant="activeTab === 'research' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
{{ t('gmView.research') }}
|
||||
</Button>
|
||||
<Button @click="activeTab = 'ships'" :variant="activeTab === 'ships' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
{{ t('gmView.ships') }}
|
||||
</Button>
|
||||
<Button @click="activeTab = 'defense'" :variant="activeTab === 'defense' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
{{ t('gmView.defense') }}
|
||||
</Button>
|
||||
<Button @click="activeTab = 'officers'" :variant="activeTab === 'officers' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
{{ t('gmView.officers') }}
|
||||
<Button
|
||||
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>
|
||||
|
||||
@@ -177,6 +168,62 @@
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- NPC测试 -->
|
||||
<Card class="border-primary">
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('gmView.npcTesting') || 'NPC Testing' }}</CardTitle>
|
||||
<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="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>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<Button @click="testNPCSpy" variant="outline" class="w-full" :disabled="!selectedNPC">
|
||||
{{ t('gmView.testSpy') || 'Test Spy' }}
|
||||
</Button>
|
||||
<Button @click="testNPCAttack" variant="outline" class="w-full" :disabled="!selectedNPC">
|
||||
{{ t('gmView.testAttack') || 'Test Attack' }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button @click="testNPCSpyAndAttack" variant="default" class="w-full" :disabled="!selectedNPC">
|
||||
{{ t('gmView.testSpyAndAttack') || 'Test Spy & Attack' }}
|
||||
</Button>
|
||||
|
||||
<Button @click="initializeNPCFleet" variant="secondary" class="w-full" :disabled="!selectedNPC">
|
||||
{{ t('gmView.initializeFleet') || 'Initialize NPC Fleet' }}
|
||||
</Button>
|
||||
|
||||
<Button @click="accelerateAllMissions" variant="secondary" class="w-full" :disabled="!selectedNPC">
|
||||
{{ t('gmView.accelerateMissions') || 'Accelerate All Missions (5s)' }}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 危险操作 -->
|
||||
<Card class="border-destructive">
|
||||
<CardHeader>
|
||||
@@ -193,6 +240,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useNPCStore } from '@/stores/npcStore'
|
||||
import { useUniverseStore } from '@/stores/universeStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { useGameConfig } from '@/composables/useGameConfig'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
@@ -202,14 +251,19 @@
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { BuildingType, TechnologyType, ShipType, DefenseType, OfficerType } from '@/types/game'
|
||||
import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const npcStore = useNPCStore()
|
||||
const universeStore = useUniverseStore()
|
||||
const { t } = useI18n()
|
||||
const { BUILDINGS, TECHNOLOGIES, SHIPS, DEFENSES, OFFICERS } = useGameConfig()
|
||||
|
||||
const selectedPlanetId = ref<string>(gameStore.player.planets[0]?.id || '')
|
||||
const activeTab = ref<'resources' | 'buildings' | 'research' | 'ships' | 'defense' | 'officers'>('resources')
|
||||
const officerDays = ref<Record<OfficerType, number>>({} as Record<OfficerType, number>)
|
||||
const selectedNPCId = ref<string>(npcStore.npcs[0]?.id || '')
|
||||
const targetPlanetIndex = ref<string>('0')
|
||||
|
||||
// 初始化军官天数显示
|
||||
Object.values(OfficerType).forEach(officer => {
|
||||
@@ -226,6 +280,14 @@
|
||||
return gameStore.player.planets.find(p => p.id === selectedPlanetId.value)
|
||||
})
|
||||
|
||||
const selectedNPC = computed(() => {
|
||||
return npcStore.npcs.find(npc => npc.id === selectedNPCId.value)
|
||||
})
|
||||
|
||||
const allPlanets = computed(() => {
|
||||
return [...gameStore.player.planets, ...Object.values(universeStore.planets)]
|
||||
})
|
||||
|
||||
const resourceTypes = ['metal', 'crystal', 'deuterium', 'darkMatter'] as const
|
||||
const buildingTypes = Object.values(BuildingType)
|
||||
const technologyTypes = Object.values(TechnologyType)
|
||||
@@ -233,6 +295,16 @@
|
||||
const defenseTypes = Object.values(DefenseType)
|
||||
const officerTypes = Object.values(OfficerType)
|
||||
|
||||
// Tab配置
|
||||
const tabs = [
|
||||
{ value: 'resources' as const, label: 'gmView.resources' },
|
||||
{ value: 'buildings' as const, label: 'gmView.buildings' },
|
||||
{ value: 'research' as const, label: 'gmView.research' },
|
||||
{ value: 'ships' as const, label: 'gmView.ships' },
|
||||
{ value: 'defense' as const, label: 'gmView.defense' },
|
||||
{ value: 'officers' as const, label: 'gmView.officers' }
|
||||
]
|
||||
|
||||
const setResourceAmount = (resource: string, amount: number) => {
|
||||
if (selectedPlanet.value) {
|
||||
selectedPlanet.value.resources[resource as keyof typeof selectedPlanet.value.resources] += amount
|
||||
@@ -288,4 +360,113 @@
|
||||
location.reload()
|
||||
}
|
||||
}
|
||||
|
||||
// NPC测试函数
|
||||
const testNPCSpy = () => {
|
||||
if (!selectedNPC.value) {
|
||||
alert(t('gmView.selectNPCFirst') || 'Please select an NPC first')
|
||||
return
|
||||
}
|
||||
|
||||
const mission = npcBehaviorLogic.forceNPCSpyPlayer(
|
||||
selectedNPC.value,
|
||||
gameStore.player,
|
||||
allPlanets.value,
|
||||
parseInt(targetPlanetIndex.value)
|
||||
)
|
||||
|
||||
if (mission) {
|
||||
// 加速任务到5秒后到达
|
||||
npcBehaviorLogic.accelerateNPCMission(selectedNPC.value, mission.id, 5)
|
||||
alert(`${selectedNPC.value.name} will spy in 5 seconds`)
|
||||
} else {
|
||||
alert(t('gmView.npcNoProbes') || 'NPC does not have spy probes')
|
||||
}
|
||||
}
|
||||
|
||||
const testNPCAttack = () => {
|
||||
if (!selectedNPC.value) {
|
||||
alert(t('gmView.selectNPCFirst') || 'Please select an NPC first')
|
||||
return
|
||||
}
|
||||
|
||||
const mission = npcBehaviorLogic.forceNPCAttackPlayer(
|
||||
selectedNPC.value,
|
||||
gameStore.player,
|
||||
allPlanets.value,
|
||||
parseInt(targetPlanetIndex.value)
|
||||
)
|
||||
|
||||
if (mission) {
|
||||
// 加速任务到5秒后到达
|
||||
npcBehaviorLogic.accelerateNPCMission(selectedNPC.value, mission.id, 5)
|
||||
alert(`${selectedNPC.value.name} will attack in 5 seconds`)
|
||||
} else {
|
||||
alert(t('gmView.npcNoSpyReport') || 'NPC needs to spy first')
|
||||
}
|
||||
}
|
||||
|
||||
const testNPCSpyAndAttack = () => {
|
||||
if (!selectedNPC.value) {
|
||||
alert(t('gmView.selectNPCFirst') || 'Please select an NPC first')
|
||||
return
|
||||
}
|
||||
|
||||
const { spyMission, attackMission } = npcBehaviorLogic.forceNPCSpyAndAttack(
|
||||
selectedNPC.value,
|
||||
gameStore.player,
|
||||
allPlanets.value,
|
||||
parseInt(targetPlanetIndex.value)
|
||||
)
|
||||
|
||||
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`)
|
||||
} else {
|
||||
alert(t('gmView.npcMissionFailed') || 'Failed to create missions')
|
||||
}
|
||||
}
|
||||
|
||||
const accelerateAllMissions = () => {
|
||||
if (!selectedNPC.value) {
|
||||
alert(t('gmView.selectNPCFirst') || 'Please select an NPC first')
|
||||
return
|
||||
}
|
||||
|
||||
const count = npcBehaviorLogic.accelerateAllNPCMissions(selectedNPC.value, 5)
|
||||
alert(`Accelerated ${count} missions to 5 seconds`)
|
||||
}
|
||||
|
||||
// 初始化NPC舰队
|
||||
const initializeNPCFleet = () => {
|
||||
if (!selectedNPC.value) {
|
||||
alert(t('gmView.selectNPCFirst') || 'Please select an NPC first')
|
||||
return
|
||||
}
|
||||
|
||||
// 给NPC的第一个星球添加基础舰队
|
||||
const npcPlanet = selectedNPC.value.planets[0]
|
||||
if (!npcPlanet) {
|
||||
alert('NPC has no planets')
|
||||
return
|
||||
}
|
||||
|
||||
// 添加间谍探测器
|
||||
npcPlanet.fleet[ShipType.EspionageProbe] = (npcPlanet.fleet[ShipType.EspionageProbe] || 0) + 100
|
||||
|
||||
// 添加战斗舰船
|
||||
npcPlanet.fleet[ShipType.LightFighter] = (npcPlanet.fleet[ShipType.LightFighter] || 0) + 500
|
||||
npcPlanet.fleet[ShipType.HeavyFighter] = (npcPlanet.fleet[ShipType.HeavyFighter] || 0) + 300
|
||||
npcPlanet.fleet[ShipType.Cruiser] = (npcPlanet.fleet[ShipType.Cruiser] || 0) + 200
|
||||
npcPlanet.fleet[ShipType.Battleship] = (npcPlanet.fleet[ShipType.Battleship] || 0) + 100
|
||||
npcPlanet.fleet[ShipType.Bomber] = (npcPlanet.fleet[ShipType.Bomber] || 0) + 50
|
||||
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`
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -8,12 +8,18 @@
|
||||
<CardTitle>{{ t('galaxyView.selectCoordinates') }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3 sm:gap-4">
|
||||
<div
|
||||
:class="[
|
||||
'grid gap-3 sm:gap-4',
|
||||
highlightedNpc ? 'grid-cols-2 sm:grid-cols-4' : isInHomePlanetSystem ? 'grid-cols-2' : 'grid-cols-2 sm:grid-cols-3'
|
||||
]"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<Label for="select-galaxy" class="text-xs sm:text-sm">{{ t('galaxyView.galaxy') }}</Label>
|
||||
<Select
|
||||
:key="gameStore.locale"
|
||||
:model-value="String(selectedGalaxy)"
|
||||
:modal="false"
|
||||
@update:model-value="
|
||||
val => {
|
||||
selectedGalaxy = Number(val)
|
||||
@@ -24,7 +30,7 @@
|
||||
<SelectTrigger id="select-galaxy" class="w-full">
|
||||
<SelectValue :placeholder="t('galaxyView.selectGalaxy')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectContent position="popper">
|
||||
<SelectItem v-for="g in 9" :key="g" :value="String(g)">{{ t('galaxyView.galaxy') }} {{ g }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -34,6 +40,7 @@
|
||||
<Select
|
||||
:key="`${gameStore.locale}-system`"
|
||||
:model-value="String(selectedSystem)"
|
||||
:modal="false"
|
||||
@update:model-value="
|
||||
val => {
|
||||
selectedSystem = Number(val)
|
||||
@@ -44,16 +51,101 @@
|
||||
<SelectTrigger id="select-system" class="w-full">
|
||||
<SelectValue :placeholder="t('galaxyView.selectSystem')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectContent position="popper">
|
||||
<SelectItem v-for="s in 10" :key="s" :value="String(s)">{{ t('galaxyView.system') }} {{ s }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="col-span-2 sm:col-span-1 flex items-end">
|
||||
<Button @click="goToCurrentPlanet" variant="outline" class="w-full">
|
||||
<Home class="h-4 w-4 mr-2" />
|
||||
{{ t('galaxyView.myPlanet') }}
|
||||
</Button>
|
||||
<div v-if="!isInHomePlanetSystem" :class="highlightedNpc ? '' : 'col-span-2 sm:col-span-1'" class="space-y-2">
|
||||
<Label class="text-xs sm:text-sm opacity-0">{{ t('galaxyView.myPlanets') }}</Label>
|
||||
<!-- 不在母星星系时显示Popover选择 -->
|
||||
<Popover>
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="outline" class="w-full">
|
||||
<Home class="h-4 w-4 mr-2" />
|
||||
{{ t('galaxyView.myPlanets') }}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-72 p-2" align="start">
|
||||
<div class="space-y-1">
|
||||
<div class="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
|
||||
{{ t('galaxyView.selectPlanetToView') }}
|
||||
</div>
|
||||
<Button
|
||||
v-for="p in myPlanets"
|
||||
:key="p.id"
|
||||
@click="goToPlanet(p)"
|
||||
:disabled="p.position.galaxy === currentGalaxy && p.position.system === currentSystem"
|
||||
variant="ghost"
|
||||
:class="[
|
||||
'w-full justify-start h-auto py-2 px-2 text-left',
|
||||
p.position.galaxy === currentGalaxy &&
|
||||
p.position.system === currentSystem &&
|
||||
'bg-blue-100 dark:bg-blue-950/50 border border-blue-400 dark:border-blue-600'
|
||||
]"
|
||||
size="sm"
|
||||
>
|
||||
<div class="flex items-start gap-2 w-full min-w-0">
|
||||
<Globe class="h-4 w-4 flex-shrink-0 mt-0.5" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-1.5 mb-0.5">
|
||||
<span class="truncate font-medium text-sm">{{ p.name }}</span>
|
||||
<Badge v-if="p.isMoon" variant="outline" class="text-[10px] px-1 py-0 h-4">
|
||||
{{ t('planet.moon') }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="text-[11px] text-muted-foreground">
|
||||
[{{ p.position.galaxy }}:{{ p.position.system }}:{{ p.position.position }}]
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<!-- NPC星球列表 -->
|
||||
<div v-if="highlightedNpc" :class="isInHomePlanetSystem ? 'col-span-2 sm:col-span-2' : ''" class="space-y-2">
|
||||
<Label class="text-xs sm:text-sm opacity-0">{{ t('galaxyView.npcPlanets') }}</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="outline" class="w-full border-yellow-400 dark:border-yellow-600">
|
||||
<Globe class="h-4 w-4 mr-2" />
|
||||
{{ highlightedNpc.name }} ({{ highlightedNpc.planets.length }})
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-72 p-2" align="start">
|
||||
<div class="space-y-1">
|
||||
<div class="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
|
||||
{{ t('galaxyView.selectPlanetToView') }}
|
||||
</div>
|
||||
<Button
|
||||
v-for="p in highlightedNpc.planets"
|
||||
:key="p.id"
|
||||
@click="goToPlanet(p)"
|
||||
:disabled="p.position.galaxy === currentGalaxy && p.position.system === currentSystem"
|
||||
variant="ghost"
|
||||
:class="[
|
||||
'w-full justify-start h-auto py-2 px-2 text-left',
|
||||
p.position.galaxy === currentGalaxy &&
|
||||
p.position.system === currentSystem &&
|
||||
'bg-yellow-100 dark:bg-yellow-950/50 border border-yellow-400 dark:border-yellow-600'
|
||||
]"
|
||||
size="sm"
|
||||
>
|
||||
<div class="flex items-start gap-2 w-full min-w-0">
|
||||
<Globe class="h-4 w-4 flex-shrink-0 mt-0.5" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="truncate font-medium text-sm mb-0.5">{{ p.name }}</div>
|
||||
<div class="text-[11px] text-muted-foreground">
|
||||
[{{ p.position.galaxy }}:{{ p.position.system }}:{{ p.position.position }}]
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -72,8 +164,32 @@
|
||||
:key="slot.position"
|
||||
class="flex items-center gap-2 sm:gap-4 p-2 sm:p-3 border rounded-lg hover:bg-muted/50 transition-colors"
|
||||
:class="{
|
||||
'bg-blue-50 dark:bg-blue-950 border-blue-300 dark:border-blue-700': isMyPlanet(slot.planet),
|
||||
'bg-muted/30': !slot.planet
|
||||
// 空位置
|
||||
'bg-muted/30': !slot.planet,
|
||||
// 我的星球 - 蓝色
|
||||
'bg-blue-50 dark:bg-blue-950 border-blue-300 dark:border-blue-700': slot.planet && isMyPlanet(slot.planet),
|
||||
// 高亮NPC - 黄色
|
||||
'bg-yellow-50 dark:bg-yellow-950/30 border-yellow-400 dark:border-yellow-600 ring-2 ring-yellow-400 dark:ring-yellow-500':
|
||||
slot.planet && isHighlightedNpcPlanet(slot.planet) && !isMyPlanet(slot.planet),
|
||||
// 友好NPC - 绿色
|
||||
'bg-green-50 dark:bg-green-950/30 border-green-300 dark:border-green-700':
|
||||
slot.planet &&
|
||||
!isMyPlanet(slot.planet) &&
|
||||
!isHighlightedNpcPlanet(slot.planet) &&
|
||||
getRelation(slot.planet)?.status === RelationStatus.Friendly,
|
||||
// 敌对NPC - 红色
|
||||
'bg-red-50 dark:bg-red-950/30 border-red-300 dark:border-red-700':
|
||||
slot.planet &&
|
||||
!isMyPlanet(slot.planet) &&
|
||||
!isHighlightedNpcPlanet(slot.planet) &&
|
||||
getRelation(slot.planet)?.status === RelationStatus.Hostile,
|
||||
// 中立NPC - 灰色
|
||||
'bg-gray-50 dark:bg-gray-950/30 border-gray-300 dark:border-gray-700':
|
||||
slot.planet &&
|
||||
!isMyPlanet(slot.planet) &&
|
||||
!isHighlightedNpcPlanet(slot.planet) &&
|
||||
getPlanetNPC(slot.planet) &&
|
||||
(!getRelation(slot.planet) || getRelation(slot.planet)?.status === RelationStatus.Neutral)
|
||||
}"
|
||||
>
|
||||
<!-- 位置编号 -->
|
||||
@@ -84,32 +200,104 @@
|
||||
<!-- 星球信息 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div v-if="slot.planet" class="space-y-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="font-semibold text-sm sm:text-base truncate">{{ slot.planet.name }}</h3>
|
||||
<Badge v-if="isMyPlanet(slot.planet)" variant="default" class="text-xs">{{ t('galaxyView.mine') }}</Badge>
|
||||
<Badge v-else variant="secondary" class="text-xs">{{ t('galaxyView.hostile') }}</Badge>
|
||||
<!-- 移动端:垂直布局 / 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>
|
||||
<!-- 徽章组 -->
|
||||
<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>
|
||||
<!-- 残骸场徽章 - 紧凑显示 -->
|
||||
<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 class="hidden sm:inline">{{ 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>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
<!-- 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="text-sm text-muted-foreground">{{ t('galaxyView.emptySlot') }}</div>
|
||||
|
||||
<!-- 残骸场信息 -->
|
||||
<div v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)" class="mt-2 p-2 bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800 rounded text-xs">
|
||||
<div class="flex items-center gap-2 text-amber-700 dark:text-amber-400 font-medium mb-1">
|
||||
<span>{{ t('galaxyView.debrisField') }}</span>
|
||||
</div>
|
||||
<div class="flex gap-3 text-xs">
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="text-muted-foreground">{{ t('resources.metal') }}:</span>
|
||||
<span class="font-medium">{{ formatNumber(getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)!.resources.metal) }}</span>
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="text-muted-foreground">{{ t('resources.crystal') }}:</span>
|
||||
<span class="font-medium">{{ formatNumber(getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)!.resources.crystal) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<!-- 空位置 -->
|
||||
<div 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>
|
||||
|
||||
@@ -136,6 +324,16 @@
|
||||
<p>{{ t('galaxyView.attack') }}</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 sm:h-4 sm:w-4" />
|
||||
</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">
|
||||
@@ -158,7 +356,12 @@
|
||||
</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">
|
||||
<Button
|
||||
@click="showPlanetActions(slot.planet, 'recycle', slot.position)"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-8 w-8 p-0"
|
||||
>
|
||||
<Recycle class="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
@@ -174,44 +377,122 @@
|
||||
</Card>
|
||||
|
||||
<!-- 快速派遣对话框 -->
|
||||
<AlertDialog ref="actionDialog" />
|
||||
<AlertDialog :open="alertDialogOpen" @update:open="alertDialogOpen = $event">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ alertDialogTitle }}</AlertDialogTitle>
|
||||
<AlertDialogDescription class="whitespace-pre-line">
|
||||
{{ alertDialogMessage }}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{{ t('common.cancel') }}</AlertDialogCancel>
|
||||
<AlertDialogAction @click="handleAlertDialogConfirm">{{ t('common.confirm') }}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useUniverseStore } from '@/stores/universeStore'
|
||||
import { useNPCStore } from '@/stores/npcStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import type { Planet, DebrisField } from '@/types/game'
|
||||
import { RelationStatus } from '@/types/game'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import AlertDialog from '@/components/AlertDialog.vue'
|
||||
import { Home, Eye, Sword, Rocket, Recycle } from 'lucide-vue-next'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
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 { useRouter, useRoute } from 'vue-router'
|
||||
import * as gameLogic from '@/logic/gameLogic'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const universeStore = useUniverseStore()
|
||||
const npcStore = useNPCStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
const actionDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
|
||||
|
||||
// AlertDialog 状态
|
||||
const alertDialogOpen = ref(false)
|
||||
const alertDialogTitle = ref('')
|
||||
const alertDialogMessage = ref('')
|
||||
const alertDialogConfirmAction = ref<(() => void) | null>(null)
|
||||
|
||||
const selectedGalaxy = ref(1)
|
||||
const selectedSystem = ref(1)
|
||||
const currentGalaxy = ref(1)
|
||||
const currentSystem = ref(1)
|
||||
|
||||
// 保存要高亮的NPC ID(从URL参数初始化,之后即使URL清除也保持)
|
||||
const highlightNpcId = ref<string | undefined>(undefined)
|
||||
|
||||
// 获取要高亮的NPC对象
|
||||
const highlightedNpc = computed(() => {
|
||||
if (!highlightNpcId.value) return null
|
||||
return npcStore.npcs.find(n => n.id === highlightNpcId.value) || null
|
||||
})
|
||||
|
||||
const systemSlots = ref<Array<{ position: number; planet: Planet | null }>>([])
|
||||
|
||||
// 获取玩家的母星
|
||||
const homePlanet = computed(() => {
|
||||
// 第一个非月球星球就是母星
|
||||
return gameStore.player.planets.find(p => !p.isMoon)
|
||||
})
|
||||
|
||||
// 获取玩家所有非月球星球
|
||||
const myPlanets = computed(() => {
|
||||
return gameStore.player.planets.filter(p => !p.isMoon)
|
||||
})
|
||||
|
||||
// 判断当前是否在母星所在星系
|
||||
const isInHomePlanetSystem = computed(() => {
|
||||
if (!homePlanet.value) return false
|
||||
return currentGalaxy.value === homePlanet.value.position.galaxy && currentSystem.value === homePlanet.value.position.system
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// 默认显示当前星球所在的星系
|
||||
if (gameStore.currentPlanet) {
|
||||
// 从URL参数中读取并保存高亮NPC ID
|
||||
if (route.query.highlightNpc) {
|
||||
highlightNpcId.value = route.query.highlightNpc as string
|
||||
}
|
||||
|
||||
// 优先检查URL参数中的星系坐标
|
||||
const queryGalaxy = route.query.galaxy ? Number(route.query.galaxy) : null
|
||||
const querySystem = route.query.system ? Number(route.query.system) : null
|
||||
|
||||
if (queryGalaxy && querySystem) {
|
||||
// 如果URL中有坐标参数,跳转到指定星系
|
||||
currentGalaxy.value = queryGalaxy
|
||||
currentSystem.value = querySystem
|
||||
selectedGalaxy.value = queryGalaxy
|
||||
selectedSystem.value = querySystem
|
||||
loadSystem()
|
||||
|
||||
// 立即清除URL参数,但保持本地变量中的highlightNpcId
|
||||
clearUrlParams()
|
||||
} else if (gameStore.currentPlanet) {
|
||||
// 否则默认显示当前星球所在的星系
|
||||
currentGalaxy.value = gameStore.currentPlanet.position.galaxy
|
||||
currentSystem.value = gameStore.currentPlanet.position.system
|
||||
selectedGalaxy.value = currentGalaxy.value
|
||||
@@ -225,11 +506,12 @@
|
||||
return positions.map(pos => {
|
||||
const key = gameLogic.generatePositionKey(galaxy, system, pos.position)
|
||||
// 先从玩家星球中查找,再从宇宙地图中查找
|
||||
const planet = gameStore.player.planets.find(p =>
|
||||
p.position.galaxy === galaxy &&
|
||||
p.position.system === system &&
|
||||
p.position.position === pos.position
|
||||
) || universeStore.planets[key] || null
|
||||
const planet =
|
||||
gameStore.player.planets.find(
|
||||
p => p.position.galaxy === galaxy && p.position.system === system && p.position.position === pos.position
|
||||
) ||
|
||||
universeStore.planets[key] ||
|
||||
null
|
||||
return { position: pos.position, planet }
|
||||
})
|
||||
}
|
||||
@@ -240,6 +522,13 @@
|
||||
return universeStore.debrisFields[debrisId] || null
|
||||
}
|
||||
|
||||
// 清除URL参数
|
||||
const clearUrlParams = () => {
|
||||
if (route.query.highlightNpc || route.query.galaxy || route.query.system) {
|
||||
router.replace({ query: {} })
|
||||
}
|
||||
}
|
||||
|
||||
// 加载星系
|
||||
const loadSystem = () => {
|
||||
currentGalaxy.value = selectedGalaxy.value
|
||||
@@ -247,15 +536,13 @@
|
||||
systemSlots.value = getSystemPlanets(currentGalaxy.value, currentSystem.value)
|
||||
}
|
||||
|
||||
// 跳转到当前星球
|
||||
const goToCurrentPlanet = () => {
|
||||
if (gameStore.currentPlanet) {
|
||||
currentGalaxy.value = gameStore.currentPlanet.position.galaxy
|
||||
currentSystem.value = gameStore.currentPlanet.position.system
|
||||
selectedGalaxy.value = currentGalaxy.value
|
||||
selectedSystem.value = currentSystem.value
|
||||
loadSystem()
|
||||
}
|
||||
// 跳转到指定星球的星系
|
||||
const goToPlanet = (planet: Planet) => {
|
||||
currentGalaxy.value = planet.position.galaxy
|
||||
currentSystem.value = planet.position.system
|
||||
selectedGalaxy.value = currentGalaxy.value
|
||||
selectedSystem.value = currentSystem.value
|
||||
systemSlots.value = getSystemPlanets(currentGalaxy.value, currentSystem.value)
|
||||
}
|
||||
|
||||
// 判断是否为我的星球
|
||||
@@ -264,14 +551,95 @@
|
||||
return planet.ownerId === gameStore.player.id
|
||||
}
|
||||
|
||||
// 判断星球是否属于高亮显示的NPC
|
||||
const isHighlightedNpcPlanet = (planet: Planet | null): boolean => {
|
||||
if (!planet || !highlightNpcId.value) return false
|
||||
const npc = npcStore.npcs.find(n => n.id === highlightNpcId.value)
|
||||
if (!npc) return false
|
||||
return npc.planets.some(p => p.id === planet.id)
|
||||
}
|
||||
|
||||
// 获取星球所属的NPC
|
||||
const getPlanetNPC = (planet: Planet | null) => {
|
||||
if (!planet || isMyPlanet(planet)) return null
|
||||
// 通过坐标匹配,而不是ID,因为universeStore中的星球和npcStore中的星球可能是不同的对象
|
||||
return npcStore.npcs.find(npc =>
|
||||
npc.planets.some(
|
||||
p =>
|
||||
p.position.galaxy === planet.position.galaxy &&
|
||||
p.position.system === planet.position.system &&
|
||||
p.position.position === planet.position.position
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// 获取外交关系
|
||||
const getRelation = (planet: Planet | null) => {
|
||||
const npc = getPlanetNPC(planet)
|
||||
if (!npc) return null
|
||||
return gameStore.player.diplomaticRelations?.[npc.id]
|
||||
}
|
||||
|
||||
// 获取关系状态Badge样式
|
||||
const getRelationBadgeVariant = (planet: Planet | null) => {
|
||||
const relation = getRelation(planet)
|
||||
if (!relation) return 'secondary'
|
||||
|
||||
switch (relation.status) {
|
||||
case RelationStatus.Friendly:
|
||||
return 'default'
|
||||
case RelationStatus.Hostile:
|
||||
return 'destructive'
|
||||
default:
|
||||
return 'secondary'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取关系状态文本
|
||||
const getRelationStatusText = (planet: Planet | null) => {
|
||||
const relation = getRelation(planet)
|
||||
if (!relation) return t('diplomacy.status.neutral')
|
||||
|
||||
switch (relation.status) {
|
||||
case RelationStatus.Friendly:
|
||||
return t('diplomacy.status.friendly')
|
||||
case RelationStatus.Hostile:
|
||||
return t('diplomacy.status.hostile')
|
||||
default:
|
||||
return t('diplomacy.status.neutral')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取好感度值
|
||||
const getReputationValue = (planet: Planet | null): number | null => {
|
||||
const relation = getRelation(planet)
|
||||
return relation?.reputation ?? null
|
||||
}
|
||||
|
||||
// 获取好感度颜色
|
||||
const getReputationColor = (reputation: number | null) => {
|
||||
if (reputation === null) return 'text-muted-foreground'
|
||||
if (reputation >= 20) return 'text-green-600 dark:text-green-400'
|
||||
if (reputation <= -20) return 'text-red-600 dark:text-red-400'
|
||||
return 'text-muted-foreground'
|
||||
}
|
||||
|
||||
// 切换到指定星球
|
||||
const switchToPlanet = (planetId: string) => {
|
||||
gameStore.currentPlanetId = planetId
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
// AlertDialog 确认处理
|
||||
const handleAlertDialogConfirm = () => {
|
||||
if (alertDialogConfirmAction.value) {
|
||||
alertDialogConfirmAction.value()
|
||||
}
|
||||
alertDialogOpen.value = false
|
||||
}
|
||||
|
||||
// 显示星球操作
|
||||
const showPlanetActions = (planet: Planet | null, action: 'spy' | 'attack' | 'colonize' | 'recycle', position?: number) => {
|
||||
const showPlanetActions = (planet: Planet | null, action: 'spy' | 'attack' | 'colonize' | 'recycle' | 'gift', position?: number) => {
|
||||
const targetPos = planet ? planet.position : { galaxy: currentGalaxy.value, system: currentSystem.value, position: position! }
|
||||
const coordinates = `${targetPos.galaxy}:${targetPos.system}:${targetPos.position}`
|
||||
|
||||
@@ -289,23 +657,27 @@
|
||||
} else if (action === 'recycle') {
|
||||
title = t('galaxyView.recyclePlanetTitle')
|
||||
message = t('galaxyView.recyclePlanetMessage').replace('{coordinates}', coordinates)
|
||||
} else if (action === 'gift') {
|
||||
title = t('galaxyView.giftPlanetTitle')
|
||||
message = t('galaxyView.giftPlanetMessage').replace('{coordinates}', coordinates)
|
||||
}
|
||||
|
||||
actionDialog.value?.show({
|
||||
title,
|
||||
message,
|
||||
onConfirm: () => {
|
||||
// 跳转到舰队页面并填充目标坐标
|
||||
router.push({
|
||||
path: '/fleet',
|
||||
query: {
|
||||
galaxy: targetPos.galaxy,
|
||||
system: targetPos.system,
|
||||
position: targetPos.position,
|
||||
mission: action
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
// 设置对话框状态
|
||||
alertDialogTitle.value = title
|
||||
alertDialogMessage.value = message
|
||||
alertDialogConfirmAction.value = () => {
|
||||
// 跳转到舰队页面并填充目标坐标
|
||||
router.push({
|
||||
path: '/fleet',
|
||||
query: {
|
||||
galaxy: targetPos.galaxy,
|
||||
system: targetPos.system,
|
||||
position: targetPos.position,
|
||||
mission: action === 'gift' ? undefined : action,
|
||||
gift: action === 'gift' ? '1' : undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
alertDialogOpen.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -3,85 +3,293 @@
|
||||
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('messagesView.title') }}</h1>
|
||||
|
||||
<!-- 标签切换 -->
|
||||
<div class="flex gap-2 border-b">
|
||||
<Button @click="activeTab = 'battles'" :variant="activeTab === 'battles' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
<Sword class="h-4 w-4 mr-2" />
|
||||
{{ t('messagesView.battles') }}
|
||||
<Badge v-if="unreadBattles > 0" variant="destructive" class="ml-2">{{ unreadBattles }}</Badge>
|
||||
</Button>
|
||||
<Button @click="activeTab = 'spy'" :variant="activeTab === 'spy' ? 'default' : 'ghost'" class="rounded-b-none">
|
||||
<Eye class="h-4 w-4 mr-2" />
|
||||
{{ t('messagesView.spy') }}
|
||||
<Badge v-if="unreadSpyReports > 0" variant="destructive" class="ml-2">{{ unreadSpyReports }}</Badge>
|
||||
</Button>
|
||||
</div>
|
||||
<Tabs v-model="activeTab" class="w-full">
|
||||
<TabsList class="grid w-full grid-cols-2 sm:grid-cols-4" :tab-count="4">
|
||||
<TabsTrigger v-for="tab in tabs" :key="tab.value" :value="tab.value" class="flex items-center justify-center gap-1 px-2">
|
||||
<component :is="tab.icon" class="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
<span class="text-xs sm:text-sm truncate">{{ tab.label }}</span>
|
||||
<Badge v-if="tab.unreadCount > 0" variant="destructive" class="hidden sm:flex ml-1">
|
||||
{{ tab.unreadCount }}
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<!-- 战斗报告列表 -->
|
||||
<div v-if="activeTab === 'battles'" class="space-y-2">
|
||||
<Card v-if="gameStore.player.battleReports.length === 0">
|
||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('messagesView.noBattleReports') }}</CardContent>
|
||||
</Card>
|
||||
<!-- 战斗报告列表 -->
|
||||
<TabsContent value="battles" class="mt-4 space-y-2">
|
||||
<Card v-if="gameStore.player.battleReports.length === 0">
|
||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('messagesView.noBattleReports') }}</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
v-for="report in sortedBattleReports"
|
||||
:key="report.id"
|
||||
@click="openBattleReport(report)"
|
||||
class="cursor-pointer hover:shadow-md transition-shadow"
|
||||
>
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<Sword class="h-4 w-4 flex-shrink-0" />
|
||||
<CardTitle class="text-base sm:text-lg">{{ t('messagesView.battleReport') }}</CardTitle>
|
||||
<Badge v-if="!report.read" variant="default" class="text-xs">{{ t('messagesView.unread') }}</Badge>
|
||||
<Badge
|
||||
:variant="report.winner === 'attacker' ? 'default' : report.winner === 'defender' ? 'destructive' : 'secondary'"
|
||||
class="text-xs"
|
||||
<Card
|
||||
v-for="report in sortedBattleReports"
|
||||
:key="report.id"
|
||||
@click="openBattleReport(report)"
|
||||
class="cursor-pointer hover:shadow-md transition-shadow"
|
||||
>
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<Sword class="h-4 w-4 flex-shrink-0" />
|
||||
<CardTitle class="text-base sm:text-lg">{{ t('messagesView.battleReport') }}</CardTitle>
|
||||
<Badge v-if="!report.read" variant="default" class="text-xs">{{ t('messagesView.unread') }}</Badge>
|
||||
<Badge :variant="getBattleResultVariant(report)" class="text-xs">
|
||||
{{ getBattleResultText(report) }}
|
||||
</Badge>
|
||||
</div>
|
||||
<Button @click.stop="deleteBattleReport(report.id)" variant="ghost" size="icon" class="h-8 w-8 flex-shrink-0">
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<CardDescription class="text-xs sm:text-sm">
|
||||
{{ formatDate(report.timestamp) }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 间谍报告列表(合并:侦查报告 + 被侦查通知) -->
|
||||
<TabsContent value="spy" class="mt-4 space-y-2">
|
||||
<Card v-if="gameStore.player.spyReports.length === 0 && sortedSpiedNotifications.length === 0">
|
||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('messagesView.noSpyReports') }}</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 侦查报告 -->
|
||||
<Card
|
||||
v-for="report in sortedSpyReports"
|
||||
:key="report.id"
|
||||
@click="openSpyReport(report)"
|
||||
class="cursor-pointer hover:shadow-md transition-shadow"
|
||||
>
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<Eye class="h-4 w-4 flex-shrink-0" />
|
||||
<CardTitle class="text-base sm:text-lg">{{ t('messagesView.spyReport') }}</CardTitle>
|
||||
<Badge v-if="!report.read" variant="default" class="text-xs">{{ t('messagesView.unread') }}</Badge>
|
||||
<Badge variant="outline" class="text-xs">{{ report.targetPlanetId }}</Badge>
|
||||
</div>
|
||||
<Button @click.stop="deleteSpyReport(report.id)" variant="ghost" size="icon" class="h-8 w-8 flex-shrink-0">
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<CardDescription class="text-xs sm:text-sm">
|
||||
{{ formatDate(report.timestamp) }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<!-- 被侦查通知 -->
|
||||
<Card
|
||||
v-for="notification in sortedSpiedNotifications"
|
||||
:key="notification.id"
|
||||
@click="openSpiedNotification(notification)"
|
||||
class="cursor-pointer hover:shadow-md transition-shadow"
|
||||
>
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<AlertTriangle class="h-4 w-4 flex-shrink-0 text-destructive" />
|
||||
<CardTitle class="text-base sm:text-lg">{{ t('messagesView.spiedNotification') }}</CardTitle>
|
||||
<Badge v-if="!notification.read" variant="default" class="text-xs">{{ t('messagesView.unread') }}</Badge>
|
||||
<Badge :variant="notification.detectionSuccess ? 'destructive' : 'secondary'" class="text-xs">
|
||||
{{ notification.detectionSuccess ? t('messagesView.detected') : t('messagesView.undetected') }}
|
||||
</Badge>
|
||||
</div>
|
||||
<Button @click.stop="deleteSpiedNotification(notification.id)" variant="ghost" size="icon" class="h-8 w-8 flex-shrink-0">
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<CardDescription class="text-xs sm:text-sm">
|
||||
{{ notification.npcName }} → {{ notification.targetPlanetName }} · {{ formatDate(notification.timestamp) }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<!-- NPC相关消息(活动、礼物、被拒绝) -->
|
||||
<TabsContent value="npc" class="mt-4 space-y-2">
|
||||
<Card
|
||||
v-if="
|
||||
sortedNPCActivityNotifications.length === 0 &&
|
||||
sortedGiftNotifications.length === 0 &&
|
||||
sortedGiftRejectedNotifications.length === 0
|
||||
"
|
||||
>
|
||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('messagesView.noNPCActivity') }}</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- NPC活动通知 -->
|
||||
<Card
|
||||
v-for="notification in sortedNPCActivityNotifications"
|
||||
:key="notification.id"
|
||||
@click="openNPCActivityNotification(notification)"
|
||||
class="cursor-pointer hover:shadow-md transition-shadow"
|
||||
>
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<Recycle class="h-4 w-4 flex-shrink-0 text-blue-500" />
|
||||
<CardTitle class="text-base sm:text-lg">{{ t('messagesView.npcRecycleActivity') }}</CardTitle>
|
||||
<Badge v-if="!notification.read" variant="default" class="text-xs">{{ t('messagesView.unread') }}</Badge>
|
||||
</div>
|
||||
<Button
|
||||
@click.stop="deleteNPCActivityNotification(notification.id)"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 flex-shrink-0"
|
||||
>
|
||||
{{ report.winner === 'attacker' ? t('messagesView.victory') : report.winner === 'defender' ? t('messagesView.defeat') : t('messagesView.draw') }}
|
||||
</Badge>
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button @click.stop="deleteBattleReport(report.id)" variant="ghost" size="icon" class="h-8 w-8 flex-shrink-0">
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<CardDescription class="text-xs sm:text-sm">
|
||||
{{ formatDate(report.timestamp) }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</div>
|
||||
<CardDescription class="text-xs sm:text-sm">
|
||||
{{ notification.npcName }} →
|
||||
{{
|
||||
notification.targetPlanetName ||
|
||||
`[${notification.targetPosition.galaxy}:${notification.targetPosition.system}:${notification.targetPosition.position}]`
|
||||
}}
|
||||
· {{ formatDate(notification.timestamp) }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<!-- 间谍报告列表 -->
|
||||
<div v-if="activeTab === 'spy'" class="space-y-2">
|
||||
<Card v-if="gameStore.player.spyReports.length === 0">
|
||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('messagesView.noSpyReports') }}</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
v-for="report in sortedSpyReports"
|
||||
:key="report.id"
|
||||
@click="openSpyReport(report)"
|
||||
class="cursor-pointer hover:shadow-md transition-shadow"
|
||||
>
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<Eye class="h-4 w-4 flex-shrink-0" />
|
||||
<CardTitle class="text-base sm:text-lg">{{ t('messagesView.spyReport') }}</CardTitle>
|
||||
<Badge v-if="!report.read" variant="default" class="text-xs">{{ t('messagesView.unread') }}</Badge>
|
||||
<Badge variant="outline" class="text-xs">{{ report.targetPlanetId }}</Badge>
|
||||
<!-- 礼物通知 -->
|
||||
<Card
|
||||
v-for="gift in sortedGiftNotifications"
|
||||
:key="gift.id"
|
||||
@click="markGiftAsRead(gift)"
|
||||
class="hover:shadow-md transition-shadow"
|
||||
>
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<Gift class="h-4 w-4 flex-shrink-0 text-green-600" />
|
||||
<CardTitle class="text-base sm:text-lg">{{ t('messagesView.giftFrom').replace('{npcName}', gift.fromNpcName) }}</CardTitle>
|
||||
<Badge v-if="!gift.read" variant="default" class="text-xs">{{ t('messagesView.unread') }}</Badge>
|
||||
</div>
|
||||
<Button @click.stop="deleteGiftNotification(gift.id)" variant="ghost" size="icon" class="h-8 w-8 flex-shrink-0">
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button @click.stop="deleteSpyReport(report.id)" variant="ghost" size="icon" class="h-8 w-8 flex-shrink-0">
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<CardDescription class="text-xs sm:text-sm">
|
||||
{{ formatDate(report.timestamp) }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</div>
|
||||
<CardDescription class="text-xs sm:text-sm">{{ formatDate(gift.timestamp) }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="space-y-3">
|
||||
<div class="text-sm">
|
||||
<div class="font-semibold mb-1">{{ t('messagesView.giftResources') }}:</div>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div v-if="gift.resources.metal > 0">{{ t('resources.metal') }}: {{ gift.resources.metal.toLocaleString() }}</div>
|
||||
<div v-if="gift.resources.crystal > 0">{{ t('resources.crystal') }}: {{ gift.resources.crystal.toLocaleString() }}</div>
|
||||
<div v-if="gift.resources.deuterium > 0">
|
||||
{{ t('resources.deuterium') }}: {{ gift.resources.deuterium.toLocaleString() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground">
|
||||
{{ t('messagesView.expectedReputation') }}:
|
||||
<span class="text-green-600">+{{ gift.expectedReputationGain }}</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button @click.stop="acceptGift(gift)" variant="default" size="sm" class="flex-1">
|
||||
<Check class="h-4 w-4 mr-1" />
|
||||
{{ t('messagesView.acceptGift') }}
|
||||
</Button>
|
||||
<Button @click.stop="rejectGift(gift)" variant="outline" size="sm" class="flex-1">
|
||||
<Ban class="h-4 w-4 mr-1" />
|
||||
{{ t('messagesView.rejectGift') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 礼物被拒绝通知 -->
|
||||
<Card
|
||||
v-for="rejection in sortedGiftRejectedNotifications"
|
||||
:key="rejection.id"
|
||||
@click="markGiftRejectedAsRead(rejection)"
|
||||
class="hover:shadow-md transition-shadow"
|
||||
>
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<Ban class="h-4 w-4 flex-shrink-0 text-red-600" />
|
||||
<CardTitle class="text-base sm:text-lg">
|
||||
{{ t('messagesView.giftRejectedBy').replace('{npcName}', rejection.npcName) }}
|
||||
</CardTitle>
|
||||
<Badge v-if="!rejection.read" variant="default" class="text-xs">{{ t('messagesView.unread') }}</Badge>
|
||||
</div>
|
||||
<Button @click.stop="deleteGiftRejectedNotification(rejection.id)" variant="ghost" size="icon" class="h-8 w-8 flex-shrink-0">
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<CardDescription class="text-xs sm:text-sm">{{ formatDate(rejection.timestamp) }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="space-y-2">
|
||||
<div class="text-sm">
|
||||
<div class="font-semibold mb-1">{{ t('messagesView.rejectedResources') }}:</div>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div v-if="rejection.rejectedResources.metal > 0">
|
||||
{{ t('resources.metal') }}: {{ rejection.rejectedResources.metal.toLocaleString() }}
|
||||
</div>
|
||||
<div v-if="rejection.rejectedResources.crystal > 0">
|
||||
{{ t('resources.crystal') }}: {{ rejection.rejectedResources.crystal.toLocaleString() }}
|
||||
</div>
|
||||
<div v-if="rejection.rejectedResources.deuterium > 0">
|
||||
{{ t('resources.deuterium') }}: {{ rejection.rejectedResources.deuterium.toLocaleString() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground">
|
||||
{{ t('messagesView.currentReputation') }}:
|
||||
<span :class="rejection.currentReputation >= 0 ? 'text-green-600' : 'text-red-600'">{{ rejection.currentReputation }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground">
|
||||
{{ t('messagesView.rejectionReason.' + rejection.reason) }}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 任务报告列表 -->
|
||||
<TabsContent value="missions" class="mt-4 space-y-2">
|
||||
<Card v-if="sortedMissionReports.length === 0">
|
||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('messagesView.noMissionReports') }}</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
v-for="report in sortedMissionReports"
|
||||
:key="report.id"
|
||||
@click="openMissionReport(report)"
|
||||
class="cursor-pointer hover:shadow-md transition-shadow"
|
||||
>
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<Package class="h-4 w-4 flex-shrink-0" />
|
||||
<CardTitle class="text-base sm:text-lg">{{ getMissionTypeName(report.missionType) }}</CardTitle>
|
||||
<Badge v-if="!report.read" variant="default" class="text-xs">{{ t('messagesView.unread') }}</Badge>
|
||||
<Badge :variant="report.success ? 'default' : 'destructive'" class="text-xs">
|
||||
{{ report.success ? t('messagesView.success') : t('messagesView.failed') }}
|
||||
</Badge>
|
||||
</div>
|
||||
<Button @click.stop="deleteMissionReport(report.id)" variant="ghost" size="icon" class="h-8 w-8 flex-shrink-0">
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<CardDescription class="text-xs sm:text-sm">
|
||||
{{ report.originPlanetName }} →
|
||||
{{
|
||||
report.targetPlanetName ||
|
||||
`[${report.targetPosition.galaxy}:${report.targetPosition.system}:${report.targetPosition.position}]`
|
||||
}}
|
||||
· {{ formatDate(report.timestamp) }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<!-- 战斗报告对话框 -->
|
||||
<BattleReportDialog v-model:open="showBattleDialog" :report="selectedBattleReport" />
|
||||
@@ -96,17 +304,30 @@
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { computed, ref } from 'vue'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import BattleReportDialog from '@/components/BattleReportDialog.vue'
|
||||
import SpyReportDialog from '@/components/SpyReportDialog.vue'
|
||||
import { formatDate } from '@/utils/format'
|
||||
import { X, Sword, Eye } from 'lucide-vue-next'
|
||||
import type { BattleResult, SpyReport } from '@/types/game'
|
||||
import { X, Sword, Eye, AlertTriangle, Package, Recycle, Gift, Ban, Check, Users } from 'lucide-vue-next'
|
||||
import type {
|
||||
BattleResult,
|
||||
SpyReport,
|
||||
SpiedNotification,
|
||||
NPCActivityNotification,
|
||||
MissionReport,
|
||||
GiftNotification,
|
||||
GiftRejectedNotification
|
||||
} from '@/types/game'
|
||||
import { MissionType } from '@/types/game'
|
||||
import { useNPCStore } from '@/stores/npcStore'
|
||||
import * as diplomaticLogic from '@/logic/diplomaticLogic'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const npcStore = useNPCStore()
|
||||
const { t } = useI18n()
|
||||
const activeTab = ref<'battles' | 'spy'>('battles')
|
||||
const activeTab = ref<'battles' | 'spy' | 'missions' | 'npc'>('battles')
|
||||
|
||||
// 对话框状态
|
||||
const showBattleDialog = ref(false)
|
||||
@@ -124,6 +345,30 @@
|
||||
return [...gameStore.player.spyReports].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
// 排序后的被侦查通知(最新的在前)
|
||||
const sortedSpiedNotifications = computed(() => {
|
||||
if (!gameStore.player.spiedNotifications) {
|
||||
return []
|
||||
}
|
||||
return [...gameStore.player.spiedNotifications].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
// 排序后的任务报告(最新的在前)
|
||||
const sortedMissionReports = computed(() => {
|
||||
if (!gameStore.player.missionReports) {
|
||||
return []
|
||||
}
|
||||
return [...gameStore.player.missionReports].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
// 排序后的NPC活动通知(最新的在前)
|
||||
const sortedNPCActivityNotifications = computed(() => {
|
||||
if (!gameStore.player.npcActivityNotifications) {
|
||||
return []
|
||||
}
|
||||
return [...gameStore.player.npcActivityNotifications].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
// 未读战斗报告数量
|
||||
const unreadBattles = computed(() => {
|
||||
return gameStore.player.battleReports.filter(r => !r.read).length
|
||||
@@ -134,6 +379,126 @@
|
||||
return gameStore.player.spyReports.filter(r => !r.read).length
|
||||
})
|
||||
|
||||
// 未读被侦查通知数量
|
||||
const unreadSpiedNotifications = computed(() => {
|
||||
if (!gameStore.player.spiedNotifications) {
|
||||
return 0
|
||||
}
|
||||
return gameStore.player.spiedNotifications.filter(n => !n.read).length
|
||||
})
|
||||
|
||||
// 未读NPC活动通知数量
|
||||
const unreadNPCActivity = computed(() => {
|
||||
if (!gameStore.player.npcActivityNotifications) {
|
||||
return 0
|
||||
}
|
||||
return gameStore.player.npcActivityNotifications.filter(n => !n.read).length
|
||||
})
|
||||
|
||||
// 未读任务报告数量
|
||||
const unreadMissionReports = computed(() => {
|
||||
if (!gameStore.player.missionReports) {
|
||||
return 0
|
||||
}
|
||||
return gameStore.player.missionReports.filter(r => !r.read).length
|
||||
})
|
||||
|
||||
// 未读礼物通知数量
|
||||
const unreadGiftNotifications = computed(() => {
|
||||
if (!gameStore.player.giftNotifications) {
|
||||
return 0
|
||||
}
|
||||
return gameStore.player.giftNotifications.filter(n => !n.read).length
|
||||
})
|
||||
|
||||
// 未读礼物被拒绝通知数量
|
||||
const unreadGiftRejected = computed(() => {
|
||||
if (!gameStore.player.giftRejectedNotifications) {
|
||||
return 0
|
||||
}
|
||||
return gameStore.player.giftRejectedNotifications.filter(n => !n.read).length
|
||||
})
|
||||
|
||||
// 合并:侦查相关未读总数(侦查报告 + 被侦查通知)
|
||||
const unreadSpyTotal = computed(() => {
|
||||
return unreadSpyReports.value + unreadSpiedNotifications.value
|
||||
})
|
||||
|
||||
// 合并:NPC相关未读总数(NPC活动 + 礼物通知 + 礼物被拒绝)
|
||||
const unreadNPCTotal = computed(() => {
|
||||
return unreadNPCActivity.value + unreadGiftNotifications.value + unreadGiftRejected.value
|
||||
})
|
||||
|
||||
// 标签页配置
|
||||
const tabs = computed(() => [
|
||||
{
|
||||
value: 'battles',
|
||||
icon: Sword,
|
||||
label: t('messagesView.battles'),
|
||||
unreadCount: unreadBattles.value
|
||||
},
|
||||
{
|
||||
value: 'spy',
|
||||
icon: Eye,
|
||||
label: t('messagesView.spy'),
|
||||
unreadCount: unreadSpyTotal.value
|
||||
},
|
||||
{
|
||||
value: 'missions',
|
||||
icon: Package,
|
||||
label: t('messagesView.missions'),
|
||||
unreadCount: unreadMissionReports.value
|
||||
},
|
||||
{
|
||||
value: 'npc',
|
||||
icon: Users,
|
||||
label: t('messagesView.npc'),
|
||||
unreadCount: unreadNPCTotal.value
|
||||
}
|
||||
])
|
||||
|
||||
// 排序后的礼物通知(最新的在前)
|
||||
const sortedGiftNotifications = computed(() => {
|
||||
if (!gameStore.player.giftNotifications) {
|
||||
return []
|
||||
}
|
||||
return [...gameStore.player.giftNotifications].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
// 排序后的礼物被拒绝通知(最新的在前)
|
||||
const sortedGiftRejectedNotifications = computed(() => {
|
||||
if (!gameStore.player.giftRejectedNotifications) {
|
||||
return []
|
||||
}
|
||||
return [...gameStore.player.giftRejectedNotifications].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
// 判断战斗结果Badge颜色
|
||||
const getBattleResultVariant = (report: BattleResult): 'default' | 'destructive' | 'secondary' => {
|
||||
if (report.winner === 'draw') {
|
||||
return 'secondary'
|
||||
}
|
||||
|
||||
// 判断玩家是攻击方还是防守方
|
||||
const isPlayerAttacker = report.attackerId === gameStore.player.id
|
||||
const playerWon = isPlayerAttacker ? report.winner === 'attacker' : report.winner === 'defender'
|
||||
|
||||
return playerWon ? 'default' : 'destructive'
|
||||
}
|
||||
|
||||
// 获取战斗结果文本
|
||||
const getBattleResultText = (report: BattleResult): string => {
|
||||
if (report.winner === 'draw') {
|
||||
return t('messagesView.draw')
|
||||
}
|
||||
|
||||
// 判断玩家是攻击方还是防守方
|
||||
const isPlayerAttacker = report.attackerId === gameStore.player.id
|
||||
const playerWon = isPlayerAttacker ? report.winner === 'attacker' : report.winner === 'defender'
|
||||
|
||||
return playerWon ? t('messagesView.victory') : t('messagesView.defeat')
|
||||
}
|
||||
|
||||
// 打开战斗报告
|
||||
const openBattleReport = (report: BattleResult) => {
|
||||
selectedBattleReport.value = report
|
||||
@@ -154,6 +519,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 打开被侦查通知
|
||||
const openSpiedNotification = (notification: SpiedNotification) => {
|
||||
// 标记为已读
|
||||
if (!notification.read) {
|
||||
notification.read = true
|
||||
}
|
||||
}
|
||||
|
||||
// 删除战斗报告
|
||||
const deleteBattleReport = (reportId: string) => {
|
||||
const index = gameStore.player.battleReports.findIndex(r => r.id === reportId)
|
||||
@@ -169,4 +542,117 @@
|
||||
gameStore.player.spyReports.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除被侦查通知
|
||||
const deleteSpiedNotification = (notificationId: string) => {
|
||||
if (!gameStore.player.spiedNotifications) {
|
||||
return
|
||||
}
|
||||
const index = gameStore.player.spiedNotifications.findIndex(n => n.id === notificationId)
|
||||
if (index > -1) {
|
||||
gameStore.player.spiedNotifications.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 打开NPC活动通知
|
||||
const openNPCActivityNotification = (notification: NPCActivityNotification) => {
|
||||
// 标记为已读
|
||||
if (!notification.read) {
|
||||
notification.read = true
|
||||
}
|
||||
}
|
||||
|
||||
// 删除NPC活动通知
|
||||
const deleteNPCActivityNotification = (notificationId: string) => {
|
||||
if (!gameStore.player.npcActivityNotifications) {
|
||||
return
|
||||
}
|
||||
const index = gameStore.player.npcActivityNotifications.findIndex(n => n.id === notificationId)
|
||||
if (index > -1) {
|
||||
gameStore.player.npcActivityNotifications.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取任务类型名称
|
||||
const getMissionTypeName = (missionType: string): string => {
|
||||
const typeMap: Record<string, string> = {
|
||||
[MissionType.Transport]: t('fleetView.transport'),
|
||||
[MissionType.Colonize]: t('fleetView.colonize'),
|
||||
[MissionType.Deploy]: t('fleetView.deploy'),
|
||||
[MissionType.Recycle]: t('fleetView.recycle'),
|
||||
[MissionType.Destroy]: t('fleetView.destroy')
|
||||
}
|
||||
return typeMap[missionType] || missionType
|
||||
}
|
||||
|
||||
// 打开任务报告
|
||||
const openMissionReport = (report: MissionReport) => {
|
||||
// 标记为已读
|
||||
if (!report.read) {
|
||||
report.read = true
|
||||
}
|
||||
}
|
||||
|
||||
// 删除任务报告
|
||||
const deleteMissionReport = (reportId: string) => {
|
||||
if (!gameStore.player.missionReports) {
|
||||
return
|
||||
}
|
||||
const index = gameStore.player.missionReports.findIndex(r => r.id === reportId)
|
||||
if (index > -1) {
|
||||
gameStore.player.missionReports.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 标记礼物通知为已读
|
||||
const markGiftAsRead = (gift: GiftNotification) => {
|
||||
if (!gift.read) {
|
||||
gift.read = true
|
||||
}
|
||||
}
|
||||
|
||||
// 接受礼物
|
||||
const acceptGift = (gift: GiftNotification) => {
|
||||
const npc = npcStore.npcs.find(n => n.id === gift.fromNpcId)
|
||||
if (npc) {
|
||||
diplomaticLogic.acceptNPCGift(gameStore.player, npc, gift)
|
||||
}
|
||||
}
|
||||
|
||||
// 拒绝礼物
|
||||
const rejectGift = (gift: GiftNotification) => {
|
||||
const npc = npcStore.npcs.find(n => n.id === gift.fromNpcId)
|
||||
if (npc) {
|
||||
diplomaticLogic.rejectNPCGift(gameStore.player, npc, gift)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除礼物通知
|
||||
const deleteGiftNotification = (giftId: string) => {
|
||||
if (!gameStore.player.giftNotifications) {
|
||||
return
|
||||
}
|
||||
const index = gameStore.player.giftNotifications.findIndex(g => g.id === giftId)
|
||||
if (index > -1) {
|
||||
gameStore.player.giftNotifications.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 标记礼物被拒绝通知为已读
|
||||
const markGiftRejectedAsRead = (rejection: GiftRejectedNotification) => {
|
||||
if (!rejection.read) {
|
||||
rejection.read = true
|
||||
}
|
||||
}
|
||||
|
||||
// 删除礼物被拒绝通知
|
||||
const deleteGiftRejectedNotification = (rejectionId: string) => {
|
||||
if (!gameStore.player.giftRejectedNotifications) {
|
||||
return
|
||||
}
|
||||
const index = gameStore.player.giftRejectedNotifications.findIndex(r => r.id === rejectionId)
|
||||
if (index > -1) {
|
||||
gameStore.player.giftRejectedNotifications.splice(index, 1)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -5,14 +5,18 @@
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-4 sm:gap-6">
|
||||
<Card v-for="officerType in Object.values(OfficerType)" :key="officerType">
|
||||
<CardHeader>
|
||||
<div class="flex justify-between items-start gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<CardTitle class="text-lg sm:text-xl">{{ OFFICERS[officerType].name }}</CardTitle>
|
||||
<CardDescription class="text-xs sm:text-sm">{{ OFFICERS[officerType].description }}</CardDescription>
|
||||
<div class="mb-2">
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-2">
|
||||
<CardTitle class="text-sm sm:text-base lg:text-lg order-2 sm:order-1">{{ OFFICERS[officerType].name }}</CardTitle>
|
||||
<Badge v-if="isOfficerActive(officerType)" variant="default" class="text-xs whitespace-nowrap self-start order-1 sm:order-2">
|
||||
{{ t('officersView.activated') }}
|
||||
</Badge>
|
||||
<Badge v-else variant="outline" class="text-xs whitespace-nowrap self-start order-1 sm:order-2">
|
||||
{{ t('officersView.inactive') }}
|
||||
</Badge>
|
||||
</div>
|
||||
<Badge v-if="isOfficerActive(officerType)" variant="default" class="text-xs">{{ t('officersView.activated') }}</Badge>
|
||||
<Badge v-else variant="outline" class="text-xs">{{ t('officersView.inactive') }}</Badge>
|
||||
</div>
|
||||
<CardDescription class="text-xs sm:text-sm">{{ OFFICERS[officerType].description }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<!-- 状态信息 -->
|
||||
@@ -30,44 +34,21 @@
|
||||
<div class="space-y-2">
|
||||
<p class="text-sm font-medium text-muted-foreground">{{ t('officersView.recruitCost') }} (7{{ t('officersView.days') }}):</p>
|
||||
<div class="space-y-1.5">
|
||||
<div class="flex items-center gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.metal') }}:</span>
|
||||
<div
|
||||
v-for="resourceType in costResourceTypes"
|
||||
:key="resourceType.key"
|
||||
v-show="resourceType.key !== 'darkMatter' || OFFICERS[officerType].cost.darkMatter > 0"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<ResourceIcon :type="resourceType.key" size="sm" />
|
||||
<span class="text-xs">{{ t(`resources.${resourceType.key}`) }}:</span>
|
||||
<span
|
||||
class="font-medium text-sm"
|
||||
:class="planet ? getResourceCostColor(planet.resources.metal, OFFICERS[officerType].cost.metal) : ''"
|
||||
:class="
|
||||
planet ? getResourceCostColor(planet.resources[resourceType.key], OFFICERS[officerType].cost[resourceType.key]) : ''
|
||||
"
|
||||
>
|
||||
{{ formatNumber(OFFICERS[officerType].cost.metal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.crystal') }}:</span>
|
||||
<span
|
||||
class="font-medium text-sm"
|
||||
:class="planet ? getResourceCostColor(planet.resources.crystal, OFFICERS[officerType].cost.crystal) : ''"
|
||||
>
|
||||
{{ formatNumber(OFFICERS[officerType].cost.crystal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.deuterium') }}:</span>
|
||||
<span
|
||||
class="font-medium text-sm"
|
||||
:class="planet ? getResourceCostColor(planet.resources.deuterium, OFFICERS[officerType].cost.deuterium) : ''"
|
||||
>
|
||||
{{ formatNumber(OFFICERS[officerType].cost.deuterium) }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="OFFICERS[officerType].cost.darkMatter > 0" class="flex items-center gap-2">
|
||||
<ResourceIcon type="darkMatter" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.darkMatter') }}:</span>
|
||||
<span
|
||||
class="font-medium text-sm"
|
||||
:class="planet ? getResourceCostColor(planet.resources.darkMatter, OFFICERS[officerType].cost.darkMatter) : ''"
|
||||
>
|
||||
{{ formatNumber(OFFICERS[officerType].cost.darkMatter) }}
|
||||
{{ formatNumber(OFFICERS[officerType].cost[resourceType.key]) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -121,14 +102,19 @@
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex gap-2">
|
||||
<Button v-if="!isOfficerActive(officerType)" @click="handleHire(officerType)" :disabled="!canHire(officerType)" class="flex-1">
|
||||
<div class="flex flex-col sm:flex-row gap-2">
|
||||
<Button v-if="!isOfficerActive(officerType)" @click="handleHire(officerType)" :disabled="!canHire(officerType)" class="w-full">
|
||||
{{ t('officersView.hire') }} (7{{ t('officersView.days') }})
|
||||
</Button>
|
||||
<Button v-if="isOfficerActive(officerType)" @click="handleRenew(officerType)" :disabled="!canHire(officerType)" class="flex-1">
|
||||
<Button
|
||||
v-if="isOfficerActive(officerType)"
|
||||
@click="handleRenew(officerType)"
|
||||
:disabled="!canHire(officerType)"
|
||||
class="w-full sm:flex-1"
|
||||
>
|
||||
{{ t('officersView.renew') }} (7{{ t('officersView.days') }})
|
||||
</Button>
|
||||
<Button v-if="isOfficerActive(officerType)" @click="handleDismiss(officerType)" variant="outline" size="sm">
|
||||
<Button v-if="isOfficerActive(officerType)" @click="handleDismiss(officerType)" variant="outline" class="w-full sm:w-auto">
|
||||
{{ t('officersView.dismiss') }}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -136,9 +122,36 @@
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- 提示对话框 -->
|
||||
<AlertDialog :open="alertDialogOpen" @update:open="alertDialogOpen = $event">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ alertDialogTitle }}</AlertDialogTitle>
|
||||
<AlertDialogDescription class="whitespace-pre-line">
|
||||
{{ alertDialogMessage }}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction>{{ t('common.confirm') }}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<!-- 确认对话框 -->
|
||||
<AlertDialog ref="alertDialog" />
|
||||
<ConfirmDialog ref="confirmDialog" />
|
||||
<AlertDialog :open="confirmDialogOpen" @update:open="confirmDialogOpen = $event">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ confirmDialogTitle }}</AlertDialogTitle>
|
||||
<AlertDialogDescription class="whitespace-pre-line">
|
||||
{{ confirmDialogMessage }}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{{ t('common.cancel') }}</AlertDialogCancel>
|
||||
<AlertDialogAction @click="handleConfirmDialogConfirm">{{ t('common.confirm') }}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -150,8 +163,16 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import AlertDialog from '@/components/AlertDialog.vue'
|
||||
import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { formatNumber, formatTime, formatDate, getResourceCostColor } from '@/utils/format'
|
||||
import * as officerLogic from '@/logic/officerLogic'
|
||||
import * as resourceLogic from '@/logic/resourceLogic'
|
||||
@@ -162,8 +183,32 @@
|
||||
const { OFFICERS } = useGameConfig()
|
||||
const gameStore = useGameStore()
|
||||
const planet = computed(() => gameStore.currentPlanet)
|
||||
const alertDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
|
||||
const confirmDialog = ref<InstanceType<typeof ConfirmDialog> | null>(null)
|
||||
|
||||
// AlertDialog 状态
|
||||
const alertDialogOpen = ref(false)
|
||||
const alertDialogTitle = ref('')
|
||||
const alertDialogMessage = ref('')
|
||||
|
||||
// ConfirmDialog 状态
|
||||
const confirmDialogOpen = ref(false)
|
||||
const confirmDialogTitle = ref('')
|
||||
const confirmDialogMessage = ref('')
|
||||
const confirmDialogAction = ref<(() => void) | null>(null)
|
||||
|
||||
const handleConfirmDialogConfirm = () => {
|
||||
if (confirmDialogAction.value) {
|
||||
confirmDialogAction.value()
|
||||
}
|
||||
confirmDialogOpen.value = false
|
||||
}
|
||||
|
||||
// 资源类型配置(用于成本显示)
|
||||
const costResourceTypes = [
|
||||
{ key: 'metal' as const },
|
||||
{ key: 'crystal' as const },
|
||||
{ key: 'deuterium' as const },
|
||||
{ key: 'darkMatter' as const }
|
||||
]
|
||||
|
||||
// 检查军官是否激活
|
||||
const isOfficerActive = (officerType: OfficerType): boolean => {
|
||||
@@ -212,19 +257,17 @@
|
||||
|
||||
// 招募军官
|
||||
const handleHire = (officerType: OfficerType) => {
|
||||
confirmDialog.value?.show({
|
||||
title: t('officersView.hireTitle'),
|
||||
message: t('officersView.hireMessage').replace('{name}', OFFICERS.value[officerType].name),
|
||||
onConfirm: () => {
|
||||
const success = hireOfficer(officerType, 7)
|
||||
if (!success) {
|
||||
alertDialog.value?.show({
|
||||
title: t('officersView.hireFailed'),
|
||||
message: t('officersView.insufficientResources')
|
||||
})
|
||||
}
|
||||
confirmDialogTitle.value = t('officersView.hireTitle')
|
||||
confirmDialogMessage.value = t('officersView.hireMessage').replace('{name}', OFFICERS.value[officerType].name)
|
||||
confirmDialogAction.value = () => {
|
||||
const success = hireOfficer(officerType, 7)
|
||||
if (!success) {
|
||||
alertDialogTitle.value = t('officersView.hireFailed')
|
||||
alertDialogMessage.value = t('officersView.insufficientResources')
|
||||
alertDialogOpen.value = true
|
||||
}
|
||||
})
|
||||
}
|
||||
confirmDialogOpen.value = true
|
||||
}
|
||||
|
||||
const renewOfficer = (officerType: OfficerType, duration: number = 7): boolean => {
|
||||
@@ -241,29 +284,26 @@
|
||||
|
||||
// 续约军官
|
||||
const handleRenew = (officerType: OfficerType) => {
|
||||
confirmDialog.value?.show({
|
||||
title: t('officersView.renewTitle'),
|
||||
message: t('officersView.renewMessage').replace('{name}', OFFICERS.value[officerType].name),
|
||||
onConfirm: () => {
|
||||
const success = renewOfficer(officerType, 7)
|
||||
if (!success) {
|
||||
alertDialog.value?.show({
|
||||
title: t('officersView.renewFailed'),
|
||||
message: t('officersView.insufficientResources')
|
||||
})
|
||||
}
|
||||
confirmDialogTitle.value = t('officersView.renewTitle')
|
||||
confirmDialogMessage.value = t('officersView.renewMessage').replace('{name}', OFFICERS.value[officerType].name)
|
||||
confirmDialogAction.value = () => {
|
||||
const success = renewOfficer(officerType, 7)
|
||||
if (!success) {
|
||||
alertDialogTitle.value = t('officersView.renewFailed')
|
||||
alertDialogMessage.value = t('officersView.insufficientResources')
|
||||
alertDialogOpen.value = true
|
||||
}
|
||||
})
|
||||
}
|
||||
confirmDialogOpen.value = true
|
||||
}
|
||||
|
||||
// 解雇军官
|
||||
const handleDismiss = (officerType: OfficerType): void => {
|
||||
confirmDialog.value?.show({
|
||||
title: t('officersView.dismissTitle'),
|
||||
message: t('officersView.dismissMessage').replace('{name}', OFFICERS.value[officerType].name),
|
||||
onConfirm: () => {
|
||||
gameStore.player.officers[officerType] = officerLogic.createInactiveOfficer(officerType)
|
||||
}
|
||||
})
|
||||
confirmDialogTitle.value = t('officersView.dismissTitle')
|
||||
confirmDialogMessage.value = t('officersView.dismissMessage').replace('{name}', OFFICERS.value[officerType].name)
|
||||
confirmDialogAction.value = () => {
|
||||
gameStore.player.officers[officerType] = officerLogic.createInactiveOfficer(officerType)
|
||||
}
|
||||
confirmDialogOpen.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -21,160 +21,153 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 资源显示 -->
|
||||
<!-- 资源管理 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('overview.resourceOverview') }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{{ t('common.resourceType') }}</TableHead>
|
||||
<TableHead class="text-right">{{ t('resources.current') }}</TableHead>
|
||||
<TableHead class="text-right">{{ t('resources.max') }}</TableHead>
|
||||
<TableHead class="text-right">{{ t('resources.production') }}{{ t('resources.perHour') }}</TableHead>
|
||||
<TableHead class="text-right">{{ t('resources.consumption') }}{{ t('resources.perHour') }}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-for="resourceType in resourceTypes" :key="resourceType.key">
|
||||
<TableCell class="font-medium">
|
||||
<div class="flex items-center gap-2">
|
||||
<Tabs default-value="overview" class="w-full">
|
||||
<TabsList class="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="overview">概览</TabsTrigger>
|
||||
<TabsTrigger value="production">产量详情</TabsTrigger>
|
||||
<TabsTrigger value="consumption">消耗详情</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<!-- 概览标签页 -->
|
||||
<TabsContent value="overview" class="mt-4">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{{ t('common.resourceType') }}</TableHead>
|
||||
<TableHead class="text-right">{{ t('resources.current') }}</TableHead>
|
||||
<TableHead class="text-right">{{ t('resources.max') }}</TableHead>
|
||||
<TableHead class="text-right">{{ t('resources.production') }}{{ t('resources.perHour') }}</TableHead>
|
||||
<TableHead class="text-right">{{ t('resources.consumption') }}{{ t('resources.perHour') }}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-for="resourceType in resourceTypes" :key="resourceType.key">
|
||||
<TableCell class="font-medium">
|
||||
<div class="flex items-center gap-2">
|
||||
<ResourceIcon :type="resourceType.key" size="sm" />
|
||||
{{ t(`resources.${resourceType.key}`) }}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class="text-right"
|
||||
:class="getResourceColor(planet.resources[resourceType.key], capacity?.[resourceType.key] || Infinity)"
|
||||
>
|
||||
{{ formatNumber(planet.resources[resourceType.key]) }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right text-muted-foreground">
|
||||
{{ formatNumber(capacity?.[resourceType.key] || 0) }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right text-green-600 dark:text-green-400">
|
||||
+{{ formatNumber(production?.[resourceType.key] || 0) }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right text-red-600 dark:text-red-400">
|
||||
<template v-if="resourceType.key === 'energy'">-{{ formatNumber(energyConsumption) }}</template>
|
||||
<template v-else>-</template>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 产量详情标签页 -->
|
||||
<TabsContent value="production" class="mt-4">
|
||||
<div class="space-y-4">
|
||||
<div v-for="resourceType in resourceTypes" :key="resourceType.key" class="border-b last:border-b-0 pb-4 last:pb-0">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<ResourceIcon :type="resourceType.key" size="sm" />
|
||||
{{ t(`resources.${resourceType.key}`) }}
|
||||
<span class="font-semibold">{{ t(`resources.${resourceType.key}`) }}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<!-- 所有资源统一显示 -->
|
||||
<TableCell
|
||||
class="text-right"
|
||||
:class="getResourceColor(planet.resources[resourceType.key], capacity?.[resourceType.key] || Infinity)"
|
||||
|
||||
<div v-if="productionBreakdown" class="ml-6 space-y-1 text-sm">
|
||||
<!-- 电力有多个来源 -->
|
||||
<template v-if="resourceType.key === 'energy' && productionBreakdown.energy.sources">
|
||||
<div v-for="(source, idx) in productionBreakdown.energy.sources" :key="idx" class="flex justify-between">
|
||||
<span class="text-muted-foreground">
|
||||
{{ t(source.name) }}
|
||||
<template v-if="source.name.startsWith('buildings.')">({{ t('common.level') }} {{ source.level }})</template>
|
||||
<template v-else>({{ source.level }})</template>
|
||||
</span>
|
||||
<span class="text-green-600 dark:text-green-400">
|
||||
+{{ formatNumber(Math.floor(source.production)) }}/{{ t('resources.hour') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 其他资源单一建筑产量 -->
|
||||
<template v-else>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">
|
||||
{{ t(productionBreakdown[resourceType.key].buildingName) }}
|
||||
({{ t('common.level') }} {{ productionBreakdown[resourceType.key].buildingLevel }})
|
||||
</span>
|
||||
<span class="text-green-600 dark:text-green-400">
|
||||
+{{ formatNumber(Math.floor(productionBreakdown[resourceType.key].baseProduction)) }}/{{ t('resources.hour') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 加成列表 -->
|
||||
<div v-for="(bonus, idx) in productionBreakdown[resourceType.key].bonuses" :key="idx" class="flex justify-between">
|
||||
<span class="text-muted-foreground ml-4">
|
||||
{{ t(bonus.name) }} ({{ bonus.percentage > 0 ? '+' : '' }}{{ bonus.percentage }}%)
|
||||
</span>
|
||||
<span :class="bonus.value > 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'">
|
||||
{{ bonus.value > 0 ? '+' : '' }}{{ formatNumber(Math.floor(bonus.value)) }}/{{ t('resources.hour') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 最终产量 -->
|
||||
<div class="flex justify-between font-semibold pt-1 border-t mt-1">
|
||||
<span>{{ t('overview.totalProduction') }}</span>
|
||||
<span class="text-green-600 dark:text-green-400">
|
||||
+{{ formatNumber(Math.floor(productionBreakdown[resourceType.key].finalProduction)) }}/{{ t('resources.hour') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 消耗详情标签页 -->
|
||||
<TabsContent value="consumption" class="mt-4">
|
||||
<div class="space-y-2">
|
||||
<!-- 各建筑消耗 -->
|
||||
<div
|
||||
v-for="consumptionType in consumptionTypes"
|
||||
:key="consumptionType.key"
|
||||
v-show="consumptionBreakdown && consumptionBreakdown[consumptionType.key].buildingLevel > 0"
|
||||
class="flex justify-between text-sm"
|
||||
>
|
||||
{{ formatNumber(planet.resources[resourceType.key]) }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right text-muted-foreground">
|
||||
{{ formatNumber(capacity?.[resourceType.key] || 0) }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right text-green-600 dark:text-green-400">
|
||||
+{{ formatNumber(production?.[resourceType.key] || 0) }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right text-red-600 dark:text-red-400">
|
||||
<template v-if="resourceType.key === 'energy'">
|
||||
-{{ formatNumber(energyConsumption) }}
|
||||
</template>
|
||||
<template v-else>
|
||||
-
|
||||
</template>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 资源获取来源 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('overview.productionSources') }}</CardTitle>
|
||||
<CardDescription>{{ t('overview.productionSourcesDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="space-y-4">
|
||||
<div v-for="resourceType in resourceTypes" :key="resourceType.key" class="border-b last:border-b-0 pb-4 last:pb-0">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<ResourceIcon :type="resourceType.key" size="sm" />
|
||||
<span class="font-semibold">{{ t(`resources.${resourceType.key}`) }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="productionBreakdown" class="ml-6 space-y-1 text-sm">
|
||||
<!-- 建筑基础产量 -->
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">
|
||||
{{ t(productionBreakdown[resourceType.key].buildingName) }}
|
||||
({{ t('common.level') }} {{ productionBreakdown[resourceType.key].buildingLevel }})
|
||||
<span v-if="consumptionBreakdown" class="text-muted-foreground">
|
||||
{{ t(consumptionBreakdown[consumptionType.key].buildingName) }}
|
||||
({{ t('common.level') }} {{ consumptionBreakdown[consumptionType.key].buildingLevel }})
|
||||
</span>
|
||||
<span class="text-green-600 dark:text-green-400">
|
||||
+{{ formatNumber(Math.floor(productionBreakdown[resourceType.key].baseProduction)) }}/{{ t('resources.hour') }}
|
||||
<span v-if="consumptionBreakdown" class="text-red-600 dark:text-red-400">
|
||||
-{{ formatNumber(Math.floor(consumptionBreakdown[consumptionType.key].consumption)) }}/{{ t('resources.hour') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 加成列表 -->
|
||||
<div v-for="(bonus, idx) in productionBreakdown[resourceType.key].bonuses" :key="idx" class="flex justify-between">
|
||||
<span class="text-muted-foreground ml-4">
|
||||
{{ t(bonus.name) }}
|
||||
</span>
|
||||
<span :class="bonus.value > 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'">
|
||||
{{ bonus.value > 0 ? '+' : '' }}{{ bonus.value }}%
|
||||
<!-- 总消耗 -->
|
||||
<div v-if="consumptionBreakdown" class="flex justify-between font-semibold pt-2 border-t">
|
||||
<span>{{ t('overview.totalConsumption') }}</span>
|
||||
<span class="text-red-600 dark:text-red-400">
|
||||
-{{ formatNumber(Math.floor(consumptionBreakdown.total)) }}/{{ t('resources.hour') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 最终产量 -->
|
||||
<div class="flex justify-between font-semibold pt-1 border-t mt-1">
|
||||
<span>{{ t('overview.totalProduction') }}</span>
|
||||
<span class="text-green-600 dark:text-green-400">
|
||||
+{{ formatNumber(Math.floor(productionBreakdown[resourceType.key].finalProduction)) }}/{{ t('resources.hour') }}
|
||||
</span>
|
||||
<!-- 无消耗提示 -->
|
||||
<div v-if="consumptionBreakdown && consumptionBreakdown.total === 0" class="text-sm text-muted-foreground text-center py-2">
|
||||
{{ t('overview.noConsumption') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 资源消耗来源 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('overview.consumptionSources') }}</CardTitle>
|
||||
<CardDescription>{{ t('overview.consumptionSourcesDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="space-y-2">
|
||||
<!-- 金属矿消耗 -->
|
||||
<div v-if="consumptionBreakdown && consumptionBreakdown.metalMine.buildingLevel > 0" class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">
|
||||
{{ t(consumptionBreakdown.metalMine.buildingName) }}
|
||||
({{ t('common.level') }} {{ consumptionBreakdown.metalMine.buildingLevel }})
|
||||
</span>
|
||||
<span class="text-red-600 dark:text-red-400">
|
||||
-{{ formatNumber(Math.floor(consumptionBreakdown.metalMine.consumption)) }}/{{ t('resources.hour') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 晶体矿消耗 -->
|
||||
<div v-if="consumptionBreakdown && consumptionBreakdown.crystalMine.buildingLevel > 0" class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">
|
||||
{{ t(consumptionBreakdown.crystalMine.buildingName) }}
|
||||
({{ t('common.level') }} {{ consumptionBreakdown.crystalMine.buildingLevel }})
|
||||
</span>
|
||||
<span class="text-red-600 dark:text-red-400">
|
||||
-{{ formatNumber(Math.floor(consumptionBreakdown.crystalMine.consumption)) }}/{{ t('resources.hour') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 重氢合成器消耗 -->
|
||||
<div v-if="consumptionBreakdown && consumptionBreakdown.deuteriumSynthesizer.buildingLevel > 0" class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">
|
||||
{{ t(consumptionBreakdown.deuteriumSynthesizer.buildingName) }}
|
||||
({{ t('common.level') }} {{ consumptionBreakdown.deuteriumSynthesizer.buildingLevel }})
|
||||
</span>
|
||||
<span class="text-red-600 dark:text-red-400">
|
||||
-{{ formatNumber(Math.floor(consumptionBreakdown.deuteriumSynthesizer.consumption)) }}/{{ t('resources.hour') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 总消耗 -->
|
||||
<div v-if="consumptionBreakdown" class="flex justify-between font-semibold pt-2 border-t">
|
||||
<span>{{ t('overview.totalConsumption') }}</span>
|
||||
<span class="text-red-600 dark:text-red-400">
|
||||
-{{ formatNumber(Math.floor(consumptionBreakdown.total)) }}/{{ t('resources.hour') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 无消耗提示 -->
|
||||
<div v-if="consumptionBreakdown && consumptionBreakdown.total === 0" class="text-sm text-muted-foreground text-center py-2">
|
||||
{{ t('overview.noConsumption') }}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -202,6 +195,7 @@
|
||||
import { useGameConfig } from '@/composables/useGameConfig'
|
||||
import { computed } from 'vue'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
@@ -210,7 +204,6 @@
|
||||
import type { Planet } from '@/types/game'
|
||||
import * as publicLogic from '@/logic/publicLogic'
|
||||
import * as resourceLogic from '@/logic/resourceLogic'
|
||||
import * as officerLogic from '@/logic/officerLogic'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const { t } = useI18n()
|
||||
@@ -228,8 +221,7 @@
|
||||
// 资源产量详细breakdown
|
||||
const productionBreakdown = computed(() => {
|
||||
if (!planet.value) return null
|
||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
|
||||
return resourceLogic.calculateProductionBreakdown(planet.value, bonuses)
|
||||
return resourceLogic.calculateProductionBreakdown(planet.value, gameStore.player.officers, Date.now())
|
||||
})
|
||||
|
||||
// 资源消耗详细breakdown
|
||||
@@ -243,10 +235,13 @@
|
||||
{ key: 'metal' as const },
|
||||
{ key: 'crystal' as const },
|
||||
{ key: 'deuterium' as const },
|
||||
{ key: 'darkMatter' as const },
|
||||
{ key: 'energy' as const }
|
||||
{ key: 'energy' as const },
|
||||
{ key: 'darkMatter' as const }
|
||||
]
|
||||
|
||||
// 消耗类型配置
|
||||
const consumptionTypes = [{ key: 'metalMine' as const }, { key: 'crystalMine' as const }, { key: 'deuteriumSynthesizer' as const }]
|
||||
|
||||
// 月球相关
|
||||
const moon = computed(() => {
|
||||
if (!planet.value || planet.value.isMoon) return null
|
||||
|
||||
@@ -9,54 +9,44 @@
|
||||
<Card v-for="techType in Object.values(TechnologyType)" :key="techType" class="relative">
|
||||
<CardUnlockOverlay :requirements="TECHNOLOGIES[techType].requirements" :currentLevel="getTechLevel(techType)" />
|
||||
<CardHeader>
|
||||
<div class="flex justify-between items-start gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="mb-2">
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-2">
|
||||
<CardTitle
|
||||
class="text-base sm:text-lg cursor-pointer hover:text-primary transition-colors"
|
||||
class="text-sm sm:text-base lg:text-lg cursor-pointer hover:text-primary transition-colors underline decoration-dotted underline-offset-4 order-2 sm:order-1"
|
||||
@click="detailDialog.openTechnology(techType, getTechLevel(techType))"
|
||||
>
|
||||
{{ TECHNOLOGIES[techType].name }}
|
||||
</CardTitle>
|
||||
<CardDescription class="text-xs sm:text-sm">{{ TECHNOLOGIES[techType].description }}</CardDescription>
|
||||
<Badge variant="secondary" class="text-xs whitespace-nowrap self-start order-1 sm:order-2">
|
||||
Lv {{ getTechLevel(techType) }}
|
||||
</Badge>
|
||||
</div>
|
||||
<Badge variant="secondary" class="text-xs whitespace-nowrap flex-shrink-0">Lv {{ getTechLevel(techType) }}</Badge>
|
||||
</div>
|
||||
<CardDescription class="text-xs sm:text-sm">{{ TECHNOLOGIES[techType].description }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="space-y-2.5 sm:space-y-3">
|
||||
<div class="text-xs sm:text-sm space-y-1.5 sm:space-y-2">
|
||||
<p class="text-muted-foreground mb-1 sm:mb-2">{{ t('researchView.researchCost') }}:</p>
|
||||
<div class="space-y-1 sm:space-y-1.5">
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.metal') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.metal, getTechnologyCost(techType, getTechLevel(techType) + 1).metal)"
|
||||
>
|
||||
{{ formatNumber(getTechnologyCost(techType, getTechLevel(techType) + 1).metal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.crystal') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.crystal, getTechnologyCost(techType, getTechLevel(techType) + 1).crystal)"
|
||||
>
|
||||
{{ formatNumber(getTechnologyCost(techType, getTechLevel(techType) + 1).crystal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.deuterium') }}:</span>
|
||||
<div
|
||||
v-for="resourceType in costResourceTypes"
|
||||
:key="resourceType.key"
|
||||
v-show="resourceType.key !== 'darkMatter' || getTechnologyCost(techType, getTechLevel(techType) + 1).darkMatter > 0"
|
||||
class="flex items-center gap-1.5 sm:gap-2"
|
||||
>
|
||||
<ResourceIcon :type="resourceType.key" size="sm" />
|
||||
<span class="text-xs">{{ t(`resources.${resourceType.key}`) }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="
|
||||
getResourceCostColor(planet.resources.deuterium, getTechnologyCost(techType, getTechLevel(techType) + 1).deuterium)
|
||||
getResourceCostColor(
|
||||
planet.resources[resourceType.key],
|
||||
getTechnologyCost(techType, getTechLevel(techType) + 1)[resourceType.key]
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ formatNumber(getTechnologyCost(techType, getTechLevel(techType) + 1).deuterium) }}
|
||||
{{ formatNumber(getTechnologyCost(techType, getTechLevel(techType) + 1)[resourceType.key]) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -71,7 +61,19 @@
|
||||
</div>
|
||||
|
||||
<!-- 提示对话框 -->
|
||||
<AlertDialog ref="alertDialog" />
|
||||
<AlertDialog :open="alertDialogOpen" @update:open="alertDialogOpen = $event">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ alertDialogTitle }}</AlertDialogTitle>
|
||||
<AlertDialogDescription class="whitespace-pre-line">
|
||||
{{ alertDialogMessage }}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction>{{ t('common.confirm') }}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -87,7 +89,15 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import AlertDialog from '@/components/AlertDialog.vue'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import UnlockRequirement from '@/components/UnlockRequirement.vue'
|
||||
import CardUnlockOverlay from '@/components/CardUnlockOverlay.vue'
|
||||
import { formatNumber, getResourceCostColor } from '@/utils/format'
|
||||
@@ -101,7 +111,19 @@
|
||||
const { TECHNOLOGIES, BUILDINGS } = useGameConfig()
|
||||
const planet = computed(() => gameStore.currentPlanet)
|
||||
const player = computed(() => gameStore.player)
|
||||
const alertDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
|
||||
|
||||
// AlertDialog 状态
|
||||
const alertDialogOpen = ref(false)
|
||||
const alertDialogTitle = ref('')
|
||||
const alertDialogMessage = ref('')
|
||||
|
||||
// 资源类型配置(用于成本显示)
|
||||
const costResourceTypes = [
|
||||
{ key: 'metal' as const },
|
||||
{ key: 'crystal' as const },
|
||||
{ key: 'deuterium' as const },
|
||||
{ key: 'darkMatter' as const }
|
||||
]
|
||||
|
||||
const researchTechnology = (techType: TechnologyType): boolean => {
|
||||
if (!gameStore.currentPlanet) return false
|
||||
@@ -117,7 +139,8 @@
|
||||
gameStore.currentPlanet,
|
||||
techType,
|
||||
currentLevel,
|
||||
gameStore.player.officers
|
||||
gameStore.player.officers,
|
||||
gameStore.player.technologies
|
||||
)
|
||||
gameStore.player.researchQueue.push(queueItem)
|
||||
return true
|
||||
@@ -149,7 +172,11 @@
|
||||
return t('researchView.maxLevelReached') // "等级已满"
|
||||
}
|
||||
|
||||
if (player.value.researchQueue.length > 0) return t('researchView.research')
|
||||
// 检查研究队列是否已满
|
||||
const maxQueue = publicLogic.getMaxResearchQueue(gameStore.player.technologies)
|
||||
if (player.value.researchQueue.length >= maxQueue) {
|
||||
return t('researchView.research')
|
||||
}
|
||||
|
||||
// 检查前置条件
|
||||
if (!checkUpgradeRequirements(techType)) {
|
||||
@@ -196,19 +223,17 @@
|
||||
const handleResearch = (techType: TechnologyType) => {
|
||||
// 检查前置条件
|
||||
if (!checkUpgradeRequirements(techType)) {
|
||||
alertDialog.value?.show({
|
||||
title: t('common.requirementsNotMet'),
|
||||
message: getRequirementsList(techType)
|
||||
})
|
||||
alertDialogTitle.value = t('common.requirementsNotMet')
|
||||
alertDialogMessage.value = getRequirementsList(techType)
|
||||
alertDialogOpen.value = true
|
||||
return
|
||||
}
|
||||
|
||||
const success = researchTechnology(techType)
|
||||
if (!success) {
|
||||
alertDialog.value?.show({
|
||||
title: t('researchView.researchFailed'),
|
||||
message: t('researchView.researchFailedMessage')
|
||||
})
|
||||
alertDialogTitle.value = t('researchView.researchFailed')
|
||||
alertDialogMessage.value = t('researchView.researchFailedMessage')
|
||||
alertDialogOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,7 +254,11 @@
|
||||
return false
|
||||
}
|
||||
|
||||
if (player.value.researchQueue.length > 0) return false
|
||||
// 检查研究队列是否已满
|
||||
const maxQueue = publicLogic.getMaxResearchQueue(gameStore.player.technologies)
|
||||
if (player.value.researchQueue.length >= maxQueue) {
|
||||
return false
|
||||
}
|
||||
|
||||
const cost = getTechnologyCost(techType, currentLevel + 1)
|
||||
|
||||
@@ -237,7 +266,8 @@
|
||||
publicLogic.checkRequirements(planet.value, gameStore.player.technologies, config.requirements) &&
|
||||
planet.value.resources.metal >= cost.metal &&
|
||||
planet.value.resources.crystal >= cost.crystal &&
|
||||
planet.value.resources.deuterium >= cost.deuterium
|
||||
planet.value.resources.deuterium >= cost.deuterium &&
|
||||
planet.value.resources.darkMatter >= cost.darkMatter
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -164,7 +164,7 @@
|
||||
let confirmCallback: (() => void) | null = null
|
||||
|
||||
const openGithub = () => {
|
||||
window.open(`https://github.com/${pkg.author}/${pkg.name}`, '_blank')
|
||||
window.open(`https://github.com/${pkg.author.name}/${pkg.name}`, '_blank')
|
||||
}
|
||||
|
||||
const openQQGroup = () => {
|
||||
@@ -180,6 +180,8 @@
|
||||
const gameData = localStorage.getItem(pkg.name)
|
||||
// 获取地图数据
|
||||
const universeData = localStorage.getItem(`${pkg.name}-universe`)
|
||||
// 获取npc数据
|
||||
const npcData = localStorage.getItem(`${pkg.name}-npcs`)
|
||||
|
||||
if (!gameData) {
|
||||
toast.error(t('settings.exportFailed'))
|
||||
@@ -189,6 +191,7 @@
|
||||
// 合并数据
|
||||
const exportData = {
|
||||
game: gameData,
|
||||
npcs: npcData,
|
||||
universe: universeData || null
|
||||
}
|
||||
|
||||
@@ -247,6 +250,10 @@
|
||||
localStorage.setItem(`${pkg.name}-universe`, importData.universe)
|
||||
}
|
||||
|
||||
if (importData.npcs) {
|
||||
localStorage.setItem(`${pkg.name}-npcs`, importData.npcs)
|
||||
}
|
||||
|
||||
toast.success(t('settings.importSuccess'))
|
||||
// 延迟刷新页面以让toast显示
|
||||
setTimeout(() => window.location.reload(), 1000)
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
class="h-full transition-all duration-300"
|
||||
:class="fleetStorageUsage > maxFleetStorage ? 'bg-destructive' : 'bg-primary'"
|
||||
:style="{ width: `${Math.min((fleetStorageUsage / maxFleetStorage) * 100, 100)}%` }"
|
||||
></div>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -31,9 +31,9 @@
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3 sm:gap-4">
|
||||
<Card v-for="shipType in Object.values(ShipType)" :key="shipType" class="relative">
|
||||
<CardUnlockOverlay :requirements="SHIPS[shipType].requirements" />
|
||||
<CardHeader>
|
||||
<CardHeader class="pb-3">
|
||||
<CardTitle
|
||||
class="text-base sm:text-lg cursor-pointer hover:text-primary transition-colors"
|
||||
class="text-sm sm:text-base lg:text-lg cursor-pointer hover:text-primary transition-colors underline decoration-dotted underline-offset-4 mb-2"
|
||||
@click="detailDialog.openShip(shipType)"
|
||||
>
|
||||
{{ SHIPS[shipType].name }}
|
||||
@@ -64,34 +64,19 @@
|
||||
<div class="text-xs sm:text-sm space-y-1.5 sm:space-y-2">
|
||||
<p class="text-muted-foreground mb-1 sm:mb-2">{{ t('shipyardView.unitCost') }}:</p>
|
||||
<div class="space-y-1 sm:space-y-1.5">
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.metal') }}:</span>
|
||||
<div
|
||||
v-for="resourceType in costResourceTypes"
|
||||
:key="resourceType.key"
|
||||
v-show="resourceType.key !== 'darkMatter' || SHIPS[shipType].cost.darkMatter > 0"
|
||||
class="flex items-center gap-1.5 sm:gap-2"
|
||||
>
|
||||
<ResourceIcon :type="resourceType.key" size="sm" />
|
||||
<span class="text-xs">{{ t(`resources.${resourceType.key}`) }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.metal, SHIPS[shipType].cost.metal)"
|
||||
:class="getResourceCostColor(planet.resources[resourceType.key], SHIPS[shipType].cost[resourceType.key])"
|
||||
>
|
||||
{{ formatNumber(SHIPS[shipType].cost.metal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.crystal') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.crystal, SHIPS[shipType].cost.crystal)"
|
||||
>
|
||||
{{ formatNumber(SHIPS[shipType].cost.crystal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.deuterium') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.deuterium, SHIPS[shipType].cost.deuterium)"
|
||||
>
|
||||
{{ formatNumber(SHIPS[shipType].cost.deuterium) }}
|
||||
{{ formatNumber(SHIPS[shipType].cost[resourceType.key]) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -112,34 +97,19 @@
|
||||
<div v-if="quantities[shipType] > 0" class="text-xs sm:text-sm space-y-1.5 sm:space-y-2 p-2.5 sm:p-3 bg-muted rounded-lg">
|
||||
<p class="font-medium text-muted-foreground">{{ t('shipyardView.totalCost') }}:</p>
|
||||
<div class="space-y-1 sm:space-y-1.5">
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.metal') }}:</span>
|
||||
<div
|
||||
v-for="resourceType in costResourceTypes"
|
||||
:key="resourceType.key"
|
||||
v-show="resourceType.key !== 'darkMatter' || getTotalCost(shipType).darkMatter > 0"
|
||||
class="flex items-center gap-1.5 sm:gap-2"
|
||||
>
|
||||
<ResourceIcon :type="resourceType.key" size="sm" />
|
||||
<span class="text-xs">{{ t(`resources.${resourceType.key}`) }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.metal, getTotalCost(shipType).metal)"
|
||||
:class="getResourceCostColor(planet.resources[resourceType.key], getTotalCost(shipType)[resourceType.key])"
|
||||
>
|
||||
{{ formatNumber(getTotalCost(shipType).metal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.crystal') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.crystal, getTotalCost(shipType).crystal)"
|
||||
>
|
||||
{{ formatNumber(getTotalCost(shipType).crystal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
<span class="text-xs">{{ t('resources.deuterium') }}:</span>
|
||||
<span
|
||||
class="font-medium text-xs sm:text-sm"
|
||||
:class="getResourceCostColor(planet.resources.deuterium, getTotalCost(shipType).deuterium)"
|
||||
>
|
||||
{{ formatNumber(getTotalCost(shipType).deuterium) }}
|
||||
{{ formatNumber(getTotalCost(shipType)[resourceType.key]) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -152,7 +122,19 @@
|
||||
</div>
|
||||
|
||||
<!-- 提示对话框 -->
|
||||
<AlertDialog ref="alertDialog" />
|
||||
<AlertDialog :open="alertDialogOpen" @update:open="alertDialogOpen = $event">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ alertDialogTitle }}</AlertDialogTitle>
|
||||
<AlertDialogDescription class="whitespace-pre-line">
|
||||
{{ alertDialogMessage }}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction>{{ t('common.confirm') }}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -168,7 +150,15 @@
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import AlertDialog from '@/components/AlertDialog.vue'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import UnlockRequirement from '@/components/UnlockRequirement.vue'
|
||||
import CardUnlockOverlay from '@/components/CardUnlockOverlay.vue'
|
||||
import { formatNumber, getResourceCostColor } from '@/utils/format'
|
||||
@@ -181,7 +171,19 @@
|
||||
const { t } = useI18n()
|
||||
const { SHIPS } = useGameConfig()
|
||||
const planet = computed(() => gameStore.currentPlanet)
|
||||
const alertDialog = ref<InstanceType<typeof AlertDialog> | null>(null)
|
||||
|
||||
// AlertDialog 状态
|
||||
const alertDialogOpen = ref(false)
|
||||
const alertDialogTitle = ref('')
|
||||
const alertDialogMessage = ref('')
|
||||
|
||||
// 资源类型配置(用于成本显示)
|
||||
const costResourceTypes = [
|
||||
{ key: 'metal' as const },
|
||||
{ key: 'crystal' as const },
|
||||
{ key: 'deuterium' as const },
|
||||
{ key: 'darkMatter' as const }
|
||||
]
|
||||
|
||||
// 舰队仓储使用量
|
||||
const fleetStorageUsage = computed(() => {
|
||||
@@ -201,11 +203,15 @@
|
||||
[ShipType.HeavyFighter]: 0,
|
||||
[ShipType.Cruiser]: 0,
|
||||
[ShipType.Battleship]: 0,
|
||||
[ShipType.Battlecruiser]: 0,
|
||||
[ShipType.Bomber]: 0,
|
||||
[ShipType.Destroyer]: 0,
|
||||
[ShipType.SmallCargo]: 0,
|
||||
[ShipType.LargeCargo]: 0,
|
||||
[ShipType.ColonyShip]: 0,
|
||||
[ShipType.Recycler]: 0,
|
||||
[ShipType.EspionageProbe]: 0,
|
||||
[ShipType.SolarSatellite]: 0,
|
||||
[ShipType.DarkMatterHarvester]: 0,
|
||||
[ShipType.Deathstar]: 0
|
||||
})
|
||||
@@ -223,19 +229,17 @@
|
||||
const handleBuild = (shipType: ShipType) => {
|
||||
const quantity = quantities.value[shipType]
|
||||
if (quantity <= 0) {
|
||||
alertDialog.value?.show({
|
||||
title: t('shipyardView.inputError'),
|
||||
message: t('shipyardView.inputErrorMessage')
|
||||
})
|
||||
alertDialogTitle.value = t('shipyardView.inputError')
|
||||
alertDialogMessage.value = t('shipyardView.inputErrorMessage')
|
||||
alertDialogOpen.value = true
|
||||
return
|
||||
}
|
||||
|
||||
const success = buildShip(shipType, quantity)
|
||||
if (!success) {
|
||||
alertDialog.value?.show({
|
||||
title: t('shipyardView.buildFailed'),
|
||||
message: t('shipyardView.buildFailedMessage')
|
||||
})
|
||||
alertDialogTitle.value = t('shipyardView.buildFailed')
|
||||
alertDialogMessage.value = t('shipyardView.buildFailedMessage')
|
||||
alertDialogOpen.value = true
|
||||
} else {
|
||||
quantities.value[shipType] = 0
|
||||
}
|
||||
@@ -252,14 +256,16 @@
|
||||
const totalCost = {
|
||||
metal: config.cost.metal * quantity,
|
||||
crystal: config.cost.crystal * quantity,
|
||||
deuterium: config.cost.deuterium * quantity
|
||||
deuterium: config.cost.deuterium * quantity,
|
||||
darkMatter: config.cost.darkMatter * quantity
|
||||
}
|
||||
|
||||
return (
|
||||
publicLogic.checkRequirements(planet.value, gameStore.player.technologies, config.requirements) &&
|
||||
planet.value.resources.metal >= totalCost.metal &&
|
||||
planet.value.resources.crystal >= totalCost.crystal &&
|
||||
planet.value.resources.deuterium >= totalCost.deuterium
|
||||
planet.value.resources.deuterium >= totalCost.deuterium &&
|
||||
planet.value.resources.darkMatter >= totalCost.darkMatter
|
||||
)
|
||||
}
|
||||
|
||||
@@ -270,7 +276,8 @@
|
||||
return {
|
||||
metal: config.cost.metal * quantity,
|
||||
crystal: config.cost.crystal * quantity,
|
||||
deuterium: config.cost.deuterium * quantity
|
||||
deuterium: config.cost.deuterium * quantity,
|
||||
darkMatter: config.cost.darkMatter * quantity
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user