+
@@ -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
diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue
index 1b17ebb..6b3a452 100644
--- a/src/views/HomeView.vue
+++ b/src/views/HomeView.vue
@@ -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()
diff --git a/src/views/MessagesView.vue b/src/views/MessagesView.vue
index 1b96fc5..ce90e0d 100644
--- a/src/views/MessagesView.vue
+++ b/src/views/MessagesView.vue
@@ -62,14 +62,14 @@
-
+
{{ t('messagesView.battleReport') }}
{{ t('messagesView.unread') }}
{{ getBattleResultText(report) }}
-
+
@@ -99,12 +99,12 @@
-
+
{{ t('messagesView.spyReport') }}
{{ t('messagesView.unread') }}
- {{ report.targetPlanetId }}
+ {{ getSpyReportTargetName(report) }}
-
+
@@ -124,19 +124,20 @@
-
+
{{ t('messagesView.spiedNotification') }}
{{ t('messagesView.unread') }}
{{ notification.detectionSuccess ? t('messagesView.detected') : t('messagesView.undetected') }}
-
+
- {{ notification.npcName }} → {{ notification.targetPlanetName }} · {{ formatDate(notification.timestamp) }}
+ {{ getNpcName(notification.npcId, notification.npcName) }} → {{ notification.targetPlanetName }} ·
+ {{ formatDate(notification.timestamp) }}
@@ -168,21 +169,16 @@
-
+
{{ t('messagesView.npcRecycleActivity') }}
{{ t('messagesView.unread') }}
-
+
- {{ notification.npcName }} →
+ {{ getNpcName(notification.npcId, notification.npcName) }} →
{{
notification.targetPlanetName ||
`[${notification.targetPosition.galaxy}:${notification.targetPosition.system}:${notification.targetPosition.position}]`
@@ -202,11 +198,13 @@
-
- {{ t('messagesView.giftFrom').replace('{npcName}', gift.fromNpcName) }}
+
+
+ {{ t('messagesView.giftFrom').replace('{npcName}', getNpcName(gift.fromNpcId, gift.fromNpcName)) }}
+
{{ t('messagesView.unread') }}
-
+
@@ -252,13 +250,13 @@
-
+
- {{ t('messagesView.giftRejectedBy').replace('{npcName}', rejection.npcName) }}
+ {{ t('messagesView.giftRejectedBy').replace('{npcName}', getNpcName(rejection.npcId, rejection.npcName)) }}
{{ t('messagesView.unread') }}
-
+
@@ -306,14 +304,14 @@
-
+
{{ getMissionTypeName(report.missionType) }}
{{ t('messagesView.unread') }}
{{ report.success ? t('messagesView.success') : t('messagesView.failed') }}
-
+
@@ -339,322 +337,14 @@
-
-
-
-
-
-
- {{ t('messagesView.spiedNotificationDetails') }}
-
-
- {{ t('messagesView.spyDetected') }}
-
-
+
+
-
-
-
-
-
{{ selectedSpiedNotification.npcName }}
- {{ t('messagesView.spyDetected') }}
-
-
- {{ formatDate(selectedSpiedNotification.timestamp) }}
-
-
+
+
-
-
-
{{ t('messagesView.targetPlanet') }}
-
-
- {{ selectedSpiedNotification.targetPlanetName }}
-
-
-
-
-
-
{{ t('messagesView.detectionResult') }}
-
-
-
-
{{ t('messagesView.detectionSuccess') }}
-
-
- {{
- t('messagesView.spiedNotificationMessage', {
- npc: selectedSpiedNotification.npcName,
- planet: selectedSpiedNotification.targetPlanetName
- })
- }}
-
-
-
-
-
-
-
- {{ t('messagesView.spiedNotificationTip') }}
-
-
-
-
-
- {{ t('common.close') }}
- {{ t('messagesView.viewInGalaxy') }}
-
-
-
-
-
-
-
-
-
-
- {{ t('messagesView.missionReportDetails') }}
-
-
- {{ t('messagesView.missionDetails') }}
-
-
-
-
-
-
-
-
{{ getMissionTypeName(selectedMissionReport.missionType) }}
-
- {{ selectedMissionReport.success ? t('messagesView.missionSuccess') : t('messagesView.missionFailed') }}
-
-
-
- {{ formatDate(selectedMissionReport.timestamp) }}
-
-
-
-
-
-
-
{{ t('messagesView.origin') }}
-
-
{{ selectedMissionReport.originPlanetName }}
-
-
-
-
{{ t('messagesView.destination') }}
-
-
{{ selectedMissionReport.targetPlanetName }}
-
- [{{ selectedMissionReport.targetPosition.galaxy }}:{{ selectedMissionReport.targetPosition.system }}:{{
- selectedMissionReport.targetPosition.position
- }}]
-
-
-
-
-
-
-
-
{{ t('messagesView.missionDetails') }}
-
-
{{ selectedMissionReport.message }}
-
-
-
-
{{ t('messagesView.transportedResources') }}:
-
-
- {{ t(`resources.${res.key}`) }}: {{ selectedMissionReport.details.transportedResources[res.key].toLocaleString() }}
-
-
-
-
-
-
-
{{ t('messagesView.recycledResources') }}:
-
-
- {{ t(`resources.${res.key}`) }}: {{ selectedMissionReport.details.recycledResources[res.key].toLocaleString() }}
-
-
-
-
{{ t('messagesView.remainingDebris') }}:
-
-
- {{ t(`resources.${res.key}`) }}: {{ selectedMissionReport.details.remainingDebris[res.key].toLocaleString() }}
-
-
-
-
-
-
-
-
{{ t('messagesView.newPlanet') }}:
-
-
- {{ selectedMissionReport.details.newPlanetName }}
-
-
-
-
-
-
{{ t('galaxyView.missileAttack') }}:
-
-
- {{ t('galaxyView.missileCount') }}:
- {{ selectedMissionReport.details.missileCount }}
-
-
- {{ t('missionReports.hits') }}:
- {{ selectedMissionReport.details.missileHits }}
-
-
- {{ t('galaxyView.intercepted') }}:
- {{ selectedMissionReport.details.missileIntercepted }}
-
-
-
-
{{ t('galaxyView.defenseLosses') }}:
-
-
- {{ t('defenses.' + defenseType) }}:
- -{{ count }}
-
-
-
-
-
-
-
-
{{ t('messagesView.resources') }}:
-
-
-
- {{ t(`resources.${res.key}`) }}:
-
- +{{ (selectedMissionReport.details?.foundResources?.[res.key] ?? 0).toLocaleString() }}
-
-
-
-
-
-
-
-
-
{{ t('messagesView.fleet') }}:
-
-
- {{ t('ships.' + shipType) }}:
- +{{ count }}
-
-
-
-
-
-
-
{{ t('messagesView.attackerLosses') }}:
-
-
- {{ t('ships.' + shipType) }}:
- -{{ count }}
-
-
-
-
-
-
-
-
- {{ t('common.close') }}
-
-
-
-
-
-
-
-
-
-
- {{ t('messagesView.npcActivityDetails') }}
-
-
- {{ t('messagesView.activityDescription') }}
-
-
-
-
-
-
-
-
{{ selectedNPCActivityNotification.npcName }}
- {{ t('messagesView.activityType.' + selectedNPCActivityNotification.activityType) }}
-
-
- {{ formatDate(selectedNPCActivityNotification.timestamp) }}
-
-
-
-
-
-
{{ t('messagesView.activityLocation') }}
-
-
-
-
- {{ t('messagesView.position') }}: [{{ selectedNPCActivityNotification.targetPosition.galaxy }}:{{
- selectedNPCActivityNotification.targetPosition.system
- }}:{{ selectedNPCActivityNotification.targetPosition.position }}]
-
-
-
- {{ t('messagesView.nearPlanet') }}: {{ selectedNPCActivityNotification.targetPlanetName }}
-
-
-
-
-
-
-
{{ t('messagesView.activityDescription') }}
-
-
- {{
- t('messagesView.npcActivityMessage', {
- npc: selectedNPCActivityNotification.npcName,
- activity: t('messagesView.activityType.' + selectedNPCActivityNotification.activityType),
- position: `[${selectedNPCActivityNotification.targetPosition.galaxy}:${selectedNPCActivityNotification.targetPosition.system}:${selectedNPCActivityNotification.targetPosition.position}]`
- })
- }}
-
-
-
-
-
-
-
{{ t('messagesView.arrivalTime') }}
-
-
{{ formatDate(selectedNPCActivityNotification.arrivalTime) }}
-
-
-
-
-
-
- {{ t('messagesView.npcActivityTip') }}
-
-
-
-
-
- {{ t('common.close') }}
-
- {{ t('messagesView.viewInGalaxy') }}
-
-
-
-
+
+
@@ -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
- }
- }
diff --git a/src/views/OfficersView.vue b/src/views/OfficersView.vue
index a2bd7e6..617e796 100644
--- a/src/views/OfficersView.vue
+++ b/src/views/OfficersView.vue
@@ -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,
diff --git a/src/views/OverviewView.vue b/src/views/OverviewView.vue
index f6ad777..1cca6d2 100644
--- a/src/views/OverviewView.vue
+++ b/src/views/OverviewView.vue
@@ -9,6 +9,10 @@
{{ t('planet.position') }}: [{{ planet.position.galaxy }}:{{ planet.position.system }}:{{ planet.position.position }}]
+
+
+ {{ t('planet.temperature') }}: {{ planet.temperature.min }}°C {{ t('common.to') }} {{ planet.temperature.max }}°C
+
@@ -28,11 +32,10 @@
- 概览
- 产量详情
- 消耗详情
+ {{ t('overview.tabOverview') }}
+ {{ t('overview.tabProduction') }}
+ {{ t('overview.tabConsumption') }}
-
@@ -177,7 +180,7 @@
{{ t('overview.currentShips') }}
-
+
{{ SHIPS[shipType].name }}
{{ count }}
@@ -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(() => {
diff --git a/src/views/RankingView.vue b/src/views/RankingView.vue
new file mode 100644
index 0000000..b26f805
--- /dev/null
+++ b/src/views/RankingView.vue
@@ -0,0 +1,187 @@
+
+
+
+
{{ t('ranking.title') }}
+
+
+ {{ t('ranking.totalPlayers', { count: rankingData.length }) }}
+
+
+
+
+
+
+ {{ t('ranking.yourRanking') }}
+ #{{ playerRank }}
+ / {{ rankingData.length }}
+ {{ formatNumber(playerScore) }}
+
+
+
+
+
+
+
+ {{ t(`ranking.categories.${category.value}`) }}
+
+
+
+
+
+
+
+
+ #
+ {{ t('ranking.name') }}
+ {{ t(`ranking.categories.${activeCategory}`) }}
+ {{ t('ranking.planets') }}
+
+
+
+
+
+
+ {{ getActualRank(index) }}
+
+
+
+
+
+ {{ entry.name }}
+
+
+ {{ t('ranking.you') }}
+
+
+
+
+ {{ formatNumber(entry.scores[activeCategory]) }}
+
+
+
+
+ {{ entry.planetCount }}
+
+
+
+
+
+ {{ t('ranking.noData') }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/ResearchView.vue b/src/views/ResearchView.vue
index 8dab7b2..b558cb1 100644
--- a/src/views/ResearchView.vue
+++ b/src/views/ResearchView.vue
@@ -50,11 +50,21 @@
+
+
+
+ {{ formatTime(getResearchTime(techType)) }}
+
-
+
{{ getResearchButtonText(techType) }}
+
+
+
+ {{ t('queue.addToWaiting') }}
+
@@ -71,8 +81,8 @@
-
-
+
+
{{ req.name }}: Lv {{ req.requiredLevel }} ({{ t('common.current') }}: Lv {{ req.currentLevel }})
@@ -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)
+ }
diff --git a/src/views/SettingsView.vue b/src/views/SettingsView.vue
index a952d7f..44e6aaa 100644
--- a/src/views/SettingsView.vue
+++ b/src/views/SettingsView.vue
@@ -169,6 +169,14 @@
@update:checked="(val: boolean) => updateTypeSetting('research', val)"
/>
+
+
+ {{ t('settings.unlockNotification') }}
+ updateTypeSetting('unlock', val)"
+ />
+
@@ -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
diff --git a/src/views/ShipyardView.vue b/src/views/ShipyardView.vue
index 3158155..196531c 100644
--- a/src/views/ShipyardView.vue
+++ b/src/views/ShipyardView.vue
@@ -122,7 +122,17 @@
- {{ t('shipyardView.build') }}
+ {{ t('shipyardView.build') }}
+
+
+
+ {{ t('queue.addToWaiting') }}
+
@@ -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
+ }
diff --git a/vite.config.ts b/vite.config.ts
index 9d4636b..d786f32 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -17,7 +17,7 @@ export default defineConfig(async () => {
manifest: {
name: pkg.name,
short_name: pkg.title,
- description: '征服星辰大海',
+ description: 'Conquer the stars',
theme_color: '#000000',
background_color: '#000000',
display: 'fullscreen',