mirror of
https://github.com/setube/ogame-vue-ts.git
synced 2026-05-12 16:05:12 +08:00
feat: 新增多语言README并优化文档结构
新增德语、俄语、韩语、繁体中文多语言README,英文与简体中文README同步优化,统一下载链接与徽章样式,完善多语言入口。提升国际化支持与文档可读性。
This commit is contained in:
@@ -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'
|
||||
|
||||
|
||||
@@ -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
454
src/views/CampaignView.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
187
src/views/RankingView.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user