mirror of
https://github.com/setube/ogame-vue-ts.git
synced 2026-05-12 16:05:12 +08:00
refactor: 优化UI组件结构与积分系统
重构部分UI组件脚本结构,统一导入风格,提升可维护性。CardUnlockOverlay解锁条件弹窗改为列表展示,提升可读性。修复QueueNotifications滚动区域高度。ScrollableDialogContent增加最大高度。StarsBackground与ParticlesBg组件代码格式优化。App.vue引入玩家积分定时更新逻辑,NPC成长系统补充间谍探测器修复。
This commit is contained in:
@@ -128,9 +128,18 @@
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ alertDialogTitle }}</AlertDialogTitle>
|
||||
<AlertDialogDescription class="whitespace-pre-line">
|
||||
<AlertDialogDescription v-if="!alertDialogShowRequirements" class="whitespace-pre-line">
|
||||
{{ alertDialogMessage }}
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogDescription v-else>
|
||||
<div class="space-y-2">
|
||||
<div v-for="(req, index) in alertDialogRequirements" :key="index" class="flex items-center gap-2 text-sm">
|
||||
<Check v-if="req.met" :size="16" class="text-green-500 flex-shrink-0" />
|
||||
<X v-else :size="16" class="text-red-500 flex-shrink-0" />
|
||||
<span>{{ req.name }}: Lv {{ req.requiredLevel }} ({{ t('common.current') }}: Lv {{ req.currentLevel }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction>{{ t('common.confirm') }}</AlertDialogAction>
|
||||
@@ -179,7 +188,7 @@
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Clock, Grid3x3 } from 'lucide-vue-next'
|
||||
import { Clock, Grid3x3, Check, X } from 'lucide-vue-next'
|
||||
import { formatNumber, formatTime, getResourceCostColor } from '@/utils/format'
|
||||
import * as buildingLogic from '@/logic/buildingLogic'
|
||||
import * as buildingValidation from '@/logic/buildingValidation'
|
||||
@@ -196,6 +205,8 @@
|
||||
const alertDialogOpen = ref(false)
|
||||
const alertDialogTitle = ref('')
|
||||
const alertDialogMessage = ref('')
|
||||
const alertDialogRequirements = ref<Array<{ name: string; requiredLevel: number; currentLevel: number; met: boolean }>>([])
|
||||
const alertDialogShowRequirements = ref(false)
|
||||
|
||||
// 拆除确认对话框状态
|
||||
const demolishConfirmOpen = ref(false)
|
||||
@@ -248,7 +259,9 @@
|
||||
// 检查前置条件
|
||||
if (!checkUpgradeRequirements(buildingType)) {
|
||||
alertDialogTitle.value = t('common.requirementsNotMet')
|
||||
alertDialogMessage.value = getRequirementsList(buildingType)
|
||||
alertDialogRequirements.value = getRequirementsList(buildingType)
|
||||
alertDialogShowRequirements.value = true
|
||||
alertDialogMessage.value = ''
|
||||
alertDialogOpen.value = true
|
||||
return
|
||||
}
|
||||
@@ -257,6 +270,7 @@
|
||||
if (!result.success) {
|
||||
alertDialogTitle.value = t('buildingsView.upgradeFailed')
|
||||
alertDialogMessage.value = result.reason ? t(result.reason) : t('buildingsView.upgradeFailedMessage')
|
||||
alertDialogShowRequirements.value = false
|
||||
alertDialogOpen.value = true
|
||||
}
|
||||
}
|
||||
@@ -292,18 +306,23 @@
|
||||
return t('buildingsView.maxLevelReached') // "等级已满"
|
||||
}
|
||||
|
||||
if (planet.value.buildQueue.length > 0) return t('buildingsView.upgrade')
|
||||
// 0级为建造,1级及以上为升级
|
||||
const buttonTextKey = currentLevel === 0 ? 'buildingsView.build' : 'buildingsView.upgrade'
|
||||
|
||||
if (planet.value.buildQueue.length > 0) return t(buttonTextKey)
|
||||
|
||||
// 检查前置条件
|
||||
if (!checkUpgradeRequirements(buildingType)) {
|
||||
return t('buildingsView.requirementsNotMet')
|
||||
}
|
||||
|
||||
return t('buildingsView.upgrade')
|
||||
return t(buttonTextKey)
|
||||
}
|
||||
|
||||
// 获取前置条件列表文本
|
||||
const getRequirementsList = (buildingType: BuildingType): string => {
|
||||
// 获取前置条件列表
|
||||
const getRequirementsList = (
|
||||
buildingType: BuildingType
|
||||
): Array<{ name: string; requiredLevel: number; currentLevel: number; met: boolean }> => {
|
||||
const config = BUILDINGS.value[buildingType]
|
||||
const currentLevel = getBuildingLevel(buildingType)
|
||||
const targetLevel = currentLevel + 1
|
||||
@@ -311,28 +330,59 @@
|
||||
// 获取目标等级的所有前置条件(包括等级门槛)
|
||||
const requirements = publicLogic.getLevelRequirements(config, targetLevel)
|
||||
|
||||
if (!requirements || !planet.value) return ''
|
||||
if (!requirements || !planet.value) return []
|
||||
|
||||
const lines: string[] = []
|
||||
const items: Array<{ name: string; requiredLevel: number; currentLevel: number; met: boolean }> = []
|
||||
for (const [key, requiredLevel] of Object.entries(requirements)) {
|
||||
// 检查是否为建筑类型
|
||||
if (Object.values(BuildingType).includes(key as BuildingType)) {
|
||||
const bt = key as BuildingType
|
||||
const currentLevel = planet.value.buildings[bt] || 0
|
||||
const name = BUILDINGS.value[bt]?.name || bt
|
||||
const status = currentLevel >= requiredLevel ? '✓' : '✗'
|
||||
lines.push(`${status} ${name}: Lv ${requiredLevel} (${t('common.current')}: Lv ${currentLevel})`)
|
||||
items.push({ name, requiredLevel, currentLevel, met: currentLevel >= requiredLevel })
|
||||
}
|
||||
// 检查是否为科技类型
|
||||
else if (Object.values(TechnologyType).includes(key as TechnologyType)) {
|
||||
const tt = key as TechnologyType
|
||||
const currentLevel = gameStore.player.technologies[tt] || 0
|
||||
const name = TECHNOLOGIES.value[tt]?.name || tt
|
||||
const status = currentLevel >= requiredLevel ? '✓' : '✗'
|
||||
lines.push(`${status} ${name}: Lv ${requiredLevel} (${t('common.current')}: Lv ${currentLevel})`)
|
||||
items.push({ name, requiredLevel, currentLevel, met: currentLevel >= requiredLevel })
|
||||
}
|
||||
}
|
||||
return lines.join('\n')
|
||||
return items
|
||||
}
|
||||
|
||||
// 获取前置条件显示(简化版,用于卡片内显示)
|
||||
const getRequirementsDisplay = (buildingType: BuildingType): Array<{ name: string; level: number; met: boolean }> => {
|
||||
if (!planet.value) return []
|
||||
|
||||
const config = BUILDINGS.value[buildingType]
|
||||
const currentLevel = getBuildingLevel(buildingType)
|
||||
const targetLevel = currentLevel + 1
|
||||
|
||||
// 获取目标等级的所有前置条件(包括等级门槛)
|
||||
const requirements = publicLogic.getLevelRequirements(config, targetLevel)
|
||||
|
||||
if (!requirements || Object.keys(requirements).length === 0) return []
|
||||
|
||||
const items: Array<{ name: string; level: number; met: boolean }> = []
|
||||
for (const [key, requiredLevel] of Object.entries(requirements)) {
|
||||
// 检查是否为建筑类型
|
||||
if (Object.values(BuildingType).includes(key as BuildingType)) {
|
||||
const bt = key as BuildingType
|
||||
const currentLevel = planet.value.buildings[bt] || 0
|
||||
const name = BUILDINGS.value[bt]?.name || bt
|
||||
items.push({ name, level: requiredLevel, met: currentLevel >= requiredLevel })
|
||||
}
|
||||
// 检查是否为科技类型
|
||||
else if (Object.values(TechnologyType).includes(key as TechnologyType)) {
|
||||
const tt = key as TechnologyType
|
||||
const currentLevel = gameStore.player.technologies[tt] || 0
|
||||
const name = TECHNOLOGIES.value[tt]?.name || tt
|
||||
items.push({ name, level: requiredLevel, met: currentLevel >= requiredLevel })
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// 检查是否可以升级
|
||||
|
||||
@@ -5,8 +5,107 @@
|
||||
<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>
|
||||
<!-- NPC诊断按钮 -->
|
||||
<Button @click="showNPCDiagnostic" variant="outline" size="sm">
|
||||
<Search class="mr-2 h-4 w-4" />
|
||||
NPC状态诊断
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- NPC诊断对话框 -->
|
||||
<Dialog v-model:open="npcDiagnosticOpen">
|
||||
<ScrollableDialogContent container-class="max-w-4xl">
|
||||
<template #header>
|
||||
<DialogTitle>NPC状态诊断报告</DialogTitle>
|
||||
<DialogDescription>
|
||||
<div class="text-sm mt-2">
|
||||
玩家积分: {{ gameStore.player.points || 0 }} | 侦查间隔: {{ Math.floor(behaviorConfig.spyInterval / 60) }}分钟 | 攻击间隔:
|
||||
{{ Math.floor(behaviorConfig.attackInterval / 60) }}分钟 | 攻击概率:
|
||||
{{ (behaviorConfig.attackProbability * 100).toFixed(0) }}%
|
||||
</div>
|
||||
</DialogDescription>
|
||||
</template>
|
||||
|
||||
<div v-if="npcDiagnostics.length === 0" class="text-center py-8 text-muted-foreground">暂无NPC数据</div>
|
||||
<div v-else class="space-y-4">
|
||||
<div v-for="diagnostic in npcDiagnostics" :key="diagnostic.npcId" class="border rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="font-bold text-lg">{{ diagnostic.npcName }}</h3>
|
||||
<Badge
|
||||
:variant="
|
||||
diagnostic.relationStatus === '友好' ? 'default' : diagnostic.relationStatus === '敌对' ? 'destructive' : 'secondary'
|
||||
"
|
||||
>
|
||||
{{ diagnostic.relationStatus }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2 text-sm mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-muted-foreground">难度:</span>
|
||||
<span class="font-medium">{{ diagnostic.difficulty }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-muted-foreground">好感度:</span>
|
||||
<span class="font-medium">{{ diagnostic.reputation }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-muted-foreground">侦查探测器:</span>
|
||||
<span class="font-medium">{{ diagnostic.spyProbes }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-muted-foreground">舰队战力:</span>
|
||||
<span class="font-medium">{{ diagnostic.totalFleetPower }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-muted-foreground">可以侦查:</span>
|
||||
<span :class="diagnostic.canSpy ? 'text-green-600 font-semibold' : 'text-red-600 font-semibold'">
|
||||
{{ diagnostic.canSpy ? '是' : '否' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-muted-foreground">可以攻击:</span>
|
||||
<span :class="diagnostic.canAttack ? 'text-green-600 font-semibold' : 'text-red-600 font-semibold'">
|
||||
{{ diagnostic.canAttack ? '是' : '否' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-muted-foreground">攻击概率:</span>
|
||||
<span class="font-medium">{{ (diagnostic.attackProbability * 100).toFixed(0) }}%</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-muted-foreground">下次侦查:</span>
|
||||
<span class="font-medium">
|
||||
<template v-if="diagnostic.nextSpyIn > 0">
|
||||
{{ Math.floor(diagnostic.nextSpyIn / 60) }}分{{ diagnostic.nextSpyIn % 60 }}秒
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="text-green-600">随时</span>
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-span-2 flex items-center gap-2">
|
||||
<span class="text-muted-foreground">下次攻击:</span>
|
||||
<span class="font-medium">
|
||||
<template v-if="diagnostic.nextAttackIn > 0">
|
||||
{{ Math.floor(diagnostic.nextAttackIn / 60) }}分{{ diagnostic.nextAttackIn % 60 }}秒
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="text-green-600">随时</span>
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="diagnostic.reasons.length > 0" class="mt-3 p-3 bg-muted rounded text-xs">
|
||||
<div class="font-semibold mb-2">状态说明:</div>
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
<li v-for="(reason, idx) in diagnostic.reasons" :key="idx">{{ reason }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollableDialogContent>
|
||||
</Dialog>
|
||||
|
||||
<!-- 关系状态过滤标签 -->
|
||||
<Tabs v-model="activeTab" class="w-full">
|
||||
<TabsList class="grid w-full grid-cols-4">
|
||||
@@ -218,10 +317,15 @@
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogDescription, DialogTitle } from '@/components/ui/dialog'
|
||||
import ScrollableDialogContent from '@/components/ui/dialog/ScrollableDialogContent.vue'
|
||||
import { Pagination, PaginationContent, PaginationItem, PaginationNext, PaginationPrevious } from '@/components/ui/pagination'
|
||||
import NpcRelationCard from '@/components/NpcRelationCard.vue'
|
||||
import { RelationStatus } from '@/types/game'
|
||||
import type { DiplomaticRelation } from '@/types/game'
|
||||
import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic'
|
||||
import { Search } from 'lucide-vue-next'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const npcStore = useNPCStore()
|
||||
@@ -229,6 +333,19 @@
|
||||
|
||||
const activeTab = ref('all')
|
||||
|
||||
// NPC诊断功能
|
||||
const npcDiagnosticOpen = ref(false)
|
||||
const npcDiagnostics = ref<npcBehaviorLogic.NPCDiagnosticInfo[]>([])
|
||||
const behaviorConfig = computed(() => {
|
||||
return npcBehaviorLogic.calculateDynamicBehavior(gameStore.player.points || 0)
|
||||
})
|
||||
|
||||
const showNPCDiagnostic = () => {
|
||||
const currentTime = Date.now()
|
||||
npcDiagnostics.value = npcBehaviorLogic.diagnoseNPCBehavior(npcStore.npcs, gameStore.player, currentTime)
|
||||
npcDiagnosticOpen.value = true
|
||||
}
|
||||
|
||||
// 检测并生成NPC盟友
|
||||
const initializeNPCAllies = () => {
|
||||
const npcs = npcStore.npcs
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 sm:gap-4">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 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" />
|
||||
@@ -192,6 +192,20 @@
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="cargo-darkMatter" class="text-xs sm:text-sm flex items-center gap-2">
|
||||
<ResourceIcon type="darkMatter" size="sm" />
|
||||
{{ t('resources.darkMatter') }} ({{ t('fleetView.available') }}: {{ formatNumber(planet.resources.darkMatter) }})
|
||||
</Label>
|
||||
<Input
|
||||
id="cargo-darkMatter"
|
||||
v-model.number="cargo.darkMatter"
|
||||
type="number"
|
||||
min="0"
|
||||
:max="planet.resources.darkMatter"
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs sm:text-sm text-muted-foreground mt-2">
|
||||
{{ t('fleetView.totalCargoCapacity') }}: {{ formatNumber(getTotalCargoCapacity()) }} | {{ t('fleetView.used') }}:
|
||||
@@ -344,7 +358,7 @@
|
||||
import { useGameConfig } from '@/composables/useGameConfig'
|
||||
import { computed, ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ShipType, MissionType, BuildingType } from '@/types/game'
|
||||
import { ShipType, MissionType, BuildingType, TechnologyType } 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'
|
||||
@@ -366,7 +380,7 @@
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import UnlockRequirement from '@/components/UnlockRequirement.vue'
|
||||
import { Sword, Package, Rocket as RocketIcon, Eye, Users, Recycle, Skull, Gift } from 'lucide-vue-next'
|
||||
import { Sword, Package, Rocket as RocketIcon, Eye, Users, Recycle, Skull, Gift, Compass } from 'lucide-vue-next'
|
||||
import { formatNumber, formatTime } from '@/utils/format'
|
||||
import * as shipValidation from '@/logic/shipValidation'
|
||||
import * as fleetLogic from '@/logic/fleetLogic'
|
||||
@@ -397,7 +411,8 @@
|
||||
// 计算最大舰队任务槽位
|
||||
const maxFleetMissions = computed(() => {
|
||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
|
||||
return publicLogic.getMaxFleetMissions(bonuses.additionalFleetSlots)
|
||||
const computerTechLevel = gameStore.player.technologies[TechnologyType.ComputerTechnology] || 0
|
||||
return publicLogic.getMaxFleetMissions(bonuses.additionalFleetSlots, computerTechLevel)
|
||||
})
|
||||
|
||||
const activeTab = ref<'fleet' | 'send' | 'missions'>('fleet')
|
||||
@@ -504,6 +519,7 @@
|
||||
{ type: MissionType.Colonize, name: t('fleetView.colonize'), icon: RocketIcon },
|
||||
{ type: MissionType.Spy, name: t('fleetView.spy'), icon: Eye },
|
||||
{ type: MissionType.Deploy, name: t('fleetView.deploy'), icon: Users },
|
||||
{ type: MissionType.Expedition, name: t('fleetView.expedition'), icon: Compass },
|
||||
{ type: MissionType.Recycle, name: t('fleetView.recycle'), icon: Recycle },
|
||||
{ type: MissionType.Destroy, name: t('fleetView.destroy'), icon: Skull }
|
||||
])
|
||||
@@ -616,7 +632,8 @@
|
||||
fleet,
|
||||
cargo,
|
||||
gameStore.player.officers,
|
||||
currentMissions
|
||||
currentMissions,
|
||||
gameStore.player.technologies
|
||||
)
|
||||
if (!validation.valid) return false
|
||||
const shouldDeductCargo = missionType === MissionType.Transport
|
||||
|
||||
@@ -331,6 +331,7 @@
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { BuildingType, TechnologyType, ShipType, DefenseType, OfficerType } from '@/types/game'
|
||||
import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic'
|
||||
import * as publicLogic from '@/logic/publicLogic'
|
||||
import { Home } from 'lucide-vue-next'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -340,6 +341,11 @@
|
||||
const { t } = useI18n()
|
||||
const { BUILDINGS, TECHNOLOGIES, SHIPS, DEFENSES, OFFICERS } = useGameConfig()
|
||||
|
||||
// 更新玩家积分的辅助函数
|
||||
const updatePlayerPoints = () => {
|
||||
gameStore.player.points = publicLogic.calculatePlayerPoints(gameStore.player)
|
||||
}
|
||||
|
||||
const goHome = () => {
|
||||
router.push('/')
|
||||
}
|
||||
@@ -407,22 +413,26 @@
|
||||
const setBuildingLevel = (building: BuildingType, level: number) => {
|
||||
if (selectedPlanet.value) {
|
||||
selectedPlanet.value.buildings[building] = level
|
||||
updatePlayerPoints()
|
||||
}
|
||||
}
|
||||
|
||||
const setTechnologyLevel = (tech: TechnologyType, level: number) => {
|
||||
gameStore.player.technologies[tech] = level
|
||||
updatePlayerPoints()
|
||||
}
|
||||
|
||||
const setShipCount = (ship: ShipType, count: number) => {
|
||||
if (selectedPlanet.value) {
|
||||
selectedPlanet.value.fleet[ship] = (selectedPlanet.value.fleet[ship] || 0) + count
|
||||
updatePlayerPoints()
|
||||
}
|
||||
}
|
||||
|
||||
const setDefenseCount = (defense: DefenseType, count: number) => {
|
||||
if (selectedPlanet.value) {
|
||||
selectedPlanet.value.defense[defense] = (selectedPlanet.value.defense[defense] || 0) + count
|
||||
updatePlayerPoints()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -614,7 +624,7 @@
|
||||
const maxAllResources = () => {
|
||||
if (!selectedPlanet.value) return
|
||||
|
||||
const maxAmount = 1000000000 // 10亿
|
||||
const maxAmount = 1000000000000000000
|
||||
selectedPlanet.value.resources.metal = maxAmount
|
||||
selectedPlanet.value.resources.crystal = maxAmount
|
||||
selectedPlanet.value.resources.deuterium = maxAmount
|
||||
@@ -708,6 +718,9 @@
|
||||
}
|
||||
})
|
||||
|
||||
// 更新玩家积分(因为建筑/科技/舰队/防御可能已改变)
|
||||
updatePlayerPoints()
|
||||
|
||||
toast.success(
|
||||
t('gmView.completeQueuesSuccess', {
|
||||
buildingCount,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<!-- 标签切换 -->
|
||||
<Tabs v-model="activeTab" class="w-full">
|
||||
<TabsList class="grid w-full grid-cols-2 sm:grid-cols-4">
|
||||
<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>
|
||||
@@ -555,7 +555,7 @@
|
||||
import BattleReportDialog from '@/components/BattleReportDialog.vue'
|
||||
import SpyReportDialog from '@/components/SpyReportDialog.vue'
|
||||
import { formatDate } from '@/utils/format'
|
||||
import { X, Sword, Eye, AlertTriangle, Package, Recycle, Gift, Ban, Check, Users, Skull, Globe } from 'lucide-vue-next'
|
||||
import { X, Sword, Eye, AlertTriangle, Package, Recycle, Gift, Ban, Check, Users, Skull, Globe, Compass } from 'lucide-vue-next'
|
||||
import type {
|
||||
BattleResult,
|
||||
SpyReport,
|
||||
@@ -837,6 +837,7 @@
|
||||
[MissionType.Transport]: t('fleetView.transport'),
|
||||
[MissionType.Colonize]: t('fleetView.colonize'),
|
||||
[MissionType.Deploy]: t('fleetView.deploy'),
|
||||
[MissionType.Expedition]: t('fleetView.expedition'),
|
||||
[MissionType.Recycle]: t('fleetView.recycle'),
|
||||
[MissionType.Destroy]: t('fleetView.destroy'),
|
||||
[MissionType.MissileAttack]: t('galaxyView.missileAttack')
|
||||
@@ -963,6 +964,8 @@
|
||||
return Recycle
|
||||
case MissionType.Colonize:
|
||||
return Globe
|
||||
case MissionType.Expedition:
|
||||
return Compass
|
||||
case MissionType.Destroy:
|
||||
return Skull
|
||||
default:
|
||||
|
||||
@@ -65,9 +65,18 @@
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ alertDialogTitle }}</AlertDialogTitle>
|
||||
<AlertDialogDescription class="whitespace-pre-line">
|
||||
<AlertDialogDescription v-if="!alertDialogShowRequirements" class="whitespace-pre-line">
|
||||
{{ alertDialogMessage }}
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogDescription v-else>
|
||||
<div class="space-y-2">
|
||||
<div v-for="(req, index) in alertDialogRequirements" :key="index" class="flex items-center gap-2 text-sm">
|
||||
<Check v-if="req.met" :size="16" class="text-green-500 flex-shrink-0" />
|
||||
<X v-else :size="16" class="text-red-500 flex-shrink-0" />
|
||||
<span>{{ req.name }}: Lv {{ req.requiredLevel }} ({{ t('common.current') }}: Lv {{ req.currentLevel }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction>{{ t('common.confirm') }}</AlertDialogAction>
|
||||
@@ -99,6 +108,7 @@
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import CardUnlockOverlay from '@/components/CardUnlockOverlay.vue'
|
||||
import { Check, X } from 'lucide-vue-next'
|
||||
import { formatNumber, getResourceCostColor } from '@/utils/format'
|
||||
import * as publicLogic from '@/logic/publicLogic'
|
||||
import * as researchLogic from '@/logic/researchLogic'
|
||||
@@ -115,6 +125,8 @@
|
||||
const alertDialogOpen = ref(false)
|
||||
const alertDialogTitle = ref('')
|
||||
const alertDialogMessage = ref('')
|
||||
const alertDialogRequirements = ref<Array<{ name: string; requiredLevel: number; currentLevel: number; met: boolean }>>([])
|
||||
const alertDialogShowRequirements = ref(false)
|
||||
|
||||
// 资源类型配置(用于成本显示)
|
||||
const costResourceTypes = [
|
||||
@@ -185,8 +197,10 @@
|
||||
return t('researchView.research') // "研究"
|
||||
}
|
||||
|
||||
// 获取前置条件列表文本
|
||||
const getRequirementsList = (techType: TechnologyType): string => {
|
||||
// 获取前置条件列表
|
||||
const getRequirementsList = (
|
||||
techType: TechnologyType
|
||||
): Array<{ name: string; requiredLevel: number; currentLevel: number; met: boolean }> => {
|
||||
const config = TECHNOLOGIES.value[techType]
|
||||
const currentLevel = getTechLevel(techType)
|
||||
const targetLevel = currentLevel + 1
|
||||
@@ -194,28 +208,59 @@
|
||||
// 获取目标等级的所有前置条件(包括等级门槛)
|
||||
const requirements = publicLogic.getLevelRequirements(config, targetLevel)
|
||||
|
||||
if (!requirements || !planet.value) return ''
|
||||
if (!requirements || !planet.value) return []
|
||||
|
||||
const lines: string[] = []
|
||||
const items: Array<{ name: string; requiredLevel: number; currentLevel: number; met: boolean }> = []
|
||||
for (const [key, requiredLevel] of Object.entries(requirements)) {
|
||||
// 检查是否为建筑类型
|
||||
if (Object.values(BuildingType).includes(key as BuildingType)) {
|
||||
const bt = key as BuildingType
|
||||
const currentLevel = planet.value.buildings[bt] || 0
|
||||
const name = BUILDINGS.value[bt]?.name || bt
|
||||
const status = currentLevel >= requiredLevel ? '✓' : '✗'
|
||||
lines.push(`${status} ${name}: Lv ${requiredLevel} (${t('common.current')}: Lv ${currentLevel})`)
|
||||
items.push({ name, requiredLevel, currentLevel, met: currentLevel >= requiredLevel })
|
||||
}
|
||||
// 检查是否为科技类型
|
||||
else if (Object.values(TechnologyType).includes(key as TechnologyType)) {
|
||||
const tt = key as TechnologyType
|
||||
const currentLevel = gameStore.player.technologies[tt] || 0
|
||||
const name = TECHNOLOGIES.value[tt]?.name || tt
|
||||
const status = currentLevel >= requiredLevel ? '✓' : '✗'
|
||||
lines.push(`${status} ${name}: Lv ${requiredLevel} (${t('common.current')}: Lv ${currentLevel})`)
|
||||
items.push({ name, requiredLevel, currentLevel, met: currentLevel >= requiredLevel })
|
||||
}
|
||||
}
|
||||
return lines.join('\n')
|
||||
return items
|
||||
}
|
||||
|
||||
// 获取前置条件显示(简化版,用于卡片内显示)
|
||||
const getRequirementsDisplay = (techType: TechnologyType): Array<{ name: string; level: number; met: boolean }> => {
|
||||
if (!planet.value) return []
|
||||
|
||||
const config = TECHNOLOGIES.value[techType]
|
||||
const currentLevel = getTechLevel(techType)
|
||||
const targetLevel = currentLevel + 1
|
||||
|
||||
// 获取目标等级的所有前置条件(包括等级门槛)
|
||||
const requirements = publicLogic.getLevelRequirements(config, targetLevel)
|
||||
|
||||
if (!requirements || Object.keys(requirements).length === 0) return []
|
||||
|
||||
const items: Array<{ name: string; level: number; met: boolean }> = []
|
||||
for (const [key, requiredLevel] of Object.entries(requirements)) {
|
||||
// 检查是否为建筑类型
|
||||
if (Object.values(BuildingType).includes(key as BuildingType)) {
|
||||
const bt = key as BuildingType
|
||||
const currentLevel = planet.value.buildings[bt] || 0
|
||||
const name = BUILDINGS.value[bt]?.name || bt
|
||||
items.push({ name, level: requiredLevel, met: currentLevel >= requiredLevel })
|
||||
}
|
||||
// 检查是否为科技类型
|
||||
else if (Object.values(TechnologyType).includes(key as TechnologyType)) {
|
||||
const tt = key as TechnologyType
|
||||
const currentLevel = gameStore.player.technologies[tt] || 0
|
||||
const name = TECHNOLOGIES.value[tt]?.name || tt
|
||||
items.push({ name, level: requiredLevel, met: currentLevel >= requiredLevel })
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// 研究科技
|
||||
@@ -223,7 +268,9 @@
|
||||
// 检查前置条件
|
||||
if (!checkUpgradeRequirements(techType)) {
|
||||
alertDialogTitle.value = t('common.requirementsNotMet')
|
||||
alertDialogMessage.value = getRequirementsList(techType)
|
||||
alertDialogRequirements.value = getRequirementsList(techType)
|
||||
alertDialogShowRequirements.value = true
|
||||
alertDialogMessage.value = ''
|
||||
alertDialogOpen.value = true
|
||||
return
|
||||
}
|
||||
@@ -232,6 +279,7 @@
|
||||
if (!success) {
|
||||
alertDialogTitle.value = t('researchView.researchFailed')
|
||||
alertDialogMessage.value = t('researchView.researchFailedMessage')
|
||||
alertDialogShowRequirements.value = false
|
||||
alertDialogOpen.value = true
|
||||
}
|
||||
}
|
||||
@@ -253,6 +301,12 @@
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查队列中是否已存在该科技的研究任务
|
||||
const existingQueueItem = player.value.researchQueue.find(item => item.type === 'technology' && item.itemType === techType)
|
||||
if (existingQueueItem) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查研究队列是否已满
|
||||
const maxQueue = publicLogic.getMaxResearchQueue(gameStore.player.technologies)
|
||||
if (player.value.researchQueue.length >= maxQueue) {
|
||||
|
||||
Reference in New Issue
Block a user