mirror of
https://github.com/setube/ogame-vue-ts.git
synced 2026-05-12 07:55:11 +08:00
feat: 新增队列与外交通知组件及新手引导
引入队列通知(QueueNotifications)和外交通知(DiplomaticNotifications)组件,优化主界面队列与外交报告展示,支持一键查看与跳转。重构App.vue,移除原有队列展示,改为弹出式通知,支持功能解锁提示与新手引导(TutorialOverlay)。完善NPC外交事件处理,导弹攻击等行为影响好感度并生成报告。优化部分UI细节与多语言文本,提升交互体验。
This commit is contained in:
@@ -29,7 +29,7 @@
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4">
|
||||
<Card v-for="buildingType in availableBuildings" :key="buildingType" class="relative">
|
||||
<Card v-for="buildingType in availableBuildings" :key="buildingType" :data-building="buildingType" class="relative">
|
||||
<!-- 前置条件遮罩 -->
|
||||
<CardUnlockOverlay :requirements="BUILDINGS[buildingType].requirements" :currentLevel="getBuildingLevel(buildingType)" />
|
||||
|
||||
@@ -225,18 +225,18 @@
|
||||
})
|
||||
})
|
||||
|
||||
const upgradeBuilding = (buildingType: BuildingType): boolean => {
|
||||
if (!gameStore.currentPlanet) return false
|
||||
const upgradeBuilding = (buildingType: BuildingType): { success: boolean; reason?: string } => {
|
||||
if (!gameStore.currentPlanet) return { success: false }
|
||||
const validation = buildingValidation.validateBuildingUpgrade(
|
||||
gameStore.currentPlanet,
|
||||
buildingType,
|
||||
gameStore.player.technologies,
|
||||
gameStore.player.officers
|
||||
)
|
||||
if (!validation.valid) return false
|
||||
if (!validation.valid) return { success: false, reason: validation.reason }
|
||||
const queueItem = buildingValidation.executeBuildingUpgrade(gameStore.currentPlanet, buildingType, gameStore.player.officers)
|
||||
gameStore.currentPlanet.buildQueue.push(queueItem)
|
||||
return true
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
const getUsedSpace = (planet: Planet): number => {
|
||||
@@ -253,10 +253,10 @@
|
||||
return
|
||||
}
|
||||
|
||||
const success = upgradeBuilding(buildingType)
|
||||
if (!success) {
|
||||
const result = upgradeBuilding(buildingType)
|
||||
if (!result.success) {
|
||||
alertDialogTitle.value = t('buildingsView.upgradeFailed')
|
||||
alertDialogMessage.value = t('buildingsView.upgradeFailedMessage')
|
||||
alertDialogMessage.value = result.reason ? t(result.reason) : t('buildingsView.upgradeFailedMessage')
|
||||
alertDialogOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,14 +231,14 @@
|
||||
return defenseType === DefenseType.SmallShieldDome || defenseType === DefenseType.LargeShieldDome
|
||||
}
|
||||
|
||||
const buildDefense = (defenseType: DefenseType, quantity: number): boolean => {
|
||||
const buildDefense = (defenseType: DefenseType, quantity: number): { success: boolean; reason?: string } => {
|
||||
const currentPlanet = gameStore.currentPlanet
|
||||
if (!currentPlanet) return false
|
||||
if (!currentPlanet) return { success: false }
|
||||
const validation = shipValidation.validateDefenseBuild(currentPlanet, defenseType, quantity, gameStore.player.technologies)
|
||||
if (!validation.valid) return false
|
||||
if (!validation.valid) return { success: false, reason: validation.reason }
|
||||
const queueItem = shipValidation.executeDefenseBuild(currentPlanet, defenseType, quantity, gameStore.player.officers)
|
||||
currentPlanet.buildQueue.push(queueItem)
|
||||
return true
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
// 建造防御设施
|
||||
@@ -251,10 +251,10 @@
|
||||
return
|
||||
}
|
||||
|
||||
const success = buildDefense(defenseType, quantity)
|
||||
if (!success) {
|
||||
const result = buildDefense(defenseType, quantity)
|
||||
if (!result.success) {
|
||||
alertDialogTitle.value = t('defenseView.buildFailed')
|
||||
alertDialogMessage.value = t('defenseView.buildFailedMessage')
|
||||
alertDialogMessage.value = result.reason ? t(result.reason) : t('defenseView.buildFailedMessage')
|
||||
alertDialogOpen.value = true
|
||||
} else {
|
||||
quantities.value[defenseType] = 0
|
||||
|
||||
@@ -12,7 +12,12 @@
|
||||
<TabsList class="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="all">
|
||||
{{ t('diplomacy.tabs.all') }}
|
||||
<Badge variant="outline" class="ml-2 bg-blue-100 dark:bg-blue-950 text-blue-700 dark:text-blue-300 border-blue-300 dark:border-blue-700">{{ allNpcs.length }}</Badge>
|
||||
<Badge
|
||||
variant="outline"
|
||||
class="ml-2 bg-blue-100 dark:bg-blue-950 text-blue-700 dark:text-blue-300 border-blue-300 dark:border-blue-700"
|
||||
>
|
||||
{{ allNpcs.length }}
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="friendly">
|
||||
{{ t('diplomacy.tabs.friendly') }}
|
||||
@@ -50,7 +55,13 @@
|
||||
</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)" />
|
||||
<NpcRelationCard
|
||||
v-for="npc in paginatedAllNpcs"
|
||||
:key="npc.id"
|
||||
:npc="npc"
|
||||
:relation="getRelation(npc.id)"
|
||||
:data-npc-id="npc.id"
|
||||
/>
|
||||
</div>
|
||||
<Pagination
|
||||
v-if="totalPagesAll > 1"
|
||||
@@ -84,7 +95,13 @@
|
||||
</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)" />
|
||||
<NpcRelationCard
|
||||
v-for="npc in paginatedFriendlyNpcs"
|
||||
:key="npc.id"
|
||||
:npc="npc"
|
||||
:relation="getRelation(npc.id)"
|
||||
:data-npc-id="npc.id"
|
||||
/>
|
||||
</div>
|
||||
<Pagination
|
||||
v-if="totalPagesFriendly > 1"
|
||||
@@ -118,7 +135,13 @@
|
||||
</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)" />
|
||||
<NpcRelationCard
|
||||
v-for="npc in paginatedNeutralNpcs"
|
||||
:key="npc.id"
|
||||
:npc="npc"
|
||||
:relation="getRelation(npc.id)"
|
||||
:data-npc-id="npc.id"
|
||||
/>
|
||||
</div>
|
||||
<Pagination
|
||||
v-if="totalPagesNeutral > 1"
|
||||
@@ -152,7 +175,13 @@
|
||||
</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)" />
|
||||
<NpcRelationCard
|
||||
v-for="npc in paginatedHostileNpcs"
|
||||
:key="npc.id"
|
||||
:npc="npc"
|
||||
:relation="getRelation(npc.id)"
|
||||
:data-npc-id="npc.id"
|
||||
/>
|
||||
</div>
|
||||
<Pagination
|
||||
v-if="totalPagesHostile > 1"
|
||||
@@ -179,57 +208,20 @@
|
||||
</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, onMounted } from 'vue'
|
||||
import { computed, ref, onMounted, onUnmounted, nextTick } 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'
|
||||
import { RelationStatus } from '@/types/game'
|
||||
import type { DiplomaticRelation } from '@/types/game'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const npcStore = useNPCStore()
|
||||
@@ -281,6 +273,49 @@
|
||||
// 组件挂载时初始化NPC盟友
|
||||
onMounted(() => {
|
||||
initializeNPCAllies()
|
||||
|
||||
// 监听滚动到NPC卡片的事件
|
||||
const handleScrollToNpc = (event: Event) => {
|
||||
const customEvent = event as CustomEvent<{ npcId: string }>
|
||||
const npcId = customEvent.detail.npcId
|
||||
|
||||
// 切换到"全部"标签
|
||||
activeTab.value = 'all'
|
||||
|
||||
// 等待DOM更新后再滚动
|
||||
nextTick(() => {
|
||||
// 找到目标NPC在列表中的索引
|
||||
const npcIndex = allNpcs.value.findIndex(npc => npc.id === npcId)
|
||||
if (npcIndex === -1) return
|
||||
|
||||
// 计算目标NPC所在的页面
|
||||
const targetPage = Math.floor(npcIndex / ITEMS_PER_PAGE) + 1
|
||||
currentPage.value.all = targetPage
|
||||
|
||||
// 再次等待分页更新后滚动到卡片
|
||||
nextTick(() => {
|
||||
// 使用data属性来标识卡片
|
||||
const cards = document.querySelectorAll('[data-npc-id]')
|
||||
const targetCard = Array.from(cards).find(card => card.getAttribute('data-npc-id') === npcId)
|
||||
|
||||
if (targetCard) {
|
||||
targetCard.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
// 添加高亮效果
|
||||
targetCard.classList.add('ring-2', 'ring-primary', 'ring-offset-2')
|
||||
setTimeout(() => {
|
||||
targetCard.classList.remove('ring-2', 'ring-primary', 'ring-offset-2')
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
document.addEventListener('scrollToNpc', handleScrollToNpc)
|
||||
|
||||
// 清理事件监听器
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('scrollToNpc', handleScrollToNpc)
|
||||
})
|
||||
})
|
||||
|
||||
// 分页状态
|
||||
@@ -387,65 +422,4 @@
|
||||
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>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<!-- 标签切换 -->
|
||||
<Tabs v-model="activeTab" class="w-full">
|
||||
<TabsList class="grid w-full grid-cols-2 sm:grid-cols-4" :tab-count="4">
|
||||
<TabsList class="grid w-full grid-cols-2 sm:grid-cols-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>
|
||||
@@ -296,6 +296,249 @@
|
||||
|
||||
<!-- 间谍报告对话框 -->
|
||||
<SpyReportDialog v-model:open="showSpyDialog" :report="selectedSpyReport" />
|
||||
|
||||
<!-- 被侦查通知详情对话框 -->
|
||||
<Dialog :open="showSpiedDialog" @update:open="showSpiedDialog = $event">
|
||||
<DialogContent class="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle class="flex items-center gap-2">
|
||||
<Eye class="h-5 w-5 text-purple-500" />
|
||||
{{ t('messagesView.spiedNotificationDetails') }}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div v-if="selectedSpiedNotification" class="space-y-4">
|
||||
<!-- 侦查者信息 -->
|
||||
<div class="p-4 bg-muted/50 rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<h3 class="font-semibold text-lg">{{ selectedSpiedNotification.npcName }}</h3>
|
||||
<Badge variant="destructive">{{ t('messagesView.spyDetected') }}</Badge>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ formatDate(selectedSpiedNotification.timestamp) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 被侦查星球 -->
|
||||
<div class="space-y-2">
|
||||
<h4 class="font-semibold text-sm">{{ t('messagesView.targetPlanet') }}</h4>
|
||||
<div class="p-3 bg-muted/30 rounded-md flex items-center gap-2">
|
||||
<Globe class="h-4 w-4 text-blue-500" />
|
||||
<span class="font-medium">{{ selectedSpiedNotification.targetPlanetName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 检测结果 -->
|
||||
<div class="space-y-2">
|
||||
<h4 class="font-semibold text-sm">{{ t('messagesView.detectionResult') }}</h4>
|
||||
<div class="p-3 bg-muted/30 rounded-md">
|
||||
<div v-if="selectedSpiedNotification.detectionSuccess" class="flex items-center gap-2 text-yellow-600 dark:text-yellow-400">
|
||||
<AlertTriangle class="h-5 w-5" />
|
||||
<span class="font-medium">{{ t('messagesView.detectionSuccess') }}</span>
|
||||
</div>
|
||||
<p class="text-sm mt-2">
|
||||
{{
|
||||
t('messagesView.spiedNotificationMessage', {
|
||||
npc: selectedSpiedNotification.npcName,
|
||||
planet: selectedSpiedNotification.targetPlanetName
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 建议 -->
|
||||
<div class="p-3 bg-blue-50 dark:bg-blue-950/30 rounded-md border border-blue-200 dark:border-blue-800">
|
||||
<p class="text-sm text-blue-800 dark:text-blue-200">
|
||||
{{ t('messagesView.spiedNotificationTip') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showSpiedDialog = false">{{ t('common.close') }}</Button>
|
||||
<Button @click="viewNPCInGalaxy(selectedSpiedNotification?.npcId)">{{ t('messagesView.viewInGalaxy') }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<!-- 任务报告详情对话框 -->
|
||||
<Dialog :open="showMissionDialog" @update:open="showMissionDialog = $event">
|
||||
<DialogContent class="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle class="flex items-center gap-2">
|
||||
<component :is="getMissionIcon(selectedMissionReport?.missionType)" class="h-5 w-5" />
|
||||
{{ t('messagesView.missionReportDetails') }}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div v-if="selectedMissionReport" class="space-y-4">
|
||||
<!-- 任务状态 -->
|
||||
<div class="p-4 bg-muted/50 rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<h3 class="font-semibold text-lg">{{ getMissionTypeName(selectedMissionReport.missionType) }}</h3>
|
||||
<Badge :variant="selectedMissionReport.success ? 'default' : 'destructive'">
|
||||
{{ selectedMissionReport.success ? t('messagesView.missionSuccess') : t('messagesView.missionFailed') }}
|
||||
</Badge>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ formatDate(selectedMissionReport.timestamp) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 起点和终点 -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<h4 class="font-semibold text-sm">{{ t('messagesView.origin') }}</h4>
|
||||
<div class="p-3 bg-muted/30 rounded-md">
|
||||
<p class="font-medium">{{ selectedMissionReport.originPlanetName }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<h4 class="font-semibold text-sm">{{ t('messagesView.destination') }}</h4>
|
||||
<div class="p-3 bg-muted/30 rounded-md">
|
||||
<p class="font-medium" v-if="selectedMissionReport.targetPlanetName">{{ selectedMissionReport.targetPlanetName }}</p>
|
||||
<p class="text-sm text-muted-foreground" v-else>
|
||||
[{{ selectedMissionReport.targetPosition.galaxy }}:{{ selectedMissionReport.targetPosition.system }}:{{
|
||||
selectedMissionReport.targetPosition.position
|
||||
}}]
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务详情 -->
|
||||
<div class="space-y-2">
|
||||
<h4 class="font-semibold text-sm">{{ t('messagesView.missionDetails') }}</h4>
|
||||
<div class="p-3 bg-muted/30 rounded-md">
|
||||
<p class="text-sm mb-2">{{ selectedMissionReport.message }}</p>
|
||||
|
||||
<!-- 运输任务详情 -->
|
||||
<div v-if="selectedMissionReport.details?.transportedResources" class="mt-3 space-y-1">
|
||||
<p class="text-xs font-semibold text-muted-foreground">{{ t('messagesView.transportedResources') }}:</p>
|
||||
<div class="grid grid-cols-3 gap-2 text-sm">
|
||||
<div>{{ t('resources.metal') }}: {{ selectedMissionReport.details.transportedResources.metal.toLocaleString() }}</div>
|
||||
<div>{{ t('resources.crystal') }}: {{ selectedMissionReport.details.transportedResources.crystal.toLocaleString() }}</div>
|
||||
<div>
|
||||
{{ t('resources.deuterium') }}: {{ selectedMissionReport.details.transportedResources.deuterium.toLocaleString() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 回收任务详情 -->
|
||||
<div v-if="selectedMissionReport.details?.recycledResources" class="mt-3 space-y-1">
|
||||
<p class="text-xs font-semibold text-muted-foreground">{{ t('messagesView.recycledResources') }}:</p>
|
||||
<div class="grid grid-cols-2 gap-2 text-sm">
|
||||
<div>{{ t('resources.metal') }}: {{ selectedMissionReport.details.recycledResources.metal.toLocaleString() }}</div>
|
||||
<div>{{ t('resources.crystal') }}: {{ selectedMissionReport.details.recycledResources.crystal.toLocaleString() }}</div>
|
||||
</div>
|
||||
<div v-if="selectedMissionReport.details.remainingDebris" class="mt-2">
|
||||
<p class="text-xs font-semibold text-muted-foreground">{{ t('messagesView.remainingDebris') }}:</p>
|
||||
<div class="grid grid-cols-2 gap-2 text-sm text-yellow-600 dark:text-yellow-400">
|
||||
<div>{{ t('resources.metal') }}: {{ selectedMissionReport.details.remainingDebris.metal.toLocaleString() }}</div>
|
||||
<div>{{ t('resources.crystal') }}: {{ selectedMissionReport.details.remainingDebris.crystal.toLocaleString() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 殖民任务详情 -->
|
||||
<div v-if="selectedMissionReport.details?.newPlanetName" class="mt-3">
|
||||
<p class="text-xs font-semibold text-muted-foreground">{{ t('messagesView.newPlanet') }}:</p>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<Globe class="h-4 w-4 text-green-500" />
|
||||
<span class="font-medium">{{ selectedMissionReport.details.newPlanetName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showMissionDialog = false">{{ t('common.close') }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<!-- NPC活动通知详情对话框 -->
|
||||
<Dialog :open="showNPCActivityDialog" @update:open="showNPCActivityDialog = $event">
|
||||
<DialogContent class="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle class="flex items-center gap-2">
|
||||
<Recycle class="h-5 w-5 text-yellow-500" />
|
||||
{{ t('messagesView.npcActivityDetails') }}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div v-if="selectedNPCActivityNotification" class="space-y-4">
|
||||
<!-- NPC信息 -->
|
||||
<div class="p-4 bg-muted/50 rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<h3 class="font-semibold text-lg">{{ selectedNPCActivityNotification.npcName }}</h3>
|
||||
<Badge variant="secondary">{{ t('messagesView.activityType.' + selectedNPCActivityNotification.activityType) }}</Badge>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ formatDate(selectedNPCActivityNotification.timestamp) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 活动位置 -->
|
||||
<div class="space-y-2">
|
||||
<h4 class="font-semibold text-sm">{{ t('messagesView.activityLocation') }}</h4>
|
||||
<div class="p-3 bg-muted/30 rounded-md">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<Globe class="h-4 w-4 text-blue-500" />
|
||||
<span class="font-medium">
|
||||
{{ t('messagesView.position') }}: [{{ selectedNPCActivityNotification.targetPosition.galaxy }}:{{
|
||||
selectedNPCActivityNotification.targetPosition.system
|
||||
}}:{{ selectedNPCActivityNotification.targetPosition.position }}]
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="selectedNPCActivityNotification.targetPlanetName" class="text-sm text-muted-foreground">
|
||||
{{ t('messagesView.nearPlanet') }}: {{ selectedNPCActivityNotification.targetPlanetName }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 活动描述 -->
|
||||
<div class="space-y-2">
|
||||
<h4 class="font-semibold text-sm">{{ t('messagesView.activityDescription') }}</h4>
|
||||
<div class="p-3 bg-muted/30 rounded-md">
|
||||
<p class="text-sm">
|
||||
{{
|
||||
t('messagesView.npcActivityMessage', {
|
||||
npc: selectedNPCActivityNotification.npcName,
|
||||
activity: t('messagesView.activityType.' + selectedNPCActivityNotification.activityType),
|
||||
position: `[${selectedNPCActivityNotification.targetPosition.galaxy}:${selectedNPCActivityNotification.targetPosition.system}:${selectedNPCActivityNotification.targetPosition.position}]`
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 到达时间 -->
|
||||
<div class="space-y-2">
|
||||
<h4 class="font-semibold text-sm">{{ t('messagesView.arrivalTime') }}</h4>
|
||||
<div class="p-3 bg-muted/30 rounded-md">
|
||||
<p class="font-medium">{{ formatDate(selectedNPCActivityNotification.arrivalTime) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<div class="p-3 bg-yellow-50 dark:bg-yellow-950/30 rounded-md border border-yellow-200 dark:border-yellow-800">
|
||||
<p class="text-sm text-yellow-800 dark:text-yellow-200">
|
||||
{{ t('messagesView.npcActivityTip') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showNPCActivityDialog = false">{{ t('common.close') }}</Button>
|
||||
<Button @click="viewLocationInGalaxy(selectedNPCActivityNotification?.targetPosition)">
|
||||
{{ t('messagesView.viewInGalaxy') }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -303,14 +546,16 @@
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
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 { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||
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 } from 'lucide-vue-next'
|
||||
import { X, Sword, Eye, AlertTriangle, Package, Recycle, Gift, Ban, Check, Users, Skull, Globe } from 'lucide-vue-next'
|
||||
import type {
|
||||
BattleResult,
|
||||
SpyReport,
|
||||
@@ -324,6 +569,7 @@
|
||||
import { useNPCStore } from '@/stores/npcStore'
|
||||
import * as diplomaticLogic from '@/logic/diplomaticLogic'
|
||||
|
||||
const router = useRouter()
|
||||
const gameStore = useGameStore()
|
||||
const npcStore = useNPCStore()
|
||||
const { t } = useI18n()
|
||||
@@ -332,8 +578,14 @@
|
||||
// 对话框状态
|
||||
const showBattleDialog = ref(false)
|
||||
const showSpyDialog = ref(false)
|
||||
const showSpiedDialog = ref(false)
|
||||
const showMissionDialog = ref(false)
|
||||
const showNPCActivityDialog = ref(false)
|
||||
const selectedBattleReport = ref<BattleResult | null>(null)
|
||||
const selectedSpyReport = ref<SpyReport | null>(null)
|
||||
const selectedSpiedNotification = ref<SpiedNotification | null>(null)
|
||||
const selectedMissionReport = ref<MissionReport | null>(null)
|
||||
const selectedNPCActivityNotification = ref<NPCActivityNotification | null>(null)
|
||||
|
||||
// 排序后的战斗报告(最新的在前)
|
||||
const sortedBattleReports = computed(() => {
|
||||
@@ -525,6 +777,9 @@
|
||||
if (!notification.read) {
|
||||
notification.read = true
|
||||
}
|
||||
// 设置选中的通知并打开详情对话框
|
||||
selectedSpiedNotification.value = notification
|
||||
showSpiedDialog.value = true
|
||||
}
|
||||
|
||||
// 删除战斗报告
|
||||
@@ -560,6 +815,9 @@
|
||||
if (!notification.read) {
|
||||
notification.read = true
|
||||
}
|
||||
// 设置选中的通知并打开详情对话框
|
||||
selectedNPCActivityNotification.value = notification
|
||||
showNPCActivityDialog.value = true
|
||||
}
|
||||
|
||||
// 删除NPC活动通知
|
||||
@@ -592,6 +850,9 @@
|
||||
if (!report.read) {
|
||||
report.read = true
|
||||
}
|
||||
// 设置选中的报告并打开详情对话框
|
||||
selectedMissionReport.value = report
|
||||
showMissionDialog.value = true
|
||||
}
|
||||
|
||||
// 删除任务报告
|
||||
@@ -656,4 +917,56 @@
|
||||
gameStore.player.giftRejectedNotifications.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 查看NPC在星系中的位置
|
||||
const viewNPCInGalaxy = (npcId?: string) => {
|
||||
if (!npcId) return
|
||||
const npc = npcStore.npcs.find(n => n.id === npcId)
|
||||
if (!npc || npc.planets.length === 0) return
|
||||
|
||||
const targetPlanet = npc.planets[0]
|
||||
if (!targetPlanet) return
|
||||
|
||||
showSpiedDialog.value = false
|
||||
router.push({
|
||||
path: '/galaxy',
|
||||
query: {
|
||||
galaxy: targetPlanet.position.galaxy,
|
||||
system: targetPlanet.position.system,
|
||||
highlightNpc: npcId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 查看位置在星系中
|
||||
const viewLocationInGalaxy = (position?: { galaxy: number; system: number; position: number }) => {
|
||||
if (!position) return
|
||||
|
||||
showNPCActivityDialog.value = false
|
||||
router.push({
|
||||
path: '/galaxy',
|
||||
query: {
|
||||
galaxy: position.galaxy,
|
||||
system: position.system
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取任务类型图标
|
||||
const getMissionIcon = (missionType?: MissionType) => {
|
||||
if (!missionType) return Package
|
||||
|
||||
switch (missionType) {
|
||||
case MissionType.Transport:
|
||||
return Package
|
||||
case MissionType.Recycle:
|
||||
return Recycle
|
||||
case MissionType.Colonize:
|
||||
return Globe
|
||||
case MissionType.Destroy:
|
||||
return Skull
|
||||
default:
|
||||
return Package
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div v-if="planet" class="container mx-auto p-4 sm:p-6">
|
||||
<!-- 未解锁遮罩 -->
|
||||
<UnlockRequirement :required-building="BuildingType.ResearchLab" :required-level="1" />
|
||||
<!-- <UnlockRequirement :required-building="BuildingType.ResearchLab" :required-level="1" /> -->
|
||||
|
||||
<h1 class="text-2xl sm:text-3xl font-bold mb-4 sm:mb-6">{{ t('researchView.title') }}</h1>
|
||||
|
||||
<div class="grid grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4">
|
||||
<Card v-for="techType in Object.values(TechnologyType)" :key="techType" class="relative">
|
||||
<Card v-for="techType in Object.values(TechnologyType)" :key="techType" :data-tech="techType" class="relative">
|
||||
<CardUnlockOverlay :requirements="TECHNOLOGIES[techType].requirements" :currentLevel="getTechLevel(techType)" />
|
||||
<CardHeader>
|
||||
<div class="mb-2">
|
||||
@@ -98,7 +98,6 @@
|
||||
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'
|
||||
import * as publicLogic from '@/logic/publicLogic'
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<div
|
||||
class="h-full transition-all duration-300"
|
||||
:class="fleetStorageUsage > maxFleetStorage ? 'bg-destructive' : 'bg-primary'"
|
||||
:style="{ width: `${Math.min((fleetStorageUsage / maxFleetStorage) * 100, 100)}%` }"
|
||||
:style="{ width: `${maxFleetStorage > 0 ? Math.min((fleetStorageUsage / maxFleetStorage) * 100, 100) : 0}%` }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -216,13 +216,13 @@
|
||||
[ShipType.Deathstar]: 0
|
||||
})
|
||||
|
||||
const buildShip = (shipType: ShipType, quantity: number): boolean => {
|
||||
if (!gameStore.currentPlanet) return false
|
||||
const buildShip = (shipType: ShipType, quantity: number): { success: boolean; reason?: string } => {
|
||||
if (!gameStore.currentPlanet) return { success: false }
|
||||
const validation = shipValidation.validateShipBuild(gameStore.currentPlanet, shipType, quantity, gameStore.player.technologies)
|
||||
if (!validation.valid) return false
|
||||
if (!validation.valid) return { success: false, reason: validation.reason }
|
||||
const queueItem = shipValidation.executeShipBuild(gameStore.currentPlanet, shipType, quantity, gameStore.player.officers)
|
||||
gameStore.currentPlanet.buildQueue.push(queueItem)
|
||||
return true
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
// 建造舰船
|
||||
@@ -235,10 +235,10 @@
|
||||
return
|
||||
}
|
||||
|
||||
const success = buildShip(shipType, quantity)
|
||||
if (!success) {
|
||||
const result = buildShip(shipType, quantity)
|
||||
if (!result.success) {
|
||||
alertDialogTitle.value = t('shipyardView.buildFailed')
|
||||
alertDialogMessage.value = t('shipyardView.buildFailedMessage')
|
||||
alertDialogMessage.value = result.reason ? t(result.reason) : t('shipyardView.buildFailedMessage')
|
||||
alertDialogOpen.value = true
|
||||
} else {
|
||||
quantities.value[shipType] = 0
|
||||
@@ -260,6 +260,11 @@
|
||||
darkMatter: config.cost.darkMatter * quantity
|
||||
}
|
||||
|
||||
// 检查舰队仓储空间是否足够
|
||||
if (!fleetStorageLogic.hasEnoughFleetStorage(planet.value, shipType, quantity, gameStore.player.technologies)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
publicLogic.checkRequirements(planet.value, gameStore.player.technologies, config.requirements) &&
|
||||
planet.value.resources.metal >= totalCost.metal &&
|
||||
|
||||
Reference in New Issue
Block a user