feat: 新增多语言README并优化文档结构

新增德语、俄语、韩语、繁体中文多语言README,英文与简体中文README同步优化,统一下载链接与徽章样式,完善多语言入口。提升国际化支持与文档可读性。
This commit is contained in:
谦君
2025-12-24 01:45:17 +08:00
parent a475b1b554
commit 5e3557e2da
105 changed files with 12459 additions and 1690 deletions

View File

@@ -162,8 +162,8 @@
import { ShipType, DefenseType } from '@/types/game'
import type { Fleet, BattleResult } from '@/types/game'
import { workerManager } from '@/workers/workerManager'
import ResourceIcon from '@/components/ResourceIcon.vue'
import BattleReportDialog from '@/components/BattleReportDialog.vue'
import ResourceIcon from '@/components/common/ResourceIcon.vue'
import BattleReportDialog from '@/components/dialogs/BattleReportDialog.vue'
import { Sword, Shield, Zap, RotateCcw } from 'lucide-vue-next'
import * as planetLogic from '@/logic/planetLogic'

View File

@@ -81,20 +81,30 @@
<div class="text-xs sm:text-sm space-y-0.5 sm:space-y-1">
<div class="flex items-center gap-1.5 text-muted-foreground">
<Clock :size="14" class="flex-shrink-0" />
<Clock :size="14" class="shrink-0" />
<span>{{ formatTime(getBuildingTime(buildingType, getBuildingLevel(buildingType) + 1)) }}</span>
</div>
<div class="flex items-center gap-1.5 text-muted-foreground">
<Grid3x3 :size="14" class="flex-shrink-0" />
<Grid3x3 :size="14" class="shrink-0" />
<span>{{ BUILDINGS[buildingType].spaceUsage }}</span>
</div>
</div>
<!-- 升级按钮 -->
<Button @click="handleUpgrade(buildingType)" :disabled="!canUpgrade(buildingType)" class="w-full">
<Button @click="handleUpgrade(buildingType, $event)" :disabled="!canUpgrade(buildingType)" class="w-full">
{{ getUpgradeButtonText(buildingType) }}
</Button>
<!-- 添加到等待队列按钮 -->
<Button
v-if="canAddToWaitingQueue(buildingType)"
@click="handleAddToWaiting(buildingType, $event)"
variant="outline"
class="w-full"
>
{{ t('queue.addToWaiting') }}
</Button>
<!-- 拆除按钮 -->
<Button
v-if="getBuildingLevel(buildingType) > 0"
@@ -134,8 +144,8 @@
<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" />
<Check v-if="req.met" :size="16" class="text-green-500 shrink-0" />
<X v-else :size="16" class="text-red-500 shrink-0" />
<span>{{ req.name }}: Lv {{ req.requiredLevel }} ({{ t('common.current') }}: Lv {{ req.currentLevel }})</span>
</div>
</div>
@@ -176,8 +186,8 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import ResourceIcon from '@/components/ResourceIcon.vue'
import CardUnlockOverlay from '@/components/CardUnlockOverlay.vue'
import ResourceIcon from '@/components/common/ResourceIcon.vue'
import CardUnlockOverlay from '@/components/common/CardUnlockOverlay.vue'
import {
AlertDialog,
AlertDialogAction,
@@ -195,6 +205,8 @@
import * as publicLogic from '@/logic/publicLogic'
import * as officerLogic from '@/logic/officerLogic'
import * as gameLogic from '@/logic/gameLogic'
import * as waitingQueueLogic from '@/logic/waitingQueueLogic'
import { triggerQueueAnimation } from '@/composables/useQueueAnimation'
const gameStore = useGameStore()
const detailDialog = useDetailDialogStore()
@@ -263,7 +275,7 @@
}
// 升级建筑
const handleUpgrade = (buildingType: BuildingType) => {
const handleUpgrade = (buildingType: BuildingType, event: MouseEvent) => {
// 检查前置条件
if (!checkUpgradeRequirements(buildingType)) {
alertDialogTitle.value = t('common.requirementsNotMet')
@@ -280,6 +292,9 @@
alertDialogMessage.value = result.reason ? t(result.reason) : t('buildingsView.upgradeFailedMessage')
alertDialogShowRequirements.value = false
alertDialogOpen.value = true
} else {
// 触发抛物线动画
triggerQueueAnimation(event, 'building')
}
}
@@ -432,12 +447,8 @@
}
const handleDemolish = (buildingType: BuildingType) => {
const buildingName = BUILDINGS.value[buildingType].name
const refund = getDemolishRefund(buildingType)
demolishConfirmMessage.value = `${t('buildingsView.confirmDemolishMessage')}: ${buildingName}
${t('buildingsView.demolishRefund')}:
demolishConfirmMessage.value = `${t('buildingsView.demolishRefund')}:
${t('resources.metal')}: ${formatNumber(refund.metal)}
${t('resources.crystal')}: ${formatNumber(refund.crystal)}
${t('resources.deuterium')}: ${formatNumber(refund.deuterium)}${
@@ -482,4 +493,80 @@ ${t('resources.deuterium')}: ${formatNumber(refund.deuterium)}${
const currentLevel = getBuildingLevel(buildingType)
return buildingLogic.calculateDemolishRefund(buildingType, currentLevel)
}
// 检查是否可以添加到等待队列
const canAddToWaitingQueue = (buildingType: BuildingType): boolean => {
if (!planet.value) return false
const config = BUILDINGS.value[buildingType]
const currentLevel = getBuildingLevel(buildingType)
// 计算目标等级:当前等级 + 正式队列中的升级数 + 等待队列中的升级数 + 1
const upgradesInBuildQueue = planet.value.buildQueue.filter(q => q.type === 'building' && q.itemType === buildingType).length
const waitingQueue = planet.value.waitingBuildQueue || []
const upgradesInWaitingQueue = waitingQueue.filter(q => q.type === 'building' && q.itemType === buildingType).length
const targetLevel = currentLevel + upgradesInBuildQueue + upgradesInWaitingQueue + 1
// 检查是否达到等级上限(使用计算后的目标等级)
if (config.maxLevel !== undefined && targetLevel > config.maxLevel) {
return false
}
// 检查目标等级的前置条件是否满足
// 如果该建筑已经在队列中(正式或等待),说明基本条件已满足,跳过检查
const alreadyInQueue = upgradesInBuildQueue > 0 || upgradesInWaitingQueue > 0
if (!alreadyInQueue) {
// 第一次添加时,检查当前等级+1的前置条件
if (!checkUpgradeRequirements(buildingType)) {
return false
}
} else {
// 后续添加时,检查目标等级的前置条件
const requirements = publicLogic.getLevelRequirements(config, targetLevel)
if (requirements && Object.keys(requirements).length > 0) {
if (!publicLogic.checkRequirements(planet.value, gameStore.player.technologies, requirements)) {
return false
}
}
}
// 建筑可以多次排队比如金属矿升级到2、3、4、5级
// 只需要检查等待队列是否已满
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
const maxWaitingQueue = waitingQueueLogic.getMaxBuildWaitingQueue(planet.value, bonuses.additionalBuildQueue)
if (waitingQueue.length >= maxWaitingQueue) {
return false
}
return true
}
// 添加到等待队列
const handleAddToWaiting = (buildingType: BuildingType, event: MouseEvent) => {
if (!planet.value) return
const currentLevel = getBuildingLevel(buildingType)
// 计算目标等级:当前等级 + 正式队列中的升级数 + 等待队列中的升级数 + 1
const upgradesInBuildQueue = planet.value.buildQueue.filter(q => q.type === 'building' && q.itemType === buildingType).length
const waitingQueue = planet.value.waitingBuildQueue || []
const upgradesInWaitingQueue = waitingQueue.filter(q => q.type === 'building' && q.itemType === buildingType).length
const targetLevel = currentLevel + upgradesInBuildQueue + upgradesInWaitingQueue + 1
const item = waitingQueueLogic.createBuildingWaitingItem(buildingType, targetLevel, planet.value.id)
const result = waitingQueueLogic.canAddToBuildWaitingQueue(planet.value, item, gameStore.player.officers)
if (!result.canAdd) {
alertDialogTitle.value = t('queue.waitingQueueFull')
alertDialogMessage.value = result.reason ? t(result.reason) : ''
alertDialogShowRequirements.value = false
alertDialogOpen.value = true
return
}
// 触发抛物线动画
triggerQueueAnimation(event, 'building')
waitingQueueLogic.addToBuildWaitingQueue(planet.value, item)
}
</script>

454
src/views/CampaignView.vue Normal file
View File

@@ -0,0 +1,454 @@
<template>
<div class="container mx-auto px-4 py-6 max-w-6xl">
<!-- 战役标题和总进度 -->
<Card class="mb-6">
<CardHeader>
<div class="flex items-center justify-between">
<div>
<CardTitle class="flex items-center gap-2 text-xl">
<Scroll class="h-6 w-6 text-primary" />
{{ t('campaign.name') }}
</CardTitle>
<CardDescription class="mt-1">{{ t('campaign.description') }}</CardDescription>
</div>
<div class="text-right">
<div class="text-2xl font-bold text-primary">{{ totalProgress }}%</div>
<div class="text-xs text-muted-foreground">{{ t('campaign.totalProgress') }}</div>
</div>
</div>
</CardHeader>
<CardContent>
<Progress :model-value="totalProgress" class="h-3" />
<div class="flex justify-between mt-2 text-xs text-muted-foreground">
<span>{{ completedQuestCount }} / {{ totalQuestCount }} {{ t('campaign.questsCompleted') }}</span>
<span>{{ t('campaign.chapter') }} {{ currentChapter }}</span>
</div>
</CardContent>
</Card>
<!-- 章节选择标签 -->
<Tabs v-model="activeChapter" class="mb-6">
<TabsList class="grid w-full" :style="{ gridTemplateColumns: `repeat(${chapters.length}, 1fr)` }">
<TabsTrigger
v-for="chapter in chapters"
:key="chapter.id"
:value="chapter.number.toString()"
:disabled="chapter.number > currentChapter"
class="relative"
>
<span class="hidden sm:inline">{{ t(chapter.titleKey) }}</span>
<span class="sm:hidden">{{ chapter.number }}</span>
<Badge
v-if="getChapterProgress(chapter.number) === 100"
variant="default"
class="absolute -top-1 -right-1 h-4 w-4 p-0 flex items-center justify-center text-[10px]"
>
<Check class="h-3 w-3" />
</Badge>
</TabsTrigger>
</TabsList>
<!-- 章节内容 -->
<TabsContent v-for="chapter in chapters" :key="chapter.id" :value="chapter.number.toString()" class="mt-4">
<!-- 章节背景故事 -->
<Card class="mb-4 bg-gradient-to-r from-primary/5 to-transparent">
<CardContent class="py-4">
<p class="text-sm text-muted-foreground italic">{{ t(chapter.backgroundStoryKey) }}</p>
</CardContent>
</Card>
<!-- 任务地图 -->
<QuestMap
:quests="getChapterQuests(chapter.number)"
:progress="campaignProgress"
@select-quest="handleQuestSelect"
/>
</TabsContent>
</Tabs>
<!-- 任务详情面板 -->
<Card v-if="selectedQuest" class="mt-6">
<CardHeader>
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div
:class="[
'h-10 w-10 rounded-full flex items-center justify-center',
getQuestStatusClass(selectedQuest.id)
]"
>
<component :is="getQuestStatusIcon(selectedQuest.id)" class="h-5 w-5" />
</div>
<div>
<CardTitle class="flex items-center gap-2">
{{ t(selectedQuest.titleKey) }}
<Badge v-if="selectedQuest.isBoss" variant="destructive">BOSS</Badge>
<Badge v-if="selectedQuest.isBranch" variant="secondary">{{ t('campaign.branch') }}</Badge>
</CardTitle>
<CardDescription>{{ t(selectedQuest.descriptionKey) }}</CardDescription>
</div>
</div>
<Button
v-if="canStartQuest(selectedQuest.id)"
@click="handleStartQuest(selectedQuest.id)"
>
<Play class="h-4 w-4 mr-2" />
{{ t('campaign.startQuest') }}
</Button>
<Button
v-else-if="canClaimRewards(selectedQuest.id)"
@click="handleClaimRewards(selectedQuest.id)"
variant="default"
>
<Gift class="h-4 w-4 mr-2" />
{{ t('campaign.claimRewards') }}
</Button>
</div>
</CardHeader>
<CardContent class="space-y-6">
<!-- 任务目标 -->
<div>
<h4 class="font-semibold mb-3 flex items-center gap-2">
<Target class="h-4 w-4" />
{{ t('campaign.objectives') }}
</h4>
<div class="space-y-3">
<div
v-for="objective in selectedQuest.objectives"
:key="objective.id"
class="flex items-center gap-3"
>
<div
:class="[
'h-6 w-6 rounded-full flex items-center justify-center text-xs',
isObjectiveCompleted(selectedQuest.id, objective.id)
? 'bg-green-500 text-white'
: 'bg-muted text-muted-foreground'
]"
>
<Check v-if="isObjectiveCompleted(selectedQuest.id, objective.id)" class="h-4 w-4" />
<span v-else>{{ getObjectiveProgress(selectedQuest.id, objective.id) }}</span>
</div>
<div class="flex-1">
<div class="text-sm">{{ t(objective.descriptionKey) }}</div>
<Progress
:model-value="(getObjectiveProgress(selectedQuest.id, objective.id) / objective.required) * 100"
class="h-1.5 mt-1"
/>
</div>
<span class="text-xs text-muted-foreground">
{{ getObjectiveProgress(selectedQuest.id, objective.id) }} / {{ objective.required }}
</span>
</div>
</div>
</div>
<!-- 任务奖励 -->
<div>
<h4 class="font-semibold mb-3 flex items-center gap-2">
<Gift class="h-4 w-4" />
{{ t('campaign.rewards') }}
</h4>
<div class="flex flex-wrap gap-3">
<Badge v-if="selectedQuest.rewards.resources?.metal" variant="outline" class="gap-1">
<ResourceIcon type="metal" size="sm" />
{{ formatNumber(selectedQuest.rewards.resources.metal) }}
</Badge>
<Badge v-if="selectedQuest.rewards.resources?.crystal" variant="outline" class="gap-1">
<ResourceIcon type="crystal" size="sm" />
{{ formatNumber(selectedQuest.rewards.resources.crystal) }}
</Badge>
<Badge v-if="selectedQuest.rewards.resources?.deuterium" variant="outline" class="gap-1">
<ResourceIcon type="deuterium" size="sm" />
{{ formatNumber(selectedQuest.rewards.resources.deuterium) }}
</Badge>
<Badge v-if="selectedQuest.rewards.darkMatter" variant="outline" class="gap-1">
<ResourceIcon type="darkMatter" size="sm" />
{{ formatNumber(selectedQuest.rewards.darkMatter) }}
</Badge>
<Badge v-if="selectedQuest.rewards.points" variant="secondary" class="gap-1">
<Star class="h-3 w-3" />
+{{ formatNumber(selectedQuest.rewards.points) }} {{ t('common.points') }}
</Badge>
<Badge
v-for="(count, shipType) in selectedQuest.rewards.ships"
:key="shipType"
variant="outline"
class="gap-1"
>
<Rocket class="h-3 w-3" />
{{ count }}x {{ getShipName(shipType) }}
</Badge>
</div>
</div>
</CardContent>
</Card>
<!-- 剧情对话框 -->
<StoryDialog
v-if="showStoryDialog"
:dialogues="currentDialogues"
@close="handleDialogueClose"
@choice="handleDialogueChoice"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import { useI18n } from '@/composables/useI18n'
import { useGameStore } from '@/stores/gameStore'
import { useNPCStore } from '@/stores/npcStore'
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 { Progress } from '@/components/ui/progress'
import ResourceIcon from '@/components/common/ResourceIcon.vue'
import QuestMap from '@/components/campaign/QuestMap.vue'
import StoryDialog from '@/components/campaign/StoryDialog.vue'
import {
Scroll,
Check,
Play,
Gift,
Target,
Star,
Rocket,
Lock,
Circle,
CheckCircle2
} from 'lucide-vue-next'
import { formatNumber } from '@/utils/format'
import { MAIN_CAMPAIGN, getQuestsByChapter, getQuestById, getTotalQuestCount } from '@/config/campaignConfig'
import * as campaignLogic from '@/logic/campaignLogic'
import { QuestStatus, type CampaignQuestConfig, type StoryDialogue } from '@/types/game'
import { SHIPS } from '@/config/gameConfig'
import { toast } from 'vue-sonner'
const { t } = useI18n()
const gameStore = useGameStore()
const npcStore = useNPCStore()
// 初始化战役进度
onMounted(() => {
if (!gameStore.player.campaignProgress) {
gameStore.player.campaignProgress = campaignLogic.initializeCampaignProgress(gameStore.player)
}
})
// 响应式状态
const activeChapter = ref('1')
const selectedQuestId = ref<string | null>(null)
const showStoryDialog = ref(false)
const currentDialogues = ref<StoryDialogue[]>([])
const pendingAction = ref<'start' | 'claim' | null>(null)
// 计算属性
const chapters = computed(() => MAIN_CAMPAIGN.chapters)
const campaignProgress = computed(() => gameStore.player.campaignProgress)
const currentChapter = computed(() => campaignProgress.value?.currentChapter || 1)
const totalProgress = computed(() => {
if (!campaignProgress.value) return 0
return campaignLogic.calculateCampaignProgress(campaignProgress.value)
})
const totalQuestCount = computed(() => getTotalQuestCount())
const completedQuestCount = computed(() => campaignProgress.value?.completedQuests.length || 0)
const selectedQuest = computed(() => {
if (!selectedQuestId.value) return null
return getQuestById(selectedQuestId.value)
})
// 获取章节任务
const getChapterQuests = (chapterNumber: number): CampaignQuestConfig[] => {
return getQuestsByChapter(chapterNumber)
}
// 获取章节进度
const getChapterProgress = (chapterNumber: number): number => {
if (!campaignProgress.value) return 0
return campaignLogic.calculateChapterProgress(campaignProgress.value, chapterNumber)
}
// 获取任务状态
const getQuestStatus = (questId: string): QuestStatus => {
if (!campaignProgress.value) return QuestStatus.Locked
return campaignLogic.getQuestStatus(campaignProgress.value, questId)
}
// 获取任务状态样式
const getQuestStatusClass = (questId: string): string => {
const status = getQuestStatus(questId)
switch (status) {
case QuestStatus.Completed:
return 'bg-green-500 text-white'
case QuestStatus.Active:
return 'bg-primary text-primary-foreground'
case QuestStatus.Available:
return 'bg-blue-500 text-white'
default:
return 'bg-muted text-muted-foreground'
}
}
// 获取任务状态图标
const getQuestStatusIcon = (questId: string) => {
const status = getQuestStatus(questId)
switch (status) {
case QuestStatus.Completed:
return CheckCircle2
case QuestStatus.Active:
return Circle
case QuestStatus.Available:
return Circle
default:
return Lock
}
}
// 检查是否可以开始任务
const canStartQuest = (questId: string): boolean => {
const status = getQuestStatus(questId)
return status === QuestStatus.Available
}
// 检查是否可以领取奖励
const canClaimRewards = (questId: string): boolean => {
const status = getQuestStatus(questId)
const progress = campaignProgress.value?.questProgress[questId]
return status === QuestStatus.Completed && !progress?.rewardsClaimed
}
// 检查目标是否完成
const isObjectiveCompleted = (questId: string, objectiveId: string): boolean => {
const progress = campaignProgress.value?.questProgress[questId]
return progress?.objectives[objectiveId]?.completed || false
}
// 获取目标进度
const getObjectiveProgress = (questId: string, objectiveId: string): number => {
const progress = campaignProgress.value?.questProgress[questId]
return progress?.objectives[objectiveId]?.current || 0
}
// 获取舰船名称
const getShipName = (shipType: string): string => {
const ship = SHIPS[shipType as keyof typeof SHIPS]
return ship?.name || shipType
}
// 处理任务选择
const handleQuestSelect = (questId: string) => {
selectedQuestId.value = questId
}
// 处理开始任务
const handleStartQuest = (questId: string) => {
const quest = getQuestById(questId)
// 如果有开场对话,先显示对话
if (quest?.prologueDialogues && quest.prologueDialogues.length > 0) {
currentDialogues.value = quest.prologueDialogues
pendingAction.value = 'start'
showStoryDialog.value = true
return
}
// 直接开始任务
executeStartQuest(questId)
}
// 执行开始任务
const executeStartQuest = (questId: string) => {
const result = campaignLogic.startQuest(gameStore.player, questId)
if (result.success) {
toast.success(t('campaign.notifications.questStarted'))
// 立即检查进度
campaignLogic.checkAllActiveQuestsProgress(gameStore.player, npcStore.npcs)
} else if (result.error) {
toast.error(t(result.error))
}
}
// 处理领取奖励
const handleClaimRewards = (questId: string) => {
const quest = getQuestById(questId)
// 如果有结束对话,先显示对话
if (quest?.epilogueDialogues && quest.epilogueDialogues.length > 0) {
currentDialogues.value = quest.epilogueDialogues
pendingAction.value = 'claim'
showStoryDialog.value = true
return
}
// 直接领取奖励
executeClaimRewards(questId)
}
// 执行领取奖励
const executeClaimRewards = (questId: string) => {
const result = campaignLogic.claimQuestRewards(gameStore.player, questId)
if (result.success) {
toast.success(t('campaign.notifications.rewardsClaimed'))
} else if (result.error) {
toast.error(t(result.error))
}
}
// 处理对话关闭
const handleDialogueClose = () => {
showStoryDialog.value = false
if (pendingAction.value && selectedQuestId.value) {
if (pendingAction.value === 'start') {
executeStartQuest(selectedQuestId.value)
} else if (pendingAction.value === 'claim') {
executeClaimRewards(selectedQuestId.value)
}
}
pendingAction.value = null
currentDialogues.value = []
}
// 处理对话选项选择
const handleDialogueChoice = (choice: { effect?: string }) => {
// TODO: 处理选择效果
console.log('Dialogue choice:', choice)
}
// 监听章节变化,自动选择第一个可用任务
watch(activeChapter, (newChapter) => {
const chapterQuests = getChapterQuests(parseInt(newChapter))
const availableQuest = chapterQuests.find(quest => {
const status = getQuestStatus(quest.id)
return status === QuestStatus.Active || status === QuestStatus.Available
})
if (availableQuest) {
selectedQuestId.value = availableQuest.id
} else {
const firstQuest = chapterQuests[0]
if (firstQuest) {
selectedQuestId.value = firstQuest.id
}
}
})
// 初始选择当前任务
onMounted(() => {
if (campaignProgress.value?.currentQuestId) {
selectedQuestId.value = campaignProgress.value.currentQuestId
const quest = getQuestById(campaignProgress.value.currentQuestId)
if (quest) {
activeChapter.value = quest.chapter.toString()
}
}
})
</script>

View File

@@ -127,9 +127,19 @@
</div>
</div>
<Button @click="handleBuild(defenseType)" :disabled="!canBuild(defenseType)" class="w-full">
<Button @click="handleBuild(defenseType, $event)" :disabled="!canBuild(defenseType)" class="w-full">
{{ t('defenseView.build') }}
</Button>
<!-- 添加到等待队列按钮 -->
<Button
v-if="canAddToWaitingQueue(defenseType)"
@click="handleAddToWaiting(defenseType, $event)"
variant="outline"
class="w-full"
>
{{ t('queue.addToWaiting') }}
</Button>
</div>
</CardContent>
</Card>
@@ -164,7 +174,7 @@
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge'
import ResourceIcon from '@/components/ResourceIcon.vue'
import ResourceIcon from '@/components/common/ResourceIcon.vue'
import {
AlertDialog,
AlertDialogAction,
@@ -174,13 +184,16 @@
AlertDialogHeader,
AlertDialogTitle
} from '@/components/ui/alert-dialog'
import UnlockRequirement from '@/components/UnlockRequirement.vue'
import CardUnlockOverlay from '@/components/CardUnlockOverlay.vue'
import UnlockRequirement from '@/components/common/UnlockRequirement.vue'
import CardUnlockOverlay from '@/components/common/CardUnlockOverlay.vue'
import { formatNumber, getResourceCostColor } from '@/utils/format'
import * as publicLogic from '@/logic/publicLogic'
import * as shipValidation from '@/logic/shipValidation'
import * as shipLogic from '@/logic/shipLogic'
import * as gameLogic from '@/logic/gameLogic'
import * as waitingQueueLogic from '@/logic/waitingQueueLogic'
import * as officerLogic from '@/logic/officerLogic'
import { triggerQueueAnimation } from '@/composables/useQueueAnimation'
const gameStore = useGameStore()
const detailDialog = useDetailDialogStore()
@@ -248,7 +261,7 @@
}
// 建造防御设施
const handleBuild = (defenseType: DefenseType) => {
const handleBuild = (defenseType: DefenseType, event: MouseEvent) => {
const quantity = quantities.value[defenseType]
if (quantity <= 0) {
alertDialogTitle.value = t('defenseView.inputError')
@@ -263,6 +276,8 @@
alertDialogMessage.value = result.reason ? t(result.reason) : t('defenseView.buildFailedMessage')
alertDialogOpen.value = true
} else {
// 触发抛物线动画
triggerQueueAnimation(event, 'defense')
quantities.value[defenseType] = 0
}
}
@@ -308,4 +323,59 @@
darkMatter: config.cost.darkMatter * quantity
}
}
// 检查是否可以添加到等待队列
const canAddToWaitingQueue = (defenseType: DefenseType): boolean => {
if (!planet.value) return false
const quantity = quantities.value[defenseType]
if (quantity <= 0) return false
// 护盾罩只能建造一个
if (isShieldDome(defenseType)) {
if (planet.value.defense[defenseType] > 0) return false
if (quantity > 1) return false
}
// 检查前置条件是否满足
const config = DEFENSES.value[defenseType]
if (!publicLogic.checkRequirements(planet.value, gameStore.player.technologies, config.requirements)) {
return false
}
// 检查等待队列是否已满
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
const maxWaitingQueue = waitingQueueLogic.getMaxBuildWaitingQueue(planet.value, bonuses.additionalBuildQueue)
const waitingQueue = planet.value.waitingBuildQueue || []
if (waitingQueue.length >= maxWaitingQueue) {
return false
}
// 只有当建造按钮被禁用时(资源不足)才显示等待队列按钮
return !canBuild(defenseType)
}
// 添加到等待队列
const handleAddToWaiting = (defenseType: DefenseType, event: MouseEvent) => {
if (!planet.value) return
const quantity = quantities.value[defenseType]
if (quantity <= 0) return
const item = waitingQueueLogic.createDefenseWaitingItem(defenseType, quantity, planet.value.id)
const result = waitingQueueLogic.canAddToBuildWaitingQueue(planet.value, item, gameStore.player.officers)
if (!result.canAdd) {
alertDialogTitle.value = t('queue.waitingQueueFull')
alertDialogMessage.value = result.reason ? t(result.reason) : ''
alertDialogOpen.value = true
return
}
// 触发抛物线动画
triggerQueueAnimation(event, 'defense')
waitingQueueLogic.addToBuildWaitingQueue(planet.value, item)
quantities.value[defenseType] = 0
}
</script>

View File

@@ -6,7 +6,7 @@
<p class="text-sm text-muted-foreground mt-1">{{ t('diplomacy.description') }}</p>
</div>
<!-- 视图切换和诊断按钮 -->
<div class="flex items-center gap-2 flex-shrink-0">
<div class="flex items-center gap-2 shrink-0">
<!-- 视图模式切换 -->
<div class="flex items-center border rounded-md">
<Button
@@ -79,6 +79,12 @@
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.difficulty') }}:</span>
<span class="font-medium">{{ t(`diplomacy.diagnostic.difficultyLevels.${diagnostic.difficulty}`) }}</span>
</div>
<div class="flex items-center gap-2">
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.aiType') }}:</span>
<span class="font-medium" :title="diagnostic.aiType ? t(`diplomacy.diagnostic.aiTypeDescriptions.${diagnostic.aiType}`) : ''">
{{ diagnostic.aiType ? t(`diplomacy.diagnostic.aiTypes.${diagnostic.aiType}`) : '-' }}
</span>
</div>
<div class="flex items-center gap-2">
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.reputation') }}:</span>
<span class="font-medium">{{ diagnostic.reputation }}</span>
@@ -381,8 +387,8 @@
PaginationPrevious
} from '@/components/ui/pagination'
import { Input } from '@/components/ui/input'
import NpcRelationCard from '@/components/NpcRelationCard.vue'
import NpcRelationRow from '@/components/NpcRelationRow.vue'
import NpcRelationCard from '@/components/npc/NpcRelationCard.vue'
import NpcRelationRow from '@/components/npc/NpcRelationRow.vue'
import { RelationStatus } from '@/types/game'
import type { DiplomaticRelation } from '@/types/game'
import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic'

View File

@@ -7,7 +7,7 @@
<!-- 标签切换 -->
<Tabs v-model="activeTab" class="w-full">
<TabsList :class="['grid', 'w-full', showJumpGateTab ? 'grid-cols-4' : 'grid-cols-3']">
<TabsList :class="['grid', 'w-full', showJumpGateTab ? 'grid-cols-3' : 'grid-cols-2']">
<TabsTrigger v-for="tab in visibleTabs" :key="tab.value" :value="tab.value">
{{ t(`fleetView.${tab.labelKey}`) }}
<Badge v-if="tab.value === 'missions' && gameStore.player.fleetMissions.length > 0" variant="destructive" class="ml-1">
@@ -17,37 +17,6 @@
</TabsTrigger>
</TabsList>
<!-- 舰队总览 -->
<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>
</CardContent>
</Card>
</TabsContent>
<!-- 派遣舰队 -->
<TabsContent value="send" class="mt-4 space-y-4">
<!-- 舰队任务槽位信息 -->
@@ -213,6 +182,47 @@
</CardContent>
</Card>
<!-- 探险区域选择仅探险任务 -->
<Card v-if="selectedMission === MissionType.Expedition">
<CardHeader>
<CardTitle>{{ t('fleetView.expeditionZone') }}</CardTitle>
<CardDescription>{{ t('fleetView.expeditionZoneDesc') }}</CardDescription>
</CardHeader>
<CardContent>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<Button
v-for="item in availableExpeditionZones"
:key="item.zone"
@click="item.unlocked && (selectedExpeditionZone = item.zone)"
variant="outline"
:disabled="!item.unlocked"
:class="[
'h-auto py-3 flex flex-col items-start text-left',
selectedExpeditionZone === item.zone ? 'ring-2 ring-primary' : ''
]"
>
<div class="flex items-center gap-2 w-full">
<span class="font-medium">{{ t(`fleetView.zones.${item.zone}.name`) }}</span>
<Badge v-if="!item.unlocked" variant="secondary" class="ml-auto text-xs">
{{ t('fleetView.requiresAstro', { level: item.config.requiredTechLevel }) }}
</Badge>
</div>
<div class="text-xs text-muted-foreground mt-1">
{{ t(`fleetView.zones.${item.zone}.desc`) }}
</div>
<div class="flex gap-3 mt-2 text-xs">
<span :class="item.config.resourceMultiplier > 1 ? 'text-green-500' : ''">
{{ t('fleetView.reward') }}: x{{ item.config.resourceMultiplier }}
</span>
<span :class="item.config.dangerMultiplier > 1 ? 'text-red-500' : 'text-green-500'">
{{ t('fleetView.danger') }}: x{{ item.config.dangerMultiplier }}
</span>
</div>
</Button>
</div>
</CardContent>
</Card>
<!-- 运输资源仅运输任务 -->
<Card v-if="selectedMission === MissionType.Transport">
<CardHeader>
@@ -298,7 +308,12 @@
<CardHeader>
<div class="flex justify-between items-start">
<div>
<CardTitle class="text-base sm:text-lg">{{ getMissionName(mission.missionType) }}</CardTitle>
<CardTitle class="text-base sm:text-lg flex items-center gap-2">
{{ getMissionName(mission.missionType) }}
<Badge v-if="mission.missionType === MissionType.Expedition && mission.expeditionZone" variant="outline" class="text-xs">
{{ t(`fleetView.zones.${mission.expeditionZone}.name`) }}
</Badge>
</CardTitle>
<CardDescription class="text-xs sm:text-sm">
{{ getPlanetName(mission.originPlanetId) }} [{{ mission.targetPosition.galaxy }}:{{ mission.targetPosition.system }}:{{
mission.targetPosition.position
@@ -510,7 +525,12 @@
</div>
<AlertDialogFooter>
<AlertDialogCancel
@click="() => { showPresetNameDialog = false; pendingPresetAction = null }"
@click="
() => {
showPresetNameDialog = false
pendingPresetAction = null
}
"
>
{{ t('common.cancel') }}
</AlertDialogCancel>
@@ -531,8 +551,9 @@
import { useGameConfig } from '@/composables/useGameConfig'
import { computed, ref, onMounted, onUnmounted, watch } from 'vue'
import { useRoute } from 'vue-router'
import { ShipType, MissionType, BuildingType, TechnologyType } from '@/types/game'
import { ShipType, MissionType, BuildingType, TechnologyType, ExpeditionZone } from '@/types/game'
import type { Fleet, Resources, FleetPreset } from '@/types/game'
import { EXPEDITION_ZONES } from '@/config/gameConfig'
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'
@@ -541,7 +562,7 @@
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 ResourceIcon from '@/components/common/ResourceIcon.vue'
import {
AlertDialog,
AlertDialogAction,
@@ -552,7 +573,7 @@
AlertDialogHeader,
AlertDialogTitle
} from '@/components/ui/alert-dialog'
import UnlockRequirement from '@/components/UnlockRequirement.vue'
import UnlockRequirement from '@/components/common/UnlockRequirement.vue'
import { Empty, EmptyContent, EmptyDescription } from '@/components/ui/empty'
import {
Sword,
@@ -610,11 +631,10 @@
return publicLogic.getMaxFleetMissions(bonuses.additionalFleetSlots, computerTechLevel)
})
const activeTab = ref<'fleet' | 'send' | 'missions' | 'jumpGate'>('fleet')
const activeTab = ref<'send' | 'missions' | 'jumpGate'>('send')
// Tab 配置
const fleetTabs = [
{ value: 'fleet', labelKey: 'fleetOverview' },
{ value: 'send', labelKey: 'sendFleet' },
{ value: 'missions', labelKey: 'flightMissions' },
{ value: 'jumpGate', labelKey: 'jumpGate' }
@@ -773,6 +793,23 @@
// 选择的任务类型
const selectedMission = ref<MissionType>(MissionType.Attack)
// 探险区域选择
const selectedExpeditionZone = ref<ExpeditionZone>(ExpeditionZone.NearSpace)
// 获取玩家的天体物理学等级
const astrophysicsLevel = computed(() => {
return gameStore.player.technologies[TechnologyType.Astrophysics] || 0
})
// 可用的探险区域(基于天体物理学等级)
const availableExpeditionZones = computed(() => {
return Object.values(ExpeditionZone).map(zone => ({
zone,
config: EXPEDITION_ZONES[zone],
unlocked: astrophysicsLevel.value >= EXPEDITION_ZONES[zone].requiredTechLevel
}))
})
// 运输资源
const cargo = ref({ metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 })
@@ -1127,7 +1164,15 @@
const distance = fleetLogic.calculateDistance(planet.value.position, targetPosition.value)
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
const minSpeed = shipLogic.calculateFleetMinSpeed(selectedFleet.value, bonuses.fleetSpeedBonus)
return fleetLogic.calculateFlightTime(distance, minSpeed)
let flightTime = fleetLogic.calculateFlightTime(distance, minSpeed)
// 探险任务应用区域飞行时间倍率
if (selectedMission.value === MissionType.Expedition) {
const zoneConfig = EXPEDITION_ZONES[selectedExpeditionZone.value]
flightTime = Math.floor(flightTime * zoneConfig.flightTimeMultiplier)
}
return flightTime
}
// 检查是否可以派遣
@@ -1219,7 +1264,14 @@
const distance = fleetLogic.calculateDistance(gameStore.currentPlanet.position, targetPosition)
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
const minSpeed = shipLogic.calculateFleetMinSpeed(fleet, bonuses.fleetSpeedBonus)
const flightTime = fleetLogic.calculateFlightTime(distance, minSpeed)
let flightTime = fleetLogic.calculateFlightTime(distance, minSpeed)
// 探险任务应用区域飞行时间倍率
if (missionType === MissionType.Expedition) {
const zoneConfig = EXPEDITION_ZONES[selectedExpeditionZone.value]
flightTime = Math.floor(flightTime * zoneConfig.flightTimeMultiplier)
}
const mission = fleetLogic.createFleetMission(
gameStore.player.id,
gameStore.currentPlanet.id,
@@ -1241,6 +1293,11 @@
mission.giftTargetNpcId = targetNpc.value.id
}
// 如果是探险任务,设置探险区域
if (missionType === MissionType.Expedition) {
mission.expeditionZone = selectedExpeditionZone.value
}
gameStore.player.fleetMissions.push(mission)
return true
}

View File

@@ -86,7 +86,7 @@
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" />
<Globe class="h-4 w-4 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>
@@ -134,7 +134,7 @@
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" />
<Globe class="h-4 w-4 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">
@@ -197,7 +197,7 @@
<!-- 第一行:位置编号 + 星球信息(名称、坐标、状态、残骸) -->
<div class="flex items-start gap-2 w-full">
<!-- 位置编号 -->
<div class="w-8 text-center flex-shrink-0">
<div class="w-8 text-center shrink-0">
<Badge variant="outline" class="text-xs">{{ slot.position }}</Badge>
</div>
<!-- 星球信息 -->
@@ -208,15 +208,15 @@
<h3 class="font-semibold text-sm truncate">
{{ isMyPlanet(slot.planet) ? slot.planet.name : getNpcPlanetDisplayName(slot.planet) }}
</h3>
<span class="text-xs text-muted-foreground whitespace-nowrap flex-shrink-0">
<span class="text-xs text-muted-foreground whitespace-nowrap shrink-0">
[{{ slot.planet.position.galaxy }}:{{ slot.planet.position.system }}:{{ slot.planet.position.position }}]
</span>
<Badge v-if="isMyPlanet(slot.planet)" variant="default" class="text-xs flex-shrink-0">
<Badge v-if="isMyPlanet(slot.planet)" variant="default" class="text-xs shrink-0">
{{ t('galaxyView.mine') }}
</Badge>
<Popover v-else>
<PopoverTrigger as-child>
<Badge :variant="getRelationBadgeVariant(slot.planet)" class="text-xs flex-shrink-0 cursor-pointer">
<Badge :variant="getRelationBadgeVariant(slot.planet)" class="text-xs shrink-0 cursor-pointer">
{{ getRelationStatusText(slot.planet) }}
</Badge>
</PopoverTrigger>
@@ -233,7 +233,7 @@
<Badge
v-if="getNpcDifficultyLevel(slot.planet) !== null"
:variant="getDifficultyBadgeVariant(getNpcDifficultyLevel(slot.planet))"
class="text-xs flex-shrink-0"
class="text-xs shrink-0"
:class="getDifficultyLevelColor(getNpcDifficultyLevel(slot.planet))"
>
Lv.{{ getNpcDifficultyLevel(slot.planet) }}
@@ -269,6 +269,54 @@
</div>
</PopoverContent>
</Popover>
<!-- 矿脉储量徽章 -->
<Popover v-if="getOreDeposits(slot.planet)">
<PopoverTrigger as-child>
<Badge
variant="outline"
class="text-xs cursor-pointer hover:bg-emerald-50 dark:hover:bg-emerald-950/30 border-emerald-300 dark:border-emerald-700 text-emerald-700 dark:text-emerald-400 gap-1"
>
<Mountain class="h-3 w-3" />
</Badge>
</PopoverTrigger>
<PopoverContent class="w-auto p-3" side="top" align="center">
<div class="space-y-2">
<p class="text-xs font-semibold text-emerald-700 dark:text-emerald-400">{{ t('galaxyView.oreDeposits') }}</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"
:class="getDepositStatus(getOreDeposits(slot.planet)!, 'metal') === 'depleted' ? 'text-destructive' : getDepositStatus(getOreDeposits(slot.planet)!, 'metal') === 'warning' ? 'text-yellow-600' : ''"
>
{{ formatDepositShort(getOreDeposits(slot.planet)!.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"
:class="getDepositStatus(getOreDeposits(slot.planet)!, 'crystal') === 'depleted' ? 'text-destructive' : getDepositStatus(getOreDeposits(slot.planet)!, 'crystal') === 'warning' ? 'text-yellow-600' : ''"
>
{{ formatDepositShort(getOreDeposits(slot.planet)!.crystal) }}
</span>
</div>
<div class="flex items-center gap-2">
<ResourceIcon type="deuterium" size="sm" />
<span class="text-muted-foreground">{{ t('resources.deuterium') }}:</span>
<span
class="font-medium"
:class="getDepositStatus(getOreDeposits(slot.planet)!, 'deuterium') === 'depleted' ? 'text-destructive' : getDepositStatus(getOreDeposits(slot.planet)!, 'deuterium') === 'warning' ? 'text-yellow-600' : ''"
>
{{ formatDepositShort(getOreDeposits(slot.planet)!.deuterium) }}
</span>
</div>
</div>
</div>
</PopoverContent>
</Popover>
<!-- 月球徽章 -->
<Badge
v-if="slot.moon"
@@ -416,7 +464,7 @@
<!-- PC端布局位置编号 + 星球信息(水平) -->
<div class="hidden sm:flex items-center gap-4 flex-1 min-w-0">
<!-- 位置编号 -->
<div class="w-12 text-center flex-shrink-0">
<div class="w-12 text-center shrink-0">
<Badge variant="outline" class="text-sm">{{ slot.position }}</Badge>
</div>
@@ -488,6 +536,55 @@
</div>
</PopoverContent>
</Popover>
<!-- 矿脉储量徽章 -->
<Popover v-if="getOreDeposits(slot.planet)">
<PopoverTrigger as-child>
<Badge
variant="outline"
class="text-xs cursor-pointer hover:bg-emerald-50 dark:hover:bg-emerald-950/30 border-emerald-300 dark:border-emerald-700 text-emerald-700 dark:text-emerald-400 gap-1"
>
<Mountain class="h-3 w-3" />
<span>{{ t('galaxyView.deposits') }}</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-emerald-700 dark:text-emerald-400">{{ t('galaxyView.oreDeposits') }}</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"
:class="getDepositStatus(getOreDeposits(slot.planet)!, 'metal') === 'depleted' ? 'text-destructive' : getDepositStatus(getOreDeposits(slot.planet)!, 'metal') === 'warning' ? 'text-yellow-600' : ''"
>
{{ formatDepositShort(getOreDeposits(slot.planet)!.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"
:class="getDepositStatus(getOreDeposits(slot.planet)!, 'crystal') === 'depleted' ? 'text-destructive' : getDepositStatus(getOreDeposits(slot.planet)!, 'crystal') === 'warning' ? 'text-yellow-600' : ''"
>
{{ formatDepositShort(getOreDeposits(slot.planet)!.crystal) }}
</span>
</div>
<div class="flex items-center gap-2">
<ResourceIcon type="deuterium" size="sm" />
<span class="text-muted-foreground">{{ t('resources.deuterium') }}:</span>
<span
class="font-medium"
:class="getDepositStatus(getOreDeposits(slot.planet)!, 'deuterium') === 'depleted' ? 'text-destructive' : getDepositStatus(getOreDeposits(slot.planet)!, 'deuterium') === 'warning' ? 'text-yellow-600' : ''"
>
{{ formatDepositShort(getOreDeposits(slot.planet)!.deuterium) }}
</span>
</div>
</div>
</div>
</PopoverContent>
</Popover>
<!-- 月球徽章 -->
<Badge
v-if="slot.moon"
@@ -545,7 +642,7 @@
</div>
<!-- 操作按钮 (PC端) -->
<div class="hidden sm:flex gap-1 sm:gap-2 flex-shrink-0">
<div class="hidden sm:flex gap-1 sm:gap-2 shrink-0">
<TooltipProvider :delay-duration="300">
<Tooltip v-if="slot.planet && !isMyPlanet(slot.planet)">
<TooltipTrigger as-child>
@@ -829,14 +926,15 @@
AlertDialogHeader,
AlertDialogTitle
} from '@/components/ui/alert-dialog'
import ResourceIcon from '@/components/ResourceIcon.vue'
import { Home, Eye, Sword, Rocket, Recycle, Gift, Globe, Bomb, Moon, Radar } from 'lucide-vue-next'
import ResourceIcon from '@/components/common/ResourceIcon.vue'
import { Home, Eye, Sword, Rocket, Recycle, Gift, Globe, Bomb, Moon, Radar, Mountain } from 'lucide-vue-next'
import { useRouter, useRoute } from 'vue-router'
import * as gameLogic from '@/logic/gameLogic'
import * as moonLogic from '@/logic/moonLogic'
import * as oreDepositLogic from '@/logic/oreDepositLogic'
import { formatNumber, formatTime } from '@/utils/format'
import { BuildingType, MissionType } from '@/types/game'
import type { FleetMission } from '@/types/game'
import type { FleetMission, OreDeposits } from '@/types/game'
const gameStore = useGameStore()
const universeStore = useUniverseStore()
@@ -957,6 +1055,27 @@
return universeStore.debrisFields[debrisId] || null
}
// 获取星球的矿脉储量信息
const getOreDeposits = (planet: Planet | null): OreDeposits | null => {
if (!planet || planet.isMoon) return null
return planet.oreDeposits || null
}
// 格式化矿脉储量(短格式)
const formatDepositShort = (value: number): string => {
if (value >= 1_000_000_000) return `${(value / 1_000_000_000).toFixed(1)}B`
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`
if (value >= 1_000) return `${(value / 1_000).toFixed(0)}K`
return String(Math.floor(value))
}
// 获取矿脉储量百分比对应的颜色状态
const getDepositStatus = (deposits: OreDeposits, resourceType: 'metal' | 'crystal' | 'deuterium'): 'normal' | 'warning' | 'depleted' => {
if (oreDepositLogic.isDepositDepleted(deposits, resourceType)) return 'depleted'
if (oreDepositLogic.isDepositWarning(deposits, resourceType)) return 'warning'
return 'normal'
}
// 加载星系
const loadSystem = () => {
currentGalaxy.value = selectedGalaxy.value

View File

@@ -107,7 +107,7 @@
} from '@/components/ui/alert-dialog'
import { Checkbox } from '@/components/ui/checkbox'
import { Rocket, Languages, Shield } from 'lucide-vue-next'
import PrivacyDialog from '@/components/PrivacyDialog.vue'
import PrivacyDialog from '@/components/dialogs/PrivacyDialog.vue'
import pkg from '../../package.json'
const router = useRouter()

View File

@@ -62,14 +62,14 @@
<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" />
<Sword class="h-4 w-4 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">
<Button @click.stop="deleteBattleReport(report.id)" variant="ghost" size="icon" class="h-8 w-8 shrink-0">
<X class="h-4 w-4" />
</Button>
</div>
@@ -99,12 +99,12 @@
<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" />
<Eye class="h-4 w-4 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>
<Badge variant="outline" class="text-xs">{{ getSpyReportTargetName(report) }}</Badge>
</div>
<Button @click.stop="deleteSpyReport(report.id)" variant="ghost" size="icon" class="h-8 w-8 flex-shrink-0">
<Button @click.stop="deleteSpyReport(report.id)" variant="ghost" size="icon" class="h-8 w-8 shrink-0">
<X class="h-4 w-4" />
</Button>
</div>
@@ -124,19 +124,20 @@
<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" />
<AlertTriangle class="h-4 w-4 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">
<Button @click.stop="deleteSpiedNotification(notification.id)" variant="ghost" size="icon" class="h-8 w-8 shrink-0">
<X class="h-4 w-4" />
</Button>
</div>
<CardDescription class="text-xs sm:text-sm">
{{ notification.npcName }} {{ notification.targetPlanetName }} · {{ formatDate(notification.timestamp) }}
{{ getNpcName(notification.npcId, notification.npcName) }} {{ notification.targetPlanetName }} ·
{{ formatDate(notification.timestamp) }}
</CardDescription>
</CardHeader>
</Card>
@@ -168,21 +169,16 @@
<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" />
<Recycle class="h-4 w-4 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"
>
<Button @click.stop="deleteNPCActivityNotification(notification.id)" variant="ghost" size="icon" class="h-8 w-8 shrink-0">
<X class="h-4 w-4" />
</Button>
</div>
<CardDescription class="text-xs sm:text-sm">
{{ notification.npcName }} →
{{ getNpcName(notification.npcId, notification.npcName) }} →
{{
notification.targetPlanetName ||
`[${notification.targetPosition.galaxy}:${notification.targetPosition.system}:${notification.targetPosition.position}]`
@@ -202,11 +198,13 @@
<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>
<Gift class="h-4 w-4 shrink-0 text-green-600" />
<CardTitle class="text-base sm:text-lg">
{{ t('messagesView.giftFrom').replace('{npcName}', getNpcName(gift.fromNpcId, 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">
<Button @click.stop="deleteGiftNotification(gift.id)" variant="ghost" size="icon" class="h-8 w-8 shrink-0">
<X class="h-4 w-4" />
</Button>
</div>
@@ -252,13 +250,13 @@
<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" />
<Ban class="h-4 w-4 shrink-0 text-red-600" />
<CardTitle class="text-base sm:text-lg">
{{ t('messagesView.giftRejectedBy').replace('{npcName}', rejection.npcName) }}
{{ t('messagesView.giftRejectedBy').replace('{npcName}', getNpcName(rejection.npcId, 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">
<Button @click.stop="deleteGiftRejectedNotification(rejection.id)" variant="ghost" size="icon" class="h-8 w-8 shrink-0">
<X class="h-4 w-4" />
</Button>
</div>
@@ -306,14 +304,14 @@
<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" />
<Package class="h-4 w-4 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">
<Button @click.stop="deleteMissionReport(report.id)" variant="ghost" size="icon" class="h-8 w-8 shrink-0">
<X class="h-4 w-4" />
</Button>
</div>
@@ -339,322 +337,14 @@
<!-- 间谍报告对话框 -->
<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>
<DialogDescription>
{{ t('messagesView.spyDetected') }}
</DialogDescription>
</DialogHeader>
<!-- 被侦查通知对话框 -->
<SpiedNotificationDialog v-model:open="showSpiedDialog" :notification="selectedSpiedNotification" />
<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>
<!-- 任务报告对话框 -->
<MissionReportDialog v-model:open="showMissionDialog" :report="selectedMissionReport" />
<!-- 被侦查星球 -->
<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>
<DialogDescription>
{{ t('messagesView.missionDetails') }}
</DialogDescription>
</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 v-for="res in basicResourceFields" :key="res.key">
{{ t(`resources.${res.key}`) }}: {{ selectedMissionReport.details.transportedResources[res.key].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 v-for="res in debrisResourceFields" :key="res.key">
{{ t(`resources.${res.key}`) }}: {{ selectedMissionReport.details.recycledResources[res.key].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 v-for="res in debrisResourceFields" :key="res.key">
{{ t(`resources.${res.key}`) }}: {{ selectedMissionReport.details.remainingDebris[res.key].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 v-if="selectedMissionReport.details?.missileCount !== undefined" class="mt-3 space-y-2">
<p class="text-xs font-semibold text-muted-foreground">{{ t('galaxyView.missileAttack') }}:</p>
<div class="grid grid-cols-3 gap-2 text-sm">
<div>
<span class="text-muted-foreground">{{ t('galaxyView.missileCount') }}:</span>
<span class="ml-1 font-medium">{{ selectedMissionReport.details.missileCount }}</span>
</div>
<div>
<span class="text-muted-foreground">{{ t('missionReports.hits') }}:</span>
<span class="ml-1 font-medium text-green-600">{{ selectedMissionReport.details.missileHits }}</span>
</div>
<div>
<span class="text-muted-foreground">{{ t('galaxyView.intercepted') }}:</span>
<span class="ml-1 font-medium text-yellow-600">{{ selectedMissionReport.details.missileIntercepted }}</span>
</div>
</div>
<div v-if="Object.keys(selectedMissionReport.details.defenseLosses || {}).length > 0" class="mt-2">
<p class="text-xs font-semibold text-muted-foreground">{{ t('galaxyView.defenseLosses') }}:</p>
<div class="grid grid-cols-2 gap-1 text-xs mt-1 p-2 bg-red-50 dark:bg-red-950/30 rounded">
<div v-for="(count, defenseType) in selectedMissionReport.details.defenseLosses" :key="defenseType">
<span class="text-muted-foreground">{{ t('defenses.' + defenseType) }}:</span>
<span class="ml-1 font-medium text-red-600 dark:text-red-400">-{{ count }}</span>
</div>
</div>
</div>
</div>
<!-- 探险任务详情 - 发现资源 -->
<div v-if="selectedMissionReport.details?.foundResources" class="mt-3 space-y-1">
<p class="text-xs font-semibold text-muted-foreground">{{ t('messagesView.resources') }}:</p>
<div class="grid grid-cols-2 gap-2 text-sm p-2 bg-green-50 dark:bg-green-950/30 rounded">
<div v-for="res in allResourceFields" :key="res.key">
<template v-if="(selectedMissionReport.details?.foundResources?.[res.key] ?? 0) > 0">
<span class="text-muted-foreground">{{ t(`resources.${res.key}`) }}:</span>
<span class="ml-1 font-medium text-green-600 dark:text-green-400">
+{{ (selectedMissionReport.details?.foundResources?.[res.key] ?? 0).toLocaleString() }}
</span>
</template>
</div>
</div>
</div>
<!-- 探险任务详情 - 发现舰船 -->
<div v-if="selectedMissionReport.details?.foundFleet" class="mt-3 space-y-1">
<p class="text-xs font-semibold text-muted-foreground">{{ t('messagesView.fleet') }}:</p>
<div class="grid grid-cols-2 gap-2 text-sm p-2 bg-blue-50 dark:bg-blue-950/30 rounded">
<div v-for="(count, shipType) in selectedMissionReport.details.foundFleet" :key="shipType">
<span class="text-muted-foreground">{{ t('ships.' + shipType) }}:</span>
<span class="ml-1 font-medium text-blue-600 dark:text-blue-400">+{{ count }}</span>
</div>
</div>
</div>
<!-- 探险任务详情 - 损失舰船 -->
<div v-if="selectedMissionReport.details?.fleetLost" class="mt-3 space-y-1">
<p class="text-xs font-semibold text-muted-foreground">{{ t('messagesView.attackerLosses') }}:</p>
<div class="grid grid-cols-2 gap-2 text-sm p-2 bg-red-50 dark:bg-red-950/30 rounded">
<div v-for="(count, shipType) in selectedMissionReport.details.fleetLost" :key="shipType">
<span class="text-muted-foreground">{{ t('ships.' + shipType) }}:</span>
<span class="ml-1 font-medium text-red-600 dark:text-red-400">-{{ count }}</span>
</div>
</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>
<DialogDescription>
{{ t('messagesView.activityDescription') }}
</DialogDescription>
</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>
<!-- NPC活动通知对话框 -->
<NPCActivityDialog v-model:open="showNPCActivityDialog" :notification="selectedNPCActivityNotification" />
</div>
</template>
@@ -662,19 +352,20 @@
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, DialogDescription, DialogFooter } from '@/components/ui/dialog'
import { FixedPagination } from '@/components/ui/pagination'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Checkbox } from '@/components/ui/checkbox'
import BattleReportDialog from '@/components/BattleReportDialog.vue'
import SpyReportDialog from '@/components/SpyReportDialog.vue'
import BattleReportDialog from '@/components/dialogs/BattleReportDialog.vue'
import SpyReportDialog from '@/components/dialogs/SpyReportDialog.vue'
import SpiedNotificationDialog from '@/components/dialogs/SpiedNotificationDialog.vue'
import MissionReportDialog from '@/components/dialogs/MissionReportDialog.vue'
import NPCActivityDialog from '@/components/dialogs/NPCActivityDialog.vue'
import { formatDate } from '@/utils/format'
import { X, Sword, Eye, AlertTriangle, Package, Recycle, Gift, Ban, Check, Users, Skull, Globe, Compass, Trash2 } from 'lucide-vue-next'
import { X, Sword, Eye, AlertTriangle, Package, Recycle, Gift, Ban, Check, Users, Trash2 } from 'lucide-vue-next'
import { Empty, EmptyContent, EmptyDescription } from '@/components/ui/empty'
import type {
BattleResult,
@@ -690,7 +381,6 @@
import * as diplomaticLogic from '@/logic/diplomaticLogic'
import { toast } from 'vue-sonner'
const router = useRouter()
const gameStore = useGameStore()
const npcStore = useNPCStore()
const { t } = useI18n()
@@ -739,13 +429,45 @@
type BasicResourceKey = 'metal' | 'crystal' | 'deuterium'
const basicResourceFields: { key: BasicResourceKey }[] = [{ key: 'metal' }, { key: 'crystal' }, { key: 'deuterium' }]
// 残骸资源字段配置(只有金属和晶体)
type DebrisResourceKey = 'metal' | 'crystal'
const debrisResourceFields: { key: DebrisResourceKey }[] = [{ key: 'metal' }, { key: 'crystal' }]
/**
* 获取NPC当前名称
* 优先使用当前NPC的实际名称如果NPC不存在则使用通知中保存的旧名称
* 支持通过ID查找也支持通过旧名称中的ID模式匹配
*/
const getNpcName = (npcId: string | undefined, fallbackName: string): string => {
if (!npcStore.npcs?.length) return fallbackName
// 全部资源字段配置(包含暗物质,用于探险任务)
type AllResourceKey = 'metal' | 'crystal' | 'deuterium' | 'darkMatter'
const allResourceFields: { key: AllResourceKey }[] = [{ key: 'metal' }, { key: 'crystal' }, { key: 'deuterium' }, { key: 'darkMatter' }]
// 1. 先通过 npcId 查找
if (npcId) {
const npc = npcStore.npcs.find(n => n.id === npcId)
if (npc) return npc.name
}
// 2. 尝试从旧名称中提取ID并查找
// 旧格式如 "NPC-npc_182"新ID格式为 "npc_182"
const idMatch = fallbackName.match(/npc_\d+/)
if (idMatch) {
const extractedId = idMatch[0]
const npc = npcStore.npcs.find(n => n.id === extractedId)
if (npc) return npc.name
}
return fallbackName
}
/**
* 获取侦查报告的目标名称
* 显示 NPC 名称(如果是 NPC 星球)或星球名称
*/
const getSpyReportTargetName = (report: SpyReport): string => {
// 尝试通过 targetPlayerId 获取 NPC 名称
if (report.targetPlayerId && report.targetPlayerId !== 'unknown') {
const npc = npcStore.npcs.find(n => n.id === report.targetPlayerId)
if (npc) return npc.name
}
// 回退到星球名称
return report.targetPlanetName || report.targetPlanetId
}
const hasSelectedAny = computed(() => {
return Object.values(clearOptions.value).some(v => v)
@@ -1237,58 +959,4 @@
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.Expedition:
return Compass
case MissionType.Destroy:
return Skull
default:
return Package
}
}
</script>

View File

@@ -162,7 +162,7 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import ResourceIcon from '@/components/ResourceIcon.vue'
import ResourceIcon from '@/components/common/ResourceIcon.vue'
import {
AlertDialog,
AlertDialogAction,

View File

@@ -9,6 +9,10 @@
<p class="text-xs sm:text-sm text-muted-foreground">
{{ t('planet.position') }}: [{{ planet.position.galaxy }}:{{ planet.position.system }}:{{ planet.position.position }}]
</p>
<!-- 温度信息 -->
<p v-if="planet.temperature && !planet.isMoon" class="text-xs sm:text-sm text-muted-foreground">
{{ t('planet.temperature') }}: {{ planet.temperature.min }}°C {{ t('common.to') }} {{ planet.temperature.max }}°C
</p>
<!-- 月球信息 -->
<div v-if="!planet.isMoon && moon" class="mt-2">
<Button @click="switchToMoon" variant="outline" size="sm">
@@ -28,11 +32,10 @@
<CardContent>
<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>
<TabsTrigger value="overview">{{ t('overview.tabOverview') }}</TabsTrigger>
<TabsTrigger value="production">{{ t('overview.tabProduction') }}</TabsTrigger>
<TabsTrigger value="consumption">{{ t('overview.tabConsumption') }}</TabsTrigger>
</TabsList>
<!-- 概览标签页 -->
<TabsContent value="overview" class="mt-4">
<Table>
@@ -177,7 +180,7 @@
<CardDescription>{{ t('overview.currentShips') }}</CardDescription>
</CardHeader>
<CardContent>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3 sm:gap-4">
<div class="grid grid-cols-3 sm:grid-cols-4 gap-3 sm:gap-4">
<div v-for="(count, shipType) in planet.fleet" :key="shipType">
<p class="text-xs sm:text-sm text-muted-foreground">{{ SHIPS[shipType].name }}</p>
<p class="text-lg sm:text-xl font-bold">{{ count }}</p>
@@ -198,7 +201,7 @@
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import ResourceIcon from '@/components/ResourceIcon.vue'
import ResourceIcon from '@/components/common/ResourceIcon.vue'
import { formatNumber, getResourceColor } from '@/utils/format'
import { scaleNumber } from '@/utils/speed'
import type { Planet } from '@/types/game'
@@ -242,7 +245,23 @@
]
// 消耗类型配置
const consumptionTypes = [{ key: 'metalMine' as const }, { key: 'crystalMine' as const }, { key: 'deuteriumSynthesizer' as const }]
const consumptionTypes = [
// 资源建筑
{ key: 'metalMine' as const },
{ key: 'crystalMine' as const },
{ key: 'deuteriumSynthesizer' as const },
// 设施建筑
{ key: 'roboticsFactory' as const },
{ key: 'naniteFactory' as const },
{ key: 'shipyard' as const },
{ key: 'researchLab' as const },
{ key: 'missileSilo' as const },
{ key: 'terraformer' as const },
{ key: 'darkMatterCollector' as const },
// 月球建筑
{ key: 'sensorPhalanx' as const },
{ key: 'jumpGate' as const }
]
// 月球相关
const moon = computed(() => {

187
src/views/RankingView.vue Normal file
View File

@@ -0,0 +1,187 @@
<template>
<div class="container mx-auto p-4 sm:p-6 space-y-4 sm:space-y-6">
<div class="flex flex-row items-center justify-between gap-4">
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('ranking.title') }}</h1>
<div class="flex items-center gap-2 text-sm text-muted-foreground">
<Users class="h-4 w-4" />
{{ t('ranking.totalPlayers', { count: rankingData.length }) }}
</div>
</div>
<!-- 玩家排名概览 -->
<div class="flex items-center gap-4 p-3 rounded-lg bg-gradient-to-r from-primary/5 to-primary/10">
<Crown class="h-5 w-5 text-yellow-500 shrink-0" />
<span class="text-sm text-muted-foreground">{{ t('ranking.yourRanking') }}</span>
<span class="text-xl font-bold">#{{ playerRank }}</span>
<span class="text-sm text-muted-foreground">/ {{ rankingData.length }}</span>
<span class="ml-auto text-lg font-bold text-primary">{{ formatNumber(playerScore) }}</span>
</div>
<!-- 分类标签 -->
<Tabs v-model="activeCategory" class="w-full">
<TabsList class="w-full grid grid-cols-5 h-10">
<TabsTrigger v-for="category in categories" :key="category.value" :value="category.value" class="text-xs sm:text-sm">
<component :is="getCategoryIcon(category.value)" class="h-4 w-4 mr-1 hidden sm:inline" />
{{ t(`ranking.categories.${category.value}`) }}
</TabsTrigger>
</TabsList>
<!-- 排行榜列表 -->
<TabsContent v-for="category in categories" :key="category.value" :value="category.value" class="mt-4">
<Table>
<TableHeader>
<TableRow>
<TableHead class="w-16 text-center">#</TableHead>
<TableHead>{{ t('ranking.name') }}</TableHead>
<TableHead>{{ t(`ranking.categories.${activeCategory}`) }}</TableHead>
<TableHead>{{ t('ranking.planets') }}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="(entry, index) in paginatedRanking" :key="entry.id" :class="{ 'bg-primary/5': entry.isPlayer }">
<TableCell class="text-center">
<div
class="w-7 h-7 sm:w-8 sm:h-8 rounded-full flex items-center justify-center text-xs sm:text-sm font-bold mx-auto"
:class="getRankBadgeClass(getActualRank(index))"
>
{{ getActualRank(index) }}
</div>
</TableCell>
<TableCell>
<div class="flex items-center gap-1">
<span class="font-medium truncate" :class="{ 'text-primary': entry.isPlayer }">
{{ entry.name }}
</span>
<Badge v-if="entry.isPlayer" variant="outline" class="text-[10px] px-1 shrink-0">
{{ t('ranking.you') }}
</Badge>
</div>
</TableCell>
<TableCell class="font-mono">
{{ formatNumber(entry.scores[activeCategory]) }}
</TableCell>
<TableCell class="table-cell">
<div class="flex items-center gap-1 text-muted-foreground">
<Globe class="h-4 w-4" />
{{ entry.planetCount }}
</div>
</TableCell>
</TableRow>
<TableRow v-if="rankingData.length === 0">
<TableCell class="text-center text-muted-foreground py-8">
{{ t('ranking.noData') }}
</TableCell>
</TableRow>
</TableBody>
</Table>
</TabsContent>
</Tabs>
<!-- 分页 -->
<FixedPagination v-model:page="currentPage" :total-pages="totalPages" />
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { useGameStore } from '@/stores/gameStore'
import { useNPCStore } from '@/stores/npcStore'
import { useI18n } from '@/composables/useI18n'
import { formatNumber } from '@/utils/format'
import { Badge } from '@/components/ui/badge'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { FixedPagination } from '@/components/ui/pagination'
import { RankingCategory, type RankingEntry } from '@/types/game'
import { getRanking } from '@/logic/rankingLogic'
import { Crown, Users, Globe, Trophy, Building2, FlaskConical, Rocket, Shield } from 'lucide-vue-next'
const { t } = useI18n()
const gameStore = useGameStore()
const npcStore = useNPCStore()
const activeCategory = ref<RankingCategory>(RankingCategory.Total)
// 分页
const ITEMS_PER_PAGE = 10
const currentPage = ref(1)
const categories = [
{ value: RankingCategory.Total },
{ value: RankingCategory.Building },
{ value: RankingCategory.Research },
{ value: RankingCategory.Fleet },
{ value: RankingCategory.Defense }
]
// 获取排行榜数据
const rankingData = computed<RankingEntry[]>(() => {
return getRanking(gameStore.player, npcStore.npcs, activeCategory.value)
})
// 按当前类别排序的排行榜
const sortedRanking = computed(() => {
return [...rankingData.value].sort((a, b) => b.scores[activeCategory.value] - a.scores[activeCategory.value])
})
// 总页数
const totalPages = computed(() => Math.ceil(sortedRanking.value.length / ITEMS_PER_PAGE))
// 分页后的排行榜
const paginatedRanking = computed(() => {
const start = (currentPage.value - 1) * ITEMS_PER_PAGE
const end = start + ITEMS_PER_PAGE
return sortedRanking.value.slice(start, end)
})
// 获取实际排名(考虑分页偏移)
const getActualRank = (index: number): number => {
return (currentPage.value - 1) * ITEMS_PER_PAGE + index + 1
}
// 切换分类时重置页码
watch(activeCategory, () => {
currentPage.value = 1
})
// 玩家当前排名
const playerRank = computed(() => {
const index = sortedRanking.value.findIndex(entry => entry.isPlayer)
return index >= 0 ? index + 1 : '-'
})
// 玩家当前积分
const playerScore = computed(() => {
const playerEntry = rankingData.value.find(entry => entry.isPlayer)
return playerEntry?.scores[activeCategory.value] || 0
})
// 获取类别图标
const getCategoryIcon = (category: RankingCategory) => {
switch (category) {
case RankingCategory.Total:
return Trophy
case RankingCategory.Building:
return Building2
case RankingCategory.Research:
return FlaskConical
case RankingCategory.Fleet:
return Rocket
case RankingCategory.Defense:
return Shield
}
}
// 获取排名徽章样式
const getRankBadgeClass = (rank: number) => {
switch (rank) {
case 1:
return 'bg-yellow-500 text-yellow-950'
case 2:
return 'bg-gray-400 text-gray-900'
case 3:
return 'bg-amber-600 text-amber-100'
default:
return 'bg-muted text-muted-foreground'
}
}
</script>

View File

@@ -50,11 +50,21 @@
</span>
</div>
</div>
<!-- 研究时间 -->
<div class="flex items-center gap-1.5 sm:gap-2">
<Clock class="h-3.5 w-3.5 sm:h-4 sm:w-4 text-muted-foreground" />
<span class="font-medium text-xs sm:text-sm text-muted-foreground">{{ formatTime(getResearchTime(techType)) }}</span>
</div>
</div>
<Button @click="handleResearch(techType)" :disabled="!canResearch(techType)" class="w-full">
<Button @click="handleResearch(techType, $event)" :disabled="!canResearch(techType)" class="w-full">
{{ getResearchButtonText(techType) }}
</Button>
<!-- 添加到等待队列按钮 -->
<Button v-if="canAddToWaitingQueue(techType)" @click="handleAddToWaiting(techType, $event)" variant="outline" class="w-full">
{{ t('queue.addToWaiting') }}
</Button>
</div>
</CardContent>
</Card>
@@ -71,8 +81,8 @@
<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" />
<Check v-if="req.met" :size="16" class="text-green-500 shrink-0" />
<X v-else :size="16" class="text-red-500 shrink-0" />
<span>{{ req.name }}: Lv {{ req.requiredLevel }} ({{ t('common.current') }}: Lv {{ req.currentLevel }})</span>
</div>
</div>
@@ -97,7 +107,7 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import ResourceIcon from '@/components/ResourceIcon.vue'
import ResourceIcon from '@/components/common/ResourceIcon.vue'
import {
AlertDialog,
AlertDialogAction,
@@ -107,14 +117,17 @@
AlertDialogHeader,
AlertDialogTitle
} from '@/components/ui/alert-dialog'
import UnlockRequirement from '@/components/UnlockRequirement.vue'
import CardUnlockOverlay from '@/components/CardUnlockOverlay.vue'
import { Check, X } from 'lucide-vue-next'
import { formatNumber, getResourceCostColor } from '@/utils/format'
import UnlockRequirement from '@/components/common/UnlockRequirement.vue'
import CardUnlockOverlay from '@/components/common/CardUnlockOverlay.vue'
import { Check, X, Clock } from 'lucide-vue-next'
import { formatNumber, formatTime, getResourceCostColor } from '@/utils/format'
import * as publicLogic from '@/logic/publicLogic'
import * as researchLogic from '@/logic/researchLogic'
import * as researchValidation from '@/logic/researchValidation'
import * as gameLogic from '@/logic/gameLogic'
import * as officerLogic from '@/logic/officerLogic'
import * as waitingQueueLogic from '@/logic/waitingQueueLogic'
import { triggerQueueAnimation } from '@/composables/useQueueAnimation'
const gameStore = useGameStore()
const detailDialog = useDetailDialogStore()
@@ -238,7 +251,7 @@
}
// 研究科技
const handleResearch = (techType: TechnologyType) => {
const handleResearch = (techType: TechnologyType, event: MouseEvent) => {
// 检查前置条件
if (!checkUpgradeRequirements(techType)) {
alertDialogTitle.value = t('common.requirementsNotMet')
@@ -255,6 +268,9 @@
alertDialogMessage.value = t('researchView.researchFailedMessage')
alertDialogShowRequirements.value = false
alertDialogOpen.value = true
} else {
// 触发抛物线动画
triggerQueueAnimation(event, 'technology')
}
}
@@ -301,4 +317,88 @@
const getTechnologyCost = (techType: TechnologyType, targetLevel: number): Resources => {
return researchLogic.calculateTechnologyCost(techType, targetLevel)
}
// 获取研究时间(秒)
const getResearchTime = (techType: TechnologyType): number => {
if (!planet.value) return 0
const currentLevel = getTechLevel(techType)
const researchLabLevel = planet.value.buildings['researchLab'] || 0
const energyTechLevel = player.value.technologies['energyTechnology'] || 0
const bonuses = officerLogic.calculateActiveBonuses(player.value.officers, gameStore.gameTime)
return researchLogic.calculateTechnologyTime(techType, currentLevel, bonuses.researchSpeedBonus, researchLabLevel, energyTechLevel)
}
// 检查是否可以添加到等待队列
const canAddToWaitingQueue = (techType: TechnologyType): boolean => {
if (!planet.value) return false
const config = TECHNOLOGIES.value[techType]
const currentLevel = getTechLevel(techType)
// 计算目标等级:当前等级 + 正式队列中的升级数 + 等待队列中的升级数 + 1
const upgradesInResearchQueue = player.value.researchQueue.filter(q => q.type === 'technology' && q.itemType === techType).length
const waitingQueue = player.value.waitingResearchQueue || []
const upgradesInWaitingQueue = waitingQueue.filter(q => q.type === 'technology' && q.itemType === techType).length
const targetLevel = currentLevel + upgradesInResearchQueue + upgradesInWaitingQueue + 1
// 检查是否达到等级上限(使用计算后的目标等级)
if (config.maxLevel !== undefined && targetLevel > config.maxLevel) {
return false
}
// 检查目标等级的前置条件是否满足
// 如果该科技已经在队列中(正式或等待),说明基本条件已满足,跳过检查
const alreadyInQueue = upgradesInResearchQueue > 0 || upgradesInWaitingQueue > 0
if (!alreadyInQueue) {
// 第一次添加时,检查当前等级+1的前置条件
if (!checkUpgradeRequirements(techType)) {
return false
}
} else {
// 后续添加时,检查目标等级的前置条件
const requirements = publicLogic.getLevelRequirements(config, targetLevel)
if (requirements && Object.keys(requirements).length > 0) {
if (!publicLogic.checkRequirements(planet.value, gameStore.player.technologies, requirements)) {
return false
}
}
}
// 科技可以多次排队比如能源技术升级到2、3、4、5级
// 只需要检查等待队列是否已满
const maxWaitingQueue = waitingQueueLogic.getMaxResearchWaitingQueue(player.value.technologies)
if (waitingQueue.length >= maxWaitingQueue) {
return false
}
return true
}
// 添加到等待队列
const handleAddToWaiting = (techType: TechnologyType, event: MouseEvent) => {
const currentLevel = getTechLevel(techType)
// 计算目标等级:当前等级 + 正式队列中的升级数 + 等待队列中的升级数 + 1
const upgradesInResearchQueue = player.value.researchQueue.filter(q => q.type === 'technology' && q.itemType === techType).length
const waitingQueue = player.value.waitingResearchQueue || []
const upgradesInWaitingQueue = waitingQueue.filter(q => q.type === 'technology' && q.itemType === techType).length
const targetLevel = currentLevel + upgradesInResearchQueue + upgradesInWaitingQueue + 1
const item = waitingQueueLogic.createResearchWaitingItem(techType, targetLevel)
const result = waitingQueueLogic.canAddToResearchWaitingQueue(player.value, item)
if (!result.canAdd) {
alertDialogTitle.value = t('queue.waitingQueueFull')
alertDialogMessage.value = result.reason ? t(result.reason) : ''
alertDialogShowRequirements.value = false
alertDialogOpen.value = true
return
}
// 触发抛物线动画
triggerQueueAnimation(event, 'technology')
waitingQueueLogic.addToResearchWaitingQueue(player.value, item)
}
</script>

View File

@@ -169,6 +169,14 @@
@update:checked="(val: boolean) => updateTypeSetting('research', val)"
/>
</div>
<!-- 解锁通知 -->
<div class="flex items-center justify-between">
<Label class="font-normal cursor-pointer" @click="toggleType('unlock')">{{ t('settings.unlockNotification') }}</Label>
<Switch
:checked="gameStore.notificationSettings?.types.unlock"
@update:checked="(val: boolean) => updateTypeSetting('unlock', val)"
/>
</div>
</div>
</div>
</CardContent>
@@ -338,8 +346,8 @@
import pkg from '../../package.json'
import { checkLatestVersion, canCheckVersion } from '@/utils/versionCheck'
import type { VersionInfo } from '@/utils/versionCheck'
import UpdateDialog from '@/components/UpdateDialog.vue'
import PrivacyDialog from '@/components/PrivacyDialog.vue'
import UpdateDialog from '@/components/dialogs/UpdateDialog.vue'
import PrivacyDialog from '@/components/dialogs/PrivacyDialog.vue'
import { useHints } from '@/composables/useHints'
const { t } = useI18n()
@@ -364,7 +372,7 @@
browser: false,
inApp: true,
suppressInFocus: false,
types: { construction: true, research: true }
types: { construction: true, research: true, unlock: true }
}
}
@@ -390,13 +398,13 @@
}
}
const updateTypeSetting = (key: 'construction' | 'research', val: boolean) => {
const updateTypeSetting = (key: 'construction' | 'research' | 'unlock', val: boolean) => {
if (gameStore.notificationSettings) {
gameStore.notificationSettings.types[key] = val
}
}
const toggleType = (key: 'construction' | 'research') => {
const toggleType = (key: 'construction' | 'research' | 'unlock') => {
if (gameStore.notificationSettings) {
const current = gameStore.notificationSettings.types[key]
gameStore.notificationSettings.types[key] = !current

View File

@@ -122,7 +122,17 @@
</div>
</div>
<Button @click="handleBuild(shipType)" :disabled="!canBuild(shipType)" class="w-full">{{ t('shipyardView.build') }}</Button>
<Button @click="handleBuild(shipType, $event)" :disabled="!canBuild(shipType)" class="w-full">{{ t('shipyardView.build') }}</Button>
<!-- 添加到等待队列按钮 -->
<Button
v-if="canAddToWaitingQueue(shipType)"
@click="handleAddToWaiting(shipType, $event)"
variant="outline"
class="w-full"
>
{{ t('queue.addToWaiting') }}
</Button>
</div>
</CardContent>
</Card>
@@ -157,7 +167,7 @@
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import ResourceIcon from '@/components/ResourceIcon.vue'
import ResourceIcon from '@/components/common/ResourceIcon.vue'
import {
AlertDialog,
AlertDialogAction,
@@ -167,14 +177,17 @@
AlertDialogHeader,
AlertDialogTitle
} from '@/components/ui/alert-dialog'
import UnlockRequirement from '@/components/UnlockRequirement.vue'
import CardUnlockOverlay from '@/components/CardUnlockOverlay.vue'
import UnlockRequirement from '@/components/common/UnlockRequirement.vue'
import CardUnlockOverlay from '@/components/common/CardUnlockOverlay.vue'
import { formatNumber, getResourceCostColor } from '@/utils/format'
import * as shipValidation from '@/logic/shipValidation'
import * as publicLogic from '@/logic/publicLogic'
import * as fleetStorageLogic from '@/logic/fleetStorageLogic'
import * as shipLogic from '@/logic/shipLogic'
import * as gameLogic from '@/logic/gameLogic'
import * as waitingQueueLogic from '@/logic/waitingQueueLogic'
import * as officerLogic from '@/logic/officerLogic'
import { triggerQueueAnimation } from '@/composables/useQueueAnimation'
const gameStore = useGameStore()
const detailDialog = useDetailDialogStore()
@@ -241,7 +254,7 @@
}
// 建造舰船
const handleBuild = (shipType: ShipType) => {
const handleBuild = (shipType: ShipType, event: MouseEvent) => {
const quantity = quantities.value[shipType]
if (quantity <= 0) {
alertDialogTitle.value = t('shipyardView.inputError')
@@ -256,6 +269,8 @@
alertDialogMessage.value = result.reason ? t(result.reason) : t('shipyardView.buildFailedMessage')
alertDialogOpen.value = true
} else {
// 触发抛物线动画
triggerQueueAnimation(event, 'ship')
quantities.value[shipType] = 0
}
}
@@ -300,4 +315,58 @@
darkMatter: config.cost.darkMatter * quantity
}
}
// 检查是否可以添加到等待队列
const canAddToWaitingQueue = (shipType: ShipType): boolean => {
if (!planet.value) return false
const quantity = quantities.value[shipType]
if (quantity <= 0) return false
// 检查前置条件是否满足
const config = SHIPS.value[shipType]
if (!publicLogic.checkRequirements(planet.value, gameStore.player.technologies, config.requirements)) {
return false
}
// 检查舰队仓储空间是否足够
if (!fleetStorageLogic.hasEnoughFleetStorage(planet.value, shipType, quantity, gameStore.player.technologies)) {
return false
}
// 检查等待队列是否已满
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
const maxWaitingQueue = waitingQueueLogic.getMaxBuildWaitingQueue(planet.value, bonuses.additionalBuildQueue)
const waitingQueue = planet.value.waitingBuildQueue || []
if (waitingQueue.length >= maxWaitingQueue) {
return false
}
// 只有当建造按钮被禁用时(资源不足)才显示等待队列按钮
return !canBuild(shipType)
}
// 添加到等待队列
const handleAddToWaiting = (shipType: ShipType, event: MouseEvent) => {
if (!planet.value) return
const quantity = quantities.value[shipType]
if (quantity <= 0) return
const item = waitingQueueLogic.createShipWaitingItem(shipType, quantity, planet.value.id)
const result = waitingQueueLogic.canAddToBuildWaitingQueue(planet.value, item, gameStore.player.officers)
if (!result.canAdd) {
alertDialogTitle.value = t('queue.waitingQueueFull')
alertDialogMessage.value = result.reason ? t(result.reason) : ''
alertDialogOpen.value = true
return
}
// 触发抛物线动画
triggerQueueAnimation(event, 'ship')
waitingQueueLogic.addToBuildWaitingQueue(planet.value, item)
quantities.value[shipType] = 0
}
</script>